diff --git a/.coveragerc b/.coveragerc index d5127a71c..613a23c23 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,12 +1,15 @@ # .coveragerc to control coverage.py [run] branch = True -source = datajunction -# omit = bad_file.py +source = dj +omit = + */dj/sql/parsing/backends/grammar/generated/* + */dj/sql/parsing/backends/antlr4.py + */dj/sql/parsing/ast.py [paths] source = - src/ + dj/ */site-packages/ [report] @@ -26,3 +29,5 @@ exclude_lines = # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: + + if TYPE_CHECKING: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..b2ecf68fd --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +### Summary + + + +### Test Plan + + + +- [ ] PR has an associated issue: # +- [ ] `make check` passes +- [ ] `make test` shows 100% unit test coverage + +### Deployment Plan + + diff --git a/.github/workflows/client-integration-tests.yml b/.github/workflows/client-integration-tests.yml new file mode 100644 index 000000000..610ea8fab --- /dev/null +++ b/.github/workflows/client-integration-tests.yml @@ -0,0 +1,114 @@ +name: "Manual : Run client integration tests" +on: + schedule: + - cron: '0 12 * * *' + workflow_dispatch: +jobs: + python-client-integration: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.10', '3.11'] + steps: + - uses: actions/checkout@v2 + - name: Build and launch DJ demo environment + uses: hoverkraft-tech/compose-action@v2.0.1 + with: + compose-file: "./docker-compose.yml" + services: | + dj + postgres_metadata + db-migration + db-seed + djqs + djqs-db-migration + djrs-redis + djrs-worker + djrs-beat + dj-trino + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - uses: pdm-project/setup-pdm@v3 + name: Setup PDM + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + prerelease: true + enable-pep582: true + - name: Install dependencies + run: | + pdm sync -d + cd ./datajunction-clients/python; pdm install -d -G pandas + - name: Python client integration tests + run: cd datajunction-clients/python && make test PYTEST_ARGS="--integration -k test_integration" + + javascript-client-integration: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: + - uses: actions/checkout@v3 + - name: Build and launch DJ demo environment + uses: hoverkraft-tech/compose-action@v2.0.1 + with: + compose-file: "./docker-compose.yml" + services: | + dj + postgres_metadata + db-migration + db-seed + djqs + djqs-db-migration + djrs-redis + djrs-worker + djrs-beat + dj-trino + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: Install Dev Dependencies + run: npm install --only=dev + working-directory: ./datajunction-clients/javascript + - name: Javascript client integration tests + run: npm test + working-directory: ./datajunction-clients/javascript + + java-client-integration: + runs-on: ubuntu-latest + strategy: + matrix: + java-version: [ 17 ] + distribution: [ 'temurin' ] + steps: + - uses: actions/checkout@v3 + - name: Build and launch DJ demo environment + uses: hoverkraft-tech/compose-action@v2.0.1 + with: + compose-file: "./docker-compose.yml" + services: | + dj + postgres_metadata + db-migration + db-seed + djqs + djqs-db-migration + djrs-redis + djrs-worker + djrs-beat + dj-trino + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@v3 + with: + java-version: ${{ matrix.java-version }} + distribution: ${{ matrix.distribution }} + - name: Build with Gradle + uses: gradle/gradle-build-action@v3 + with: + arguments: cleanTest test + build-root-directory: ./datajunction-clients/java + diff --git a/.github/workflows/generate-openapi-client.yml b/.github/workflows/generate-openapi-client.yml new file mode 100644 index 000000000..b21a5904f --- /dev/null +++ b/.github/workflows/generate-openapi-client.yml @@ -0,0 +1,65 @@ +name: "Manual : Generate OpenAPI client" +on: + workflow_dispatch: +jobs: + generate-python-client: + env: + PDM_DEPS: 'urllib3<2' + runs-on: ubuntu-latest + name: Generate Python Client + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install DJ + run: | + python -m pip install --upgrade pip + pip install . + + - name: Generate OpenAPI Spec + run: ./scripts/generate-openapi.py -o openapi.json + + - name: Configure Git + run: | + git config user.name "GitHub Actions Bot" + git config user.email "<>" + + - name: Commit OpenAPI Spec + run: | + git add openapi.json + git commit -m "Updating OpenAPI Spec" + + - name: Generate Python client + uses: openapi-generators/openapitools-generator-action@v1.4.0 + with: + generator: python + openapi-file: openapi.json + config-file: ./.github/files/python-client-gen.yml + command-args: --skip-validate-spec + + - name: Move client to right directory + run: | + mkdir -p ./openapi/python + cp -r python-client/* ./openapi/python/ + rm -rf python-client + + - name: Set short sha + id: sha + run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Commit generated files + run: | + git add openapi/python/ + git commit -m "Update DJ Python client" + git checkout -b ci-pr/python-client-${{ steps.sha.outputs.short_sha }} + git push --set-upstream origin ci-pr/python-client-${{ steps.sha.outputs.short_sha }} + + - name: Open a PR + run: gh pr create -B main -H ci-pr/python-client-${{ steps.sha.outputs.short_sha }} --title 'Update Python Client - ${{ steps.sha.outputs.short_sha }}' --body '(This PR was generated by a GitHub action)' + env: + GITHUB_TOKEN: ${{ secrets.REPO_SCOPED_TOKEN }} diff --git a/.github/workflows/generate-openapi-spec.yml b/.github/workflows/generate-openapi-spec.yml new file mode 100644 index 000000000..fc4e72f7f --- /dev/null +++ b/.github/workflows/generate-openapi-spec.yml @@ -0,0 +1,61 @@ +name: "Manual : Generate OpenAPI spec" +on: + workflow_dispatch: +jobs: + generate-openapi-spec: + env: + PDM_DEPS: 'urllib3<2' + defaults: + run: + working-directory: ./datajunction-server + runs-on: ubuntu-latest + name: OpenAPI Spec + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: 19 + + - name: Install DJ + run: | + python -m pip install --upgrade pip + pip install . + + - name: Generate OpenAPI Spec + run: | + ./scripts/generate-openapi.py -o ../openapi.json + + - name: Generate Markdown Docs from Spec + run: | + npm install -g widdershins + widdershins ../openapi.json -o ../docs/content/0.1.0/docs/developers/the-datajunction-api-specification.md --code=true --omitBody=true --summary=true + + - name: Configure Git + run: | + git config user.name "GitHub Actions Bot" + git config user.email "<>" + + - name: Set short sha + id: sha + run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Commit OpenAPI Spec + run: | + git add ../openapi.json + git add ../docs/content/0.1.0/docs/developers/the-datajunction-api-specification.md + git commit -m "Updating OpenAPI Spec" + git checkout -b ci-pr/python-client-${{ steps.sha.outputs.short_sha }} + git push --set-upstream origin ci-pr/python-client-${{ steps.sha.outputs.short_sha }} + + - name: Open a PR + run: gh pr create -B main -H ci-pr/python-client-${{ steps.sha.outputs.short_sha }} --title 'Update OpenAPI Spec - ${{ steps.sha.outputs.short_sha }}' --body '(This PR was generated by a GitHub action)' + env: + GITHUB_TOKEN: ${{ secrets.REPO_SCOPED_TOKEN }} diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package-daily.yml similarity index 65% rename from .github/workflows/python-package.yml rename to .github/workflows/python-package-daily.yml index fdcc0287f..1b2b39108 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package-daily.yml @@ -1,22 +1,20 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: Python package +name: "@Daily : Run tests with latest dependencies" on: - push: - branches: [ main ] - pull_request: - branches: [ main ] + schedule: + - cron: '0 6 * * *' jobs: - build: + daily: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, '3.10'] + python-version: ['3.10', '3.11.0rc1'] steps: - uses: actions/checkout@v2 @@ -24,10 +22,11 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install latest dependencies run: | python -m pip install --upgrade pip pip install -e '.[testing]' - name: Test with pytest run: | - pytest --cov=src/datajunction -vv tests/ --doctest-modules src/datajunction + pre-commit run --all-files + pytest --cov-fail-under=100 --cov=dj -vv tests/ --doctest-modules dj --without-integration --without-slow-integration diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..f9f516c72 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,163 @@ +name: "PR Update : Run tests and linters" + +on: + pull_request: + branches: [ main ] + paths-ignore: + # python + - datajunction-clients/python/__about__.py + - datajunction-server/datajunction_server/__about__.py + - datajunction-query/djqs/__about__.py + - datajunction-reflection/datajunction_reflection/__about__.py + # javascript + - datajunction-clients/javascript/package.json + - datajunction-ui/package.json + # java: TODO + push: + branches: [ main ] + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + env: + PDM_DEPS: 'urllib3<2' + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: ['3.10', '3.11'] + library: ['client', 'server', 'djqs', 'djrs'] + + defaults: + run: + working-directory: ./ + + steps: + - uses: actions/checkout@v2 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + client: + - 'datajunction-clients/python/**' + # run Python unit tests also when server is updated (because server changes can break the client) + - 'datajunction-server/**' + - '!**/__about__.py' + server: + - 'datajunction-server/**' + - '!**/__about__.py' + djqs: + - 'datajunction-query/**' + - '!**/__about__.py' + djrs: + - 'datajunction-reflection/**' + - '!**/__about__.py' + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - uses: pdm-project/setup-pdm@v3 + name: Setup PDM + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + prerelease: true + enable-pep582: true + + - uses: pre-commit/action@v3.0.0 + name: Force check of all pdm.lock files + with: + extra_args: pdm-lock-check --all-files + + - name: Run Tests + if: | + (matrix.library == 'client' && steps.filter.outputs.client == 'true') || + (matrix.library == 'server' && steps.filter.outputs.server == 'true') || + (matrix.library == 'djqs' && steps.filter.outputs.djqs == 'true') || + (matrix.library == 'djrs' && steps.filter.outputs.djrs == 'true') + run: | + echo "Testing ${{ matrix.library }} ..." + export TEST_DIR=${{ matrix.library == 'server' && './datajunction-server' || matrix.library == 'client' && './datajunction-clients/python' || matrix.library == 'djqs' && './datajunction-query' || matrix.library == 'djrs' && './datajunction-reflection'}} + + # Install dependencies + pdm sync -d; cd $TEST_DIR; pdm install -d -G pandas -G transpilation -G test; + + # Run linters + pdm run pre-commit run --all-files + + # Run tests + export MODULE=${{ matrix.library == 'server' && 'datajunction_server' || matrix.library == 'client' && 'datajunction' || matrix.library == 'djqs' && 'djqs' || matrix.library == 'djrs' && 'datajunction_reflection'}} + pdm run pytest ${{ (matrix.library == 'server' || matrix.library == 'client') && '-n auto' || '' }} --cov-fail-under=100 --cov=$MODULE --cov-report term-missing -vv tests/ --doctest-modules $MODULE --without-integration --without-slow-integration --ignore=datajunction_server/alembic/env.py + + build-javascript: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: Install Dev Dependencies + run: npm install --only=dev + working-directory: ./datajunction-clients/javascript + - name: Build Javascript Client + run: npm run build + working-directory: ./datajunction-clients/javascript + - name: Lint Javascript Client + run: npm run lint + working-directory: ./datajunction-clients/javascript + + build-ui: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./datajunction-ui + strategy: + matrix: + node-version: [19.x] + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: Install Dependencies + run: yarn install + - name: Run Lint + run: yarn lint --fix + - name: Run Prettier + run: yarn prettier . --write + - name: Run Unit Tests + run: yarn test --runInBand --ci --coverage + - name: Build Project + run: yarn webpack-build + + build-java: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./datajunction-clients/java + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Official Gradle Wrapper Validation Action + uses: gradle/wrapper-validation-action@v1 + - name: Build with Gradle + uses: gradle/gradle-build-action@v2 + with: + arguments: build -x test + build-root-directory: ./datajunction-clients/java \ No newline at end of file diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 000000000..b9f99a27b --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,120 @@ +name: "@Weekly + Manual: Bump version of all components." +on: + schedule: + - cron: '0 3 * * MON' # each Monday at 3am + workflow_dispatch: + inputs: + bump: + type: choice + description: "Select version cycle" + required: true + default: alpha + options: + # Uncomment next item when ready to switch: + - alpha + # - patch + # - minor + # - major + +jobs: + bump: + env: + bump-type: ${{ github.event.inputs.bump || 'alpha' }} # Default to alpha + strategy: + fail-fast: false + matrix: + python-version: ['3.10'] + runs-on: 'ubuntu-latest' + defaults: + run: + working-directory: . + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Hatch + run: | + python -m pip install --upgrade pip + pip install hatch + + - name: Configure Git + run: | + git config user.name "GitHub Actions Bot" + git config user.email "<>" + + # + # Python / hatch + # + - name: Bump version for DJ Server + working-directory: ./datajunction-server + run: | + hatch version ${{ env.bump-type }} + echo "NEW_VERSION=`hatch version`" >> $GITHUB_ENV + + - name: Bump version for DJ Query service + working-directory: ./datajunction-query + run: | + hatch version ${{ env.bump-type }} + + - name: Bump version for DJ Reflection service + working-directory: ./datajunction-reflection + run: | + hatch version ${{ env.bump-type }} + + - name: Bump version for DJ Python client + working-directory: ./datajunction-clients/python + run: | + hatch version ${{ env.bump-type }} + + # + # Javascript / npm + # + - name: Bump version for DJ UI + working-directory: ./datajunction-ui + run: | + yarn version --new-version $NEW_VERSION --no-git-tag-version + + - name: Bump version for DJ Javascript client + working-directory: ./datajunction-clients/javascript + run: | + yarn version --new-version $NEW_VERSION --no-git-tag-version + + # + # Docs (after alpha) + # + - name: Update docs (for major, minor or patch release) + working-directory: ./docs + if: ${{ env.bump-type == 'major' || env.bump-type == 'minor' || env.bump-type == 'patch'}} + run: | + ./build-docs.sh $NEW_VERSION true + + # + # Pull request + # + - name: Open a PR + run: | + echo "Make a commit ..." + git add ./datajunction-clients/python/datajunction/__about__.py + git add ./datajunction-clients/javascript/package.json + git add ./datajunction-query/djqs/__about__.py + git add ./datajunction-reflection/datajunction_reflection/__about__.py + git add ./datajunction-server/datajunction_server/__about__.py + git add ./datajunction-ui/package.json + git add ./docs + git commit -m "Bumping DJ to version $NEW_VERSION" + git checkout -b releases/version-$NEW_VERSION + git push --set-upstream origin releases/version-$NEW_VERSION -f + + echo "Make a tag ..." + git tag -a version-$NEW_VERSION -m version-$NEW_VERSION + git push origin version-$NEW_VERSION + + echo "Create a PR ..." + gh pr create -B main -H "releases/version-$NEW_VERSION" --title "Bump DataJunction version to $NEW_VERSION" --body "This is an automated PR triggered by the github action. Merging this PR will publish all the component for version $NEW_VERSION ." + env: + GITHUB_TOKEN: ${{ secrets.REPO_SCOPED_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/version-publish.yml b/.github/workflows/version-publish.yml new file mode 100644 index 000000000..f282612ee --- /dev/null +++ b/.github/workflows/version-publish.yml @@ -0,0 +1,101 @@ +name: "PR Merge : Publish all components to Pypi/NPM" +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + # python + - datajunction-clients/python/__about__.py + - datajunction-server/datajunction_server/__about__.py + - datajunction-query/djqs/__about__.py + - datajunction-reflection/datajunction_reflection/__about__.py + # javascript + - datajunction-clients/javascript/package.json + - datajunction-ui/package.json + # java: TODO + +jobs: + publish: + strategy: + fail-fast: false + matrix: + python-version: ['3.10'] + runs-on: 'ubuntu-latest' + defaults: + run: + working-directory: . + env: + HATCH_INDEX_USER: __token__ + HATCH_INDEX_AUTH: ${{ secrets.PYPI_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Hatch + run: | + python -m pip install --upgrade pip + pip install hatch + + # + # Publish Python / hatch + # + - name: Publish DJ Server + working-directory: ./datajunction-server + run: | + hatch build + hatch publish + + - name: Publish DJ Query service + working-directory: ./datajunction-query + run: | + hatch build + hatch publish + + - name: Publish DJ Reflection service + working-directory: ./datajunction-reflection + run: | + hatch build + hatch publish + + - name: Publish DJ Python client + working-directory: ./datajunction-clients/python + run: | + hatch build + hatch publish + + # + # Publish Javascript / npm + # + - uses: actions/setup-node@v3 + with: + node-version: '20.x' + registry-url: 'https://registry.npmjs.org' + + - name: Publish DJ Javascript client + working-directory: ./datajunction-clients/javascript + run: | + export VERSION_IN_CODE=`cat package.json | jq -r '.version'` + export VERSION_IN_NPM=`npm view datajunction version | tr -d '-'` + # check if the current version hasn't been published yet + if [[ $VERSION_IN_CODE != $VERSION_IN_NPM ]]; then + yarn + npm publish + fi; + + - name: Publish DJ UI + working-directory: ./datajunction-ui + run: | + export VERSION_IN_CODE=`cat package.json | jq -r '.version'` + export VERSION_IN_NPM=`npm view datajunction-ui version | tr -d '-'` + # check if the current version hasn't been published yet + if [[ $VERSION_IN_CODE != $VERSION_IN_NPM ]]; then + yarn + npm publish + fi; + \ No newline at end of file diff --git a/.github/workflows/version-test.yml b/.github/workflows/version-test.yml new file mode 100644 index 000000000..44ffa365e --- /dev/null +++ b/.github/workflows/version-test.yml @@ -0,0 +1,82 @@ +name: "PR Update : Test matching versions" + +on: + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + env: + ALL_VERSIONS_MATCH: false + + steps: + - uses: actions/checkout@v2 + + - name: Install Hatch + run: | + python -m pip install --upgrade pip + pip install hatch + + # + # Collect from Python / hatch + # + - name: Find the version of DJ Server + working-directory: ./datajunction-server + run: + echo "DJ_SERVER_VERSION=`hatch version`" >> $GITHUB_ENV + + - name: Find the version of DJ Query service + working-directory: ./datajunction-query + run: + echo "DJ_QUERY_VERSION=`hatch version`" >> $GITHUB_ENV + + - name: Find the version of DJ Reflection service + working-directory: ./datajunction-reflection + run: + echo "DJ_REFLECTION_VERSION=`hatch version`" >> $GITHUB_ENV + + - name: Find the version of DJ Python client + working-directory: ./datajunction-clients/python + run: + echo "DJ_CLIENT_PY_VERSION=`hatch version`" >> $GITHUB_ENV + + # + # Collect from JavaScript / yarn + # + - name: Find the version of DJ UI + working-directory: ./datajunction-ui + run: + echo "DJ_UI_VERSION=`cat package.json | jq -r '.version'`" >> $GITHUB_ENV + + - name: Find the version of DJ Javascript client + working-directory: ./datajunction-clients/javascript + run: + echo "DJ_CLIENT_JS_VERSION=`cat package.json | jq -r '.version'`" >> $GITHUB_ENV + + # + # Collect from Java / gradle (TODO) + # + + # + # Evaluate + # + - name: All versions match! + if: ${{ env.DJ_SERVER_VERSION == env.DJ_QUERY_VERSION && env.DJ_SERVER_VERSION == env.DJ_REFLECTION_VERSION && env.DJ_SERVER_VERSION == env.DJ_UI_VERSION && env.DJ_SERVER_VERSION == env.DJ_CLIENT_PY_VERSION && env.DJ_SERVER_VERSION == env.DJ_CLIENT_JS_VERSION }} + run: | + echo "All versions match: ${DJ_SERVER_VERSION}" + echo "ALL_VERSIONS_MATCH=true" >> $GITHUB_ENV + + - name: Fail on mismatch + if: ${{ env.ALL_VERSIONS_MATCH == 'false' }} + run: | + echo "Mismatched component versions found" + echo " - DJ Server: ${DJ_SERVER_VERSION}" + echo " - DJ Query: ${DJ_QUERY_VERSION}" + echo " - DJ Reflection: ${DJ_REFLECTION_VERSION}" + echo " - DJ UI: ${DJ_UI_VERSION}" + echo " - DJ Client for Python: ${DJ_CLIENT_PY_VERSION}" + echo " - DJ Client for Javascript: ${DJ_CLIENT_JS_VERSION}" + exit 1 diff --git a/.gitignore b/.gitignore index 22e91ac24..e2342f151 100644 --- a/.gitignore +++ b/.gitignore @@ -81,9 +81,6 @@ celerybeat-schedule # SageMath parsed files *.sage.py -# dotenv -.env - # virtualenv .venv venv/ @@ -103,5 +100,27 @@ ENV/ .mypy_cache/ *.sqlite -*.db +dj.db +djqs.db *.swp + +# VS Code +.vscode + +# Idea +.idea + +# MacOS +.DS_Store +.pdm-python +.pdm.toml + +# oauth credentials +client_secret* + +# random notebooks +Untitled* +.notebook_executed + +# postgres +postgres_metadata diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..b4a7f0d05 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "docs/themes/doks"] + path = docs/themes/doks + url = https://github.com/h-enk/doks.git diff --git a/.isort.cfg b/.isort.cfg index c8a204d9c..8c960bf43 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,3 +1,3 @@ [settings] profile = black -known_first_party = datajunction +known_first_party = dj diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 539485f0e..385e146ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ -exclude: '^docs/conf.py' +files: ^datajunction-(server|query|reflection)/ +exclude: (^docs/|^openapi/|^datajunction-clients/python/|^datajunction-clients/javascript/|^datajunction-server/dj/sql/parsing/backends/grammar/generated|^README.md) repos: -- repo: git://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 hooks: - id: trailing-whitespace - - id: check-added-large-files - id: check-ast exclude: ^templates/ - id: check-json @@ -15,6 +15,7 @@ repos: - id: debug-statements exclude: ^templates/ - id: end-of-file-fixer + exclude: openapi.json - id: requirements-txt-fixer exclude: ^templates/ - id: mixed-line-ending @@ -32,12 +33,12 @@ repos: # ] - repo: https://github.com/pycqa/isort - rev: 5.7.0 + rev: 5.12.0 hooks: - id: isort - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.8.0 hooks: - id: black language_version: python3 @@ -50,7 +51,7 @@ repos: # - id: blacken-docs # additional_dependencies: [black] -- repo: https://gitlab.com/pycqa/flake8 +- repo: https://github.com/PyCQA/flake8 rev: 3.9.2 hooks: - id: flake8 @@ -59,7 +60,7 @@ repos: # additional_dependencies: [flake8-bugbear] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.910' # Use the sha / tag you want to point at + rev: 'v0.981' # Use the sha / tag you want to point at hooks: - id: mypy exclude: ^templates/ @@ -67,11 +68,10 @@ repos: - types-requests - types-freezegun - types-python-dateutil - - types-pkg_resources - - types-PyYAML + - types-setuptools - types-tabulate - repo: https://github.com/asottile/add-trailing-comma - rev: v2.1.0 + rev: v2.2.1 hooks: - id: add-trailing-comma #- repo: https://github.com/asottile/reorder_python_imports @@ -79,17 +79,55 @@ repos: # hooks: # - id: reorder-python-imports # args: [--application-directories=.:src] -- repo: https://github.com/hadialqattan/pycln - rev: v1.0.3 # Possible releases: https://github.com/hadialqattan/pycln/tags - hooks: - - id: pycln - args: [--config=pyproject.toml] - exclude: ^templates/ +## Removing this for now due to this bug: https://github.com/hadialqattan/pycln/issues/249 +# - repo: https://github.com/hadialqattan/pycln +# rev: v2.4.0 # Possible releases: https://github.com/hadialqattan/pycln/tags +# hooks: +# - id: pycln +# args: [--config=pyproject.toml] +# exclude: ^templates/ - repo: local hooks: - id: pylint name: pylint - entry: pylint + entry: pylint --disable=duplicate-code,use-implicit-booleaness-not-comparison language: system types: [python] exclude: ^templates/ +- repo: https://github.com/kynan/nbstripout + rev: 0.6.1 + hooks: + - id: nbstripout +- repo: https://github.com/tomcatling/black-nb + rev: "0.7" + hooks: + - id: black-nb + files: '\.ipynb$' +- repo: https://github.com/pdm-project/pdm + rev: 2.8.1 + hooks: + - id: pdm-lock-check + name: pdm-lock-check-root + entry: pdm lock --check --project . + files: ^pyproject.toml$ +- repo: https://github.com/pdm-project/pdm + rev: 2.18.2 + hooks: + - id: pdm-lock-check + name: pdm-lock-check-server + entry: pdm lock --check --project datajunction-server + files: ^datajunction-server/pyproject.toml$ +- repo: https://github.com/pdm-project/pdm + rev: 2.18.2 + hooks: + - id: pdm-lock-check + name: pdm-lock-check-query + entry: pdm lock --check --project datajunction-query + files: ^datajunction-query/pyproject.toml$ +- repo: https://github.com/pdm-project/pdm + rev: 2.18.2 + hooks: + - id: pdm-lock-check + name: pdm-lock-check-reflection + entry: pdm lock --check --project datajunction-reflection + files: ^datajunction-reflection/pyproject.toml$ diff --git a/.pylintrc b/.pylintrc index 928bc4e0a..5b6fccaa9 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,6 +1,6 @@ [MESSAGES CONTROL] -disable = - duplicate-code [MASTER] +# https://github.com/samuelcolvin/pydantic/issues/1961#issuecomment-759522422 +extension-pkg-whitelist=pydantic ignore=templates,docs diff --git a/AUTHORS.rst b/AUTHORS.rst index 147dafe54..dcde57edb 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -3,3 +3,9 @@ Contributors ============ * Beto Dealmeida +* Olek Gorajek +* Hamidreza Hashemi +* Ali Raza +* Sam Redai +* Nick Ouellet +* Yian Shang diff --git a/CHANGELOG.rst b/CHANGELOG.md similarity index 100% rename from CHANGELOG.rst rename to CHANGELOG.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..18c914718 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 57763db20..5473a5fe9 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,353 +1,435 @@ -.. todo:: THIS IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! - - The document assumes you are using a source repository service that promotes a - contribution model similar to `GitHub's fork and pull request workflow`_. - While this is true for the majority of services (like GitHub, GitLab, - BitBucket), it might not be the case for private repositories (e.g., when - using Gerrit). - - Also notice that the code examples might refer to GitHub URLs or the text - might use GitHub specific terminology (e.g., *Pull Request* instead of *Merge - Request*). - - Please make sure to check the document having these assumptions in mind - and update things accordingly. - -.. todo:: Provide the correct links/replacements at the bottom of the document. - -.. todo:: You might want to have a look on `PyScaffold's contributor's guide`_, - - especially if your project is open source. The text should be very similar to - this template, but there are a few extra contents that you might decide to - also include, like mentioning labels of your issue tracker or automated - releases. - - ============ Contributing ============ -Welcome to ``datajunction`` contributor's guide. - -This document focuses on getting any potential contributor familiarized -with the development processes, but `other kinds of contributions`_ are also -appreciated. - -If you are new to using git_ or have never collaborated in a project previously, -please have a look at `contribution-guide.org`_. Other resources are also -listed in the excellent `guide created by FreeCodeCamp`_ [#contrib1]_. - -Please notice, all users and contributors are expected to be **open, -considerate, reasonable, and respectful**. When in doubt, `Python Software -Foundation's Code of Conduct`_ is a good reference in terms of behavior -guidelines. - - -Issue Reports +Pre-requisites +============== + +DataJunction (DJ) is currently supported in Python 3.8, 3.9, and 3.10. It's recommended to use ``pyenv`` to create a virtual environment called "dj": + +.. code-block:: bash + + $ pyenv virtualenv 3.8 dj # or 3.9/3.10 + +Then you can pick any of the components you want to develop on, say ``cd datajunction-server`` or ``cd datajunction-clients/python``, +install required dependencies with ``pdm install`` and call ``make test`` to run all the unit tests for that component. + +DJ relies heavily on these libraries: + +- `SQLAlchemy `_ for running queries and fetching table metadata. +- `FastAPI `_ for APIs. + +Running the examples +==================== + +The repository should be run in conjunction with [`djqs`](https://github.com/DataJunction/djqs), which will load a +`postgres-roads` database with example data. In order to get this up and running: +* ``docker compose up`` from DJ to get the DJ metrics service up (defaults to run on port 8000) +* ``docker compose up`` in DJQS to get the DJ query service up (defaults to run on port 8001) + +Once both are up, you can fire up `juypter notebook` to run the `Modeling the Roads Example Database.ipynb`, +which will create all of the relevant nodes and provide some examples of API interactions. + +You can check that everything is working by querying the list of available catalogs (install `jq `_ if you don't have it): + +.. code-block:: bash + + % curl http://localhost:8000/catalogs/ | jq + [ + { + "name": "default", + "engines": [ + { + "name": "postgres", + "version": "", + "uri": "postgresql://dj:dj@postgres-roads:5432/djdb" + } + ] + } + ] + +To see the list of available nodes: + +.. code-block:: bash + + $ curl http://localhost:8000/nodes/ | jq + [ + { + "node_revision_id": 1, + "node_id": 1, + "type": "source", + "name": "repair_orders", + "display_name": "Repair Orders", + "version": "v1.0", + "status": "valid", + "mode": "published", + "catalog": { + "id": 1, + "uuid": "c2363d4d-ce0c-4eb2-9b1e-28743970f859", + "created_at": "2023-03-17T15:45:15.012784+00:00", + "updated_at": "2023-03-17T15:45:15.012795+00:00", + "extra_params": {}, + "name": "default" + }, + "schema_": "roads", + "table": "repair_orders", + "description": "Repair orders", + "query": null, + "availability": null, + "columns": [ + { + "name": "repair_order_id", + "type": "INT", + "attributes": [] + }, + { + "name": "municipality_id", + "type": "STR", + "attributes": [] + }, + { + "name": "hard_hat_id", + "type": "INT", + "attributes": [] + }, + { + "name": "order_date", + "type": "TIMESTAMP", + "attributes": [] + }, + { + "name": "required_date", + "type": "TIMESTAMP", + "attributes": [] + }, + { + "name": "dispatched_date", + "type": "TIMESTAMP", + "attributes": [] + }, + { + "name": "dispatcher_id", + "type": "INT", + "attributes": [] + } + ], + "updated_at": "2023-03-17T15:45:18.456072+00:00", + "materialization_configs": [], + "created_at": "2023-03-17T15:45:18.448321+00:00", + "tags": [] + }, + ... + ] + +And metrics: + +.. code-block:: bash + + $ curl http://localhost:8000/metrics/ | jq + [ + { + "id": 21, + "name": "num_repair_orders", + "display_name": "Num Repair Orders", + "current_version": "v1.0", + "description": "Number of repair orders", + "created_at": "2023-03-17T15:45:27.589799+00:00", + "updated_at": "2023-03-17T15:45:27.590304+00:00", + "query": "SELECT count(repair_order_id) as num_repair_orders FROM repair_orders", + "dimensions": [ + "dispatcher.company_name", + "dispatcher.dispatcher_id", + "dispatcher.phone", + "hard_hat.address", + "hard_hat.birth_date", + "hard_hat.city", + "hard_hat.contractor_id", + "hard_hat.country", + "hard_hat.first_name", + "hard_hat.hard_hat_id", + "hard_hat.hire_date", + "hard_hat.last_name", + "hard_hat.manager", + "hard_hat.postal_code", + "hard_hat.state", + "hard_hat.title", + "municipality_dim.contact_name", + "municipality_dim.contact_title", + "municipality_dim.local_region", + "municipality_dim.municipality_id", + "municipality_dim.municipality_type_desc", + "municipality_dim.municipality_type_id", + "municipality_dim.phone", + "municipality_dim.state_id", + "repair_orders.dispatched_date", + "repair_orders.dispatcher_id", + "repair_orders.hard_hat_id", + "repair_orders.municipality_id", + "repair_orders.order_date", + "repair_orders.repair_order_id", + "repair_orders.required_date" + ] + }, + { + "id": 22, + "name": "avg_repair_price", + "display_name": "Avg Repair Price", + "current_version": "v1.0", + "description": "Average repair price", + "created_at": "2023-03-17T15:45:28.121435+00:00", + "updated_at": "2023-03-17T15:45:28.121836+00:00", + "query": "SELECT avg(price) as avg_repair_price FROM repair_order_details", + "dimensions": [ + "repair_order.dispatcher_id", + "repair_order.hard_hat_id", + "repair_order.municipality_id", + "repair_order.repair_order_id", + "repair_order_details.discount", + "repair_order_details.price", + "repair_order_details.quantity", + "repair_order_details.repair_order_id", + "repair_order_details.repair_type_id" + ] + }, + ... + ] + + +To get data for a given metric: + +.. code-block:: bash + + $ curl http://localhost:8000/data/avg_repair_price/ | jq + +You can also pass query parameters to group by a dimension or filter: + +.. code-block:: bash + + $ curl "http://localhost:8000/data/avg_time_to_dispatch/?dimensions=dispatcher.company_name" | jq + $ curl "http://localhost:8000/data/avg_time_to_dispatch/?filters=hard_hat.state='AZ'" | jq + +Similarly, you can request the SQL for a given metric with given constraints: + +.. code-block:: bash + + $ curl "http://localhost:8000/sql/avg_time_to_dispatch/?dimensions=dispatcher.company_name" | jq + { + "sql": "SELECT avg(repair_orders.dispatched_date - repair_orders.order_date) AS avg_time_to_dispatch,\n\tdispatcher.company_name \n FROM \"roads\".\"repair_orders\" AS repair_orders\nLEFT JOIN (SELECT dispatchers.company_name,\n\tdispatchers.dispatcher_id,\n\tdispatchers.phone \n FROM \"roads\".\"dispatchers\" AS dispatchers\n \n) AS dispatcher\n ON repair_orders.dispatcher_id = dispatcher.dispatcher_id \n GROUP BY dispatcher.company_name" + } + +You can also run SQL queries against the metrics in DJ, using the special database with ID 0 and referencing a table called ``metrics``: + +.. code-block:: sql + + SELECT "basic.num_comments" + FROM metrics + WHERE "basic.source.comments.user_id" < 4 + GROUP BY "basic.source.comments.user_id" + + +API docs +======== + +Once you have Docker running you can see the API docs at http://localhost:8000/docs. + +Creating a PR ============= -If you experience bugs or general issues with ``datajunction``, please have a look -on the `issue tracker`_. If you don't see anything useful there, please feel -free to fire an issue report. - -.. tip:: - Please don't forget to include the closed issues in your search. - Sometimes a solution was already reported, and the problem is considered - **solved**. - -New issue reports should include information about your programming environment -(e.g., operating system, Python version) and steps to reproduce the problem. -Please try also to simplify the reproduction steps to a very minimal example -that still illustrates the problem you are facing. By removing other factors, -you help us to identify the root cause of the issue. - - -Documentation Improvements -========================== - -You can help improve ``datajunction`` docs by making them more readable and coherent, or -by adding missing information and correcting mistakes. - -``datajunction`` documentation uses Sphinx_ as its main documentation compiler. -This means that the docs are kept in the same repository as the project code, and -that any documentation update is done in the same way was a code contribution. - -.. todo:: Don't forget to mention which markup language you are using. - - e.g., reStructuredText_ or CommonMark_ with MyST_ extensions. - -.. todo:: If your project is hosted on GitHub, you can also mention the following tip: - - .. tip:: - Please notice that the `GitHub web interface`_ provides a quick way of - propose changes in ``datajunction``'s files. While this mechanism can - be tricky for normal code contributions, it works perfectly fine for - contributing to the docs, and can be quite handy. - - If you are interested in trying this method out, please navigate to - the ``docs`` folder in the source repository_, find which file you - would like to propose changes and click in the little pencil icon at the - top, to open `GitHub's code editor`_. Once you finish editing the file, - please write a message in the form at the bottom of the page describing - which changes have you made and what are the motivations behind them and - submit your proposal. - -When working on documentation changes in your local machine, you can -compile them using |tox|_:: - - tox -e docs +When creating a PR, make sure to run ``make test`` to check for test coverage. You can also run ``make check`` to run the pre-commit hooks. -and use Python's built-in web server for a preview in your web browser -(``http://localhost:8000``):: +A few `fixtures `_ are `available `_ to help writing unit tests. - python3 -m http.server --directory 'docs/_build/html' +Adding new dependencies +======================= +When a PR introduces a new dependency, add them to ``setup.cfg`` under ``install_requires``. If the dependency version is less than ``1.0`` and you expect it to change often it's better to pin the dependency, eg: -Code Contributions -================== +.. code-block:: config -.. todo:: Please include a reference or explanation about the internals of the project. + some-package==0.0.1 - An architecture description, design principles or at least a summary of the - main concepts will make it easy for potential contributors to get started - quickly. +Otherwise specify the package with a lower bound only: -Submit an issue ---------------- +.. code-block:: config -Before you work on any non-trivial code contribution it's best to first create -a report in the `issue tracker`_ to start a discussion on the subject. -This often provides additional considerations and avoids unnecessary work. + some-package>=1.2.3 -Create an environment ---------------------- +Don't use upper bounds in the dependencies. We have nightly unit tests that test if newer versions of dependencies will break. -Before you start coding, we recommend creating an isolated `virtual -environment`_ to avoid any problems with your installed Python packages. -This can easily be done via either |virtualenv|_:: +Database migrations +=================== - virtualenv - source /bin/activate +We use `Alembic `_ to manage schema migrations. If a PR introduces new models or changes existing ones a migration must be created. -or Miniconda_:: +1. Run the Docker container with ``docker compose up``. +2. Enter the ``dj`` container with ``docker exec -it dj bash``. +3. Run ``alembic revision --autogenerate -m "Description of the migration"``. This will create a file in the repository, under ``alembic/versions/``. Verify the file, checking that the upgrade and the downgrade functions make sense. +4. Still inside the container, run ``alembic upgrade head``. This will update the database schema to match the models. +5. Now run ``alembic downgrade $SHA``, where ``$SHA`` is the previous migration. You can see the hash with ``alembic history``. +6. Once you've confirmed that both the upgrade and downgrade work, upgrade again and commit the file. - conda create -n pyscaffold python=3 six virtualenv pytest pytest-cov - conda activate pyscaffold +If the migrations include ``alter_column`` or ``drop_column`` make sure to wrap them in a ``batch_alter_table`` context manager so that they work correctly with SQLite. You can see `an example here `_. -Clone the repository --------------------- +Development tips +=================== -#. Create an user account on |the repository service| if you do not already have one. -#. Fork the project repository_: click on the *Fork* button near the top of the - page. This creates a copy of the code under your account on |the repository service|. -#. Clone this copy to your local disk:: +Using ``PYTEST_ARGS`` with ``make test`` +---------------------------------------- - git clone git@github.com:YourLogin/datajunction.git - cd datajunction +If you'd like to pass additional arguments to pytest when running `make test`, you can define them as ``PYTEST_ARGS``. For example, you can include +`--fixtures` to see a list of all fixtures. -#. You should run:: +.. code-block:: sh - pip install -U pip setuptools -e . + make test PYTEST_ARGS="--fixtures" - to be able run ``putup --help``. +Running a Subset of Tests +------------------------- - .. todo:: if you are not using pre-commit, please remove the following item: +When working on tests, it's common to want to run a specific test by name. This can be done by passing ``-k`` as an additional pytest argument along +with a string expression. Pytest will only run tests which contain names that match the given string expression. -#. Install |pre-commit|_:: +.. code-block:: sh - pip install pre-commit - pre-commit install + make test PYTEST_ARGS="-k test_main_compile" - ``datajunction`` comes with a lot of hooks configured to automatically help the - developer to check the code being written. +Running TPC-DS Parsing Tests +------------------------- -Implement your changes ----------------------- +A TPC-DS test suite is included but skipped by default. As we incrementally build support for various SQL syntax into the DJ +SQL AST, it's helpful to run these tests using the `--tpcds` flag. -#. Create a branch to hold your changes:: +.. code-block:: sh - git checkout -b my-feature + make test PYTEST_ARGS="--tpcds" - and start making changes. Never work on the master branch! +You can run only the TPC-DS tests without the other tests using a `-k` filter. -#. Start your work on this branch. Don't forget to add docstrings_ to new - functions, modules and classes, especially if they are part of public APIs. +.. code-block:: sh -#. Add yourself to the list of contributors in ``AUTHORS.rst``. + make test PYTEST_ARGS="--tpcds -k tpcds" -#. When you’re done editing, do:: +Another useful option is matching on the full test identifier to run the test for a single specific query file from the +parametrize list. This is useful when paired with `--pdb` to drop into the debugger. - git add - git commit +.. code-block:: sh - to record your changes in git_. + make test PYTEST_ARGS="--tpcds --pdb -k test_parsing_ansi_tpcds_queries[./ansi/query1.sql]" - .. todo:: if you are not using pre-commit, please remove the following item: +If you prefer to use tox, these flags all work the same way. - Please make sure to see the validation messages from |pre-commit|_ and fix - any eventual issues. - This should automatically use flake8_/black_ to check/fix the code style - in a way that is compatible with the project. +.. code-block:: sh - .. important:: Don't forget to add unit tests and documentation in case your - contribution adds an additional feature and is not just a bugfix. + tox tests/sql/parsing/queries/tpcds/test_tpcds.py::test_parsing_sparksql_tpcds_queries -- --tpcds - Moreover, writing a `descriptive commit message`_ is highly recommended. - In case of doubt, you can check the commit history with:: +Enabling ``pdb`` When Running Tests +----------------------------------- - git log --graph --decorate --pretty=oneline --abbrev-commit --all +If you'd like to drop into ``pdb`` when a test fails, or on a line where you've added ``pdb.set_trace()``, you can pass ``--pdb`` as a pytest argument. - to look for recurring communication patterns. +.. code-block:: sh -#. Please check that your changes don't break any unit tests with:: + make test PYTEST_ARGS="--pdb" - tox +Using ``pdb`` In Docker +----------------------- - (after having installed |tox|_ with ``pip install tox`` or ``pipx``). +The included docker compose files make it easy to get a development environment up and running locally. When debugging or working on a new feature, +it's helpful to set breakpoints in the source code to drop into ``pdb`` at runtime. In order to do this while using the docker compose setup, there +are three steps. - You can also use |tox|_ to run several other pre-configured tasks in the - repository. Try ``tox -av`` to see a list of the available checks. +1. Set a trace in the source code on the line where you'd like to drop into ``pdb``. -Submit your contribution ------------------------- +.. code-block:: python -#. If everything works fine, push your local branch to |the repository service| with:: + import pdb; pdb.set_trace() - git push -u origin my-feature +2. In the docker compose file, enable ``stdin_open`` and ``tty`` on the service you'd like debug. -#. Go to the web page of your fork and click |contribute button| - to send your changes for review. +.. code-block:: YAML - .. todo:: if you are using GitHub, you can uncomment the following paragraph + services: + dj: + stdin_open: true + tty: true + ... - Find more detailed information `creating a PR`_. You might also want to open - the PR as a draft first and mark it as ready for review after the feedbacks - from the continuous integration (CI) system or any required fixes. +3. Once the docker environment is running, attach to the container. +.. code-block:: sh -Troubleshooting ---------------- + docker attach dj -The following tips can be used when facing problems to build or test the -package: +When the breakpoint is hit, the attached session will enter an interactive ``pdb`` session. -#. Make sure to fetch all the tags from the upstream repository_. - The command ``git describe --abbrev=0 --tags`` should return the version you - are expecting. If you are trying to run CI scripts in a fork repository, - make sure to push all the tags. - You can also try to remove all the egg files or the complete egg folder, i.e., - ``.eggs``, as well as the ``*.egg-info`` folders in the ``src`` folder or - potentially in the root of your project. +ANTLR +----- -#. Sometimes |tox|_ misses out when new dependencies are added, especially to - ``setup.cfg`` and ``docs/requirements.txt``. If you find any problems with - missing dependencies when running a command with |tox|_, try to recreate the - ``tox`` environment using the ``-r`` flag. For example, instead of:: +Generating the ANTLR Parser +--------------------------- - tox -e docs +Install the ANTLR generator tool. - Try running:: +.. code-block:: sh - tox -r -e docs + pip install antlr4-tools -#. Make sure to have a reliable |tox|_ installation that uses the correct - Python version (e.g., 3.7+). When in doubt you can run:: +While in the `dj/sql/parsing/backends/antlr4/grammar/` directory, generate the parser by running the following CLI command. - tox --version - # OR - which tox +.. code-block:: sh - If you have trouble and are seeing weird errors upon running |tox|_, you can - also try to create a dedicated `virtual environment`_ with a |tox|_ binary - freshly installed. For example:: + antlr4 -Dlanguage=Python3 -visitor SqlBaseLexer.g4 SqlBaseParser.g4 -o generated - virtualenv .venv - source .venv/bin/activate - .venv/bin/pip install tox - .venv/bin/tox -e all +A python 3 ANTLR parser will be generated in `dj/sql/parsing/backends/antlr4/grammar/generated/`. -#. `Pytest can drop you`_ in an interactive session in the case an error occurs. - In order to do that you need to pass a ``--pdb`` option (for example by - running ``tox -- -k --pdb``). - You can also setup breakpoints manually instead of using the ``--pdb`` option. +Creating a Diagram from the Grammar +----------------------------------- +Use https://bottlecaps.de/convert/ to go from ANTLR4 -> EBNF -Maintainer tasks -================ +Input the EBNF into https://bottlecaps.de/rr/ui -Releases --------- +Common Issues +=================== -.. todo:: This section assumes you are using PyPI to publicly release your package. +Docker missing dependencies +---------------------------------------- - If instead you are using a different/private package index, please update - the instructions accordingly. +If you are still new to docker development... you may run into this. If someone else modified / added new dependencies in some +of the DJ conatainers, you may notice an error like: -If you are part of the group of maintainers and have correct user permissions -on PyPI_, the following steps can be used to release a new version for -``datajunction``: +.. code-block:: sh + dj | ModuleNotFoundError: No module named 'sse_starlette' -#. Make sure all unit tests are successful. -#. Tag the current commit on the main branch with a release tag, e.g., ``v1.2.3``. -#. Push the new tag to the upstream repository_, e.g., ``git push upstream v1.2.3`` -#. Clean up the ``dist`` and ``build`` folders with ``tox -e clean`` - (or ``rm -rf dist build``) - to avoid confusion with old builds and Sphinx docs. -#. Run ``tox -e build`` and check that the files in ``dist`` have - the correct version (no ``.dirty`` or git_ hash) according to the git_ tag. - Also check the sizes of the distributions, if they are too big (e.g., > - 500KB), unwanted clutter may have been accidentally included. -#. Run ``tox -e publish -- --repository pypi`` and check that everything was - uploaded to PyPI_ correctly. +Remember it is not your local env that needs to be patched but one of the DJ docker conatainers. +Try running ``docker compose build``` and your ``docker compose up`` should work just fine. +Alembic migration error +---------------------------------------- -.. [#contrib1] Even though, these resources focus on open source projects and - communities, the general ideas behind collaborating with other developers - to collectively create software are general and can be applied to all sorts - of environments, including private companies and proprietary code bases. +If during development your alembic migrations get into a spot where the automatic upgrade (or downgrade) is stuck you may see something +similar to the following output in your ``db_migration`` agent log: +.. code-block:: sh -.. <-- strart --> -.. todo:: Please review and change the following definitions: + db_migration | sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) duplicate column name: categorical_partitions + db_migration | [SQL: ALTER TABLE availabilitystate ADD COLUMN categorical_partitions JSON] + db_migration | (Background on this error at: https://sqlalche.me/e/14/e3q8) + db_migration exited with code 1 -.. |the repository service| replace:: GitHub -.. |contribute button| replace:: "Create pull request" +The easiest way to fix it is to reset your database state using these commands (in another terminal session): -.. _repository: https://github.com//datajunction -.. _issue tracker: https://github.com//datajunction/issues -.. <-- end --> +.. code-block:: sh + $ docker exec -it dj bash + root@...:/code# -.. |virtualenv| replace:: ``virtualenv`` -.. |pre-commit| replace:: ``pre-commit`` -.. |tox| replace:: ``tox`` + root@...:/code# alembic downgrade base + ... + INFO [alembic.runtime.migration] Running downgrade e41c021c19a6 -> , Initial migration + root@...:/code# alembic upgrade head + ... -.. _black: https://pypi.org/project/black/ -.. _CommonMark: https://commonmark.org/ -.. _contribution-guide.org: http://www.contribution-guide.org/ -.. _creating a PR: https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request -.. _descriptive commit message: https://chris.beams.io/posts/git-commit -.. _docstrings: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html -.. _first-contributions tutorial: https://github.com/firstcontributions/first-contributions -.. _flake8: https://flake8.pycqa.org/en/stable/ -.. _git: https://git-scm.com -.. _GitHub's fork and pull request workflow: https://guides.github.com/activities/forking/ -.. _guide created by FreeCodeCamp: https://github.com/FreeCodeCamp/how-to-contribute-to-open-source -.. _Miniconda: https://docs.conda.io/en/latest/miniconda.html -.. _MyST: https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html -.. _other kinds of contributions: https://opensource.guide/how-to-contribute -.. _pre-commit: https://pre-commit.com/ -.. _PyPI: https://pypi.org/ -.. _PyScaffold's contributor's guide: https://pyscaffold.org/en/stable/contributing.html -.. _Pytest can drop you: https://docs.pytest.org/en/stable/usage.html#dropping-to-pdb-python-debugger-at-the-start-of-a-test -.. _Python Software Foundation's Code of Conduct: https://www.python.org/psf/conduct/ -.. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/ -.. _Sphinx: https://www.sphinx-doc.org/en/master/ -.. _tox: https://tox.readthedocs.io/en/stable/ -.. _virtual environment: https://realpython.com/python-virtual-environments-a-primer/ -.. _virtualenv: https://virtualenv.pypa.io/en/stable/ - -.. _GitHub web interface: https://docs.github.com/en/github/managing-files-in-a-repository/managing-files-on-github/editing-files-in-your-repository -.. _GitHub's code editor: https://docs.github.com/en/github/managing-files-in-a-repository/managing-files-on-github/editing-files-in-your-repository +After this, the `docker compose up` command should start the db_migration agent without problems. \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 8e5a6902a..000000000 --- a/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -pyenv: .python-version - -.python-version: setup.cfg - if [ -z "`pyenv virtualenvs | grep datajunction`" ]; then\ - pyenv virtualenv datajunction;\ - fi - if [ ! -f .python-version ]; then\ - pyenv local datajunction;\ - fi - pip install -e '.[testing]' - touch .python-version - -test: pyenv - pytest --cov=src/datajunction -vv tests/ --doctest-modules src/datajunction - -clean: - pyenv virtualenv-delete datajunction - -spellcheck: - codespell -S "*.json" src/datajunction docs/*rst tests templates - -requirements.txt: .python-version - pip install --upgrade pip - pip-compile --no-annotate diff --git a/README.md b/README.md new file mode 100644 index 000000000..9f4ccdd36 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# DataJunction + +![test workflow](https://github.com/DataJunction/dj/actions/workflows/test.yml/badge.svg?branch=main) +![client-integration-tests workflow](https://github.com/DataJunction/dj/actions/workflows/client-integration-tests.yml/badge.svg?branch=main) + +## Introduction + +DataJunction (DJ) is an open source **metrics platform** that allows users to define +metrics and the data models behind them using **SQL**, serving as a **semantic layer** +on top of a physical data warehouse. By leveraging this metadata, DJ can enable efficient +retrieval of metrics data across different dimensions and filters. + +[Documentation](http://datajunction.io) + +![DataJunction](docs/images/dj-landing.png) + +## Getting Started + +To launch the DataJunction UI with a minimal DataJunction backend, start the default docker compose environment. + +```sh +docker compose up +``` + +If you'd like to launch the full suite of services, including open-source implementations of the DataJunction query service and +DataJunction reflection service specifications, use the `demo` profile. + +```sh +docker compose --profile demo up +``` + +DJUI: [http://localhost:3000/](http://localhost:3000/) +DJ Swagger Docs: [http://localhost:8000/docs](http://localhost:8000/docs) +DJQS Swagger Docs: [http://localhost:8001/docs](http://localhost:8001/docs) +Jaeger UI: [http://localhost:16686/search](http://localhost:16686/search) +Jupyter Lab: [http://localhost:8181](http://localhost:8181) + +## How does this work? + +At its core, DJ stores metrics and their upstream abstractions as interconnected nodes. +These nodes can represent a variety of elements, such as tables in a data warehouse +(**source nodes**), SQL transformation logic (**transform nodes**), dimensions logic, +metrics logic, and even selections of metrics, dimensions, and filters (**cube nodes**). + +By parsing each node's SQL into an AST and through dimensional links between columns, +DJ can infer a graph of dependencies between nodes, which allows it to find the +appropriate join paths between nodes to generate queries for metrics. + +## The Community + +To get involved, feel free to join the DataJunction open-source community sync that's held ever two weeks--all are welcome! For an invite to the sync, simply join the [datajunction-community](https://groups.google.com/g/datajunction-community) google group. Also please join us on [slack](https://join.slack.com/t/dj-w5m3063/shared_invite/zt-2zazrd9xw-wnjm5a_sIuQ3uqgjS~pO~w)! diff --git a/README.rst b/README.rst deleted file mode 100644 index 42c81eb59..000000000 --- a/README.rst +++ /dev/null @@ -1,67 +0,0 @@ -.. These are examples of badges you might want to add to your README: - please update the URLs accordingly - - .. image:: https://api.cirrus-ci.com/github//datajunction.svg?branch=main - :alt: Built Status - :target: https://cirrus-ci.com/github//datajunction - .. image:: https://readthedocs.org/projects/datajunction/badge/?version=latest - :alt: ReadTheDocs - :target: https://datajunction.readthedocs.io/en/stable/ - .. image:: https://img.shields.io/coveralls/github//datajunction/main.svg - :alt: Coveralls - :target: https://coveralls.io/r//datajunction - .. image:: https://img.shields.io/pypi/v/datajunction.svg - :alt: PyPI-Server - :target: https://pypi.org/project/datajunction/ - .. image:: https://img.shields.io/conda/vn/conda-forge/datajunction.svg - :alt: Conda-Forge - :target: https://anaconda.org/conda-forge/datajunction - .. image:: https://pepy.tech/badge/datajunction/month - :alt: Monthly Downloads - :target: https://pepy.tech/project/datajunction - .. image:: https://img.shields.io/twitter/url/http/shields.io.svg?style=social&label=Twitter - :alt: Twitter - :target: https://twitter.com/datajunction - -.. image:: https://img.shields.io/badge/-PyScaffold-005CA0?logo=pyscaffold - :alt: Project generated with PyScaffold - :target: https://pyscaffold.org/ - -| - -============ -datajunction -============ - - - Add a short description here! - - -A longer description of your project goes here... - - -.. _pyscaffold-notes: - -Making Changes & Contributing -============================= - -This project uses `pre-commit`_, please make sure to install it before making any -changes:: - - pip install pre-commit - cd datajunction - pre-commit install - -It is a good idea to update the hooks to the latest version:: - - pre-commit autoupdate - -Don't forget to tell your contributors to also install and use pre-commit. - -.. _pre-commit: https://pre-commit.com/ - -Note -==== - -This project has been set up using PyScaffold 4.1. For details and usage -information on PyScaffold see https://pyscaffold.org/. diff --git a/TODO.mkd b/TODO.mkd deleted file mode 100644 index 3d03b90e9..000000000 --- a/TODO.mkd +++ /dev/null @@ -1,49 +0,0 @@ -- [ ] Single model for all nodes -- [ ] All nodes in the same directory -- [/] Compile repo, computing the schema of source nodes -- [ ] Auto-map dimensions from the DB schema -- [ ] Compute the schema of downstream nodes -- [ ] Translate metrics into SQL -- [ ] Compute metrics (ie, run query) -- [ ] Compute statistics on columns (histogram, correlation) -- [ ] Move data on JOINs based on column statistics -- [ ] Optimize data transfer (delta-of-delta for timeseries) -- [ ] Virtual dimensions (time, space, user-defined) -- [ ] JS dataframe with time-aware caching and additive-aware, to reuse queries -- [ ] 2 modes of join: Shillelagh and move data - -Models: - -- source -- transform -- dimension -- metric -- population - -Relationships: - -- source -> transform [-> transform ]-> metric -- source -> dimension -> population -- population based on metrics as well? - -DSL for querying: - -- `m=likes,comments&d=user.country&f=userid>10,time>2021-01-01T00:00:00+00:00` - -Examples: - -1. Dimension table in 2 storages (fast/slow), choose fast. -2. Dimension table in 2 storages (fast/slow) but only a few columns in fast; choose slow. -3. Translate query with cross-DB join, `FROM PROGRAM` in Postgres - -SQL input: - -``` -SELECT core.likes FROM metrics -``` - -Gets translated to: - -``` -SELECT COUNT(*) FROM content_actions -``` diff --git a/datajunction-clients/java/.gitignore b/datajunction-clients/java/.gitignore new file mode 100644 index 000000000..b63da4551 --- /dev/null +++ b/datajunction-clients/java/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/datajunction-clients/java/build.gradle b/datajunction-clients/java/build.gradle new file mode 100644 index 000000000..ce3d400e2 --- /dev/null +++ b/datajunction-clients/java/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' +} + +group = 'io.datajunction' +version = '1.0-SNAPSHOT' +sourceCompatibility = '17' +targetCompatibility = '17' + +repositories { + mavenCentral() +} + +dependencies { + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.2' + compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.36' + annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.36' + + testImplementation platform('org.junit:junit-bom:5.9.1') + testImplementation 'org.junit.jupiter:junit-jupiter' + testCompileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.36' + testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.36' +} + +test { + useJUnitPlatform() +} diff --git a/datajunction-clients/java/gradle.properties b/datajunction-clients/java/gradle.properties new file mode 100644 index 000000000..db5b69864 --- /dev/null +++ b/datajunction-clients/java/gradle.properties @@ -0,0 +1 @@ +org.gradle.logging.level=INFO diff --git a/datajunction-clients/java/gradle/wrapper/gradle-wrapper.jar b/datajunction-clients/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..a4b76b953 Binary files /dev/null and b/datajunction-clients/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/datajunction-clients/java/gradle/wrapper/gradle-wrapper.properties b/datajunction-clients/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e18bc253b --- /dev/null +++ b/datajunction-clients/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/datajunction-clients/java/gradlew b/datajunction-clients/java/gradlew new file mode 100755 index 000000000..f3b75f3b0 --- /dev/null +++ b/datajunction-clients/java/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/datajunction-clients/java/gradlew.bat b/datajunction-clients/java/gradlew.bat new file mode 100644 index 000000000..9b42019c7 --- /dev/null +++ b/datajunction-clients/java/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/datajunction-clients/java/settings.gradle b/datajunction-clients/java/settings.gradle new file mode 100644 index 000000000..7358c7e2d --- /dev/null +++ b/datajunction-clients/java/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'DataJunction' + diff --git a/datajunction-clients/java/src/main/java/io/datajunction/client/DJClient.java b/datajunction-clients/java/src/main/java/io/datajunction/client/DJClient.java new file mode 100644 index 000000000..8cc3d4127 --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/client/DJClient.java @@ -0,0 +1,263 @@ +package io.datajunction.client; + +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.URI; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.datajunction.models.requests.Catalog; +import io.datajunction.models.requests.Dimension; +import io.datajunction.models.requests.Engine; +import io.datajunction.models.requests.Metric; +import io.datajunction.models.requests.Source; +import io.datajunction.models.requests.Transform; +import io.datajunction.models.responses.CatalogResponse; +import io.datajunction.models.responses.CommonDimensionsResponse; +import io.datajunction.models.responses.DimensionLinkResponse; +import io.datajunction.models.responses.EngineResponse; +import io.datajunction.models.responses.NamespaceResponse; +import io.datajunction.models.responses.NodeResponse; +import io.datajunction.models.responses.SQLResponse; + +public class DJClient { + + private final HttpClient client; + private final String baseURL; + private final ObjectMapper objectMapper; + private String cookie; + + public DJClient(HttpClient client, String baseURL) { + this.client = client; + this.baseURL = baseURL; + this.objectMapper = new ObjectMapper(); + this.cookie = ""; + } + + private CompletableFuture> sendRequest(HttpRequest request) { + return client.sendAsync(request, BodyHandlers.ofString()); + } + + private String toJson(Object object) { + try { + return objectMapper.writeValueAsString(object); + } catch (Exception e) { + throw new RuntimeException("Failed to serialize object to JSON", e); + } + } + + private HttpRequest buildRequest(String endpoint, String method, String body) { + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(baseURL + endpoint)) + .header("Content-Type", "application/json"); + + if (!cookie.isEmpty()) { + builder.header("Cookie", cookie); + } + + switch (method) { + case "POST": + if (body != null) { + builder.POST(BodyPublishers.ofString(body)); + } else { + builder.POST(BodyPublishers.noBody()); + } + break; + case "PATCH": + if (body != null) { + builder.method("PATCH", BodyPublishers.ofString(body)); + } else { + builder.method("PATCH", BodyPublishers.noBody()); + } + break; + case "GET": + default: + builder.GET(); + break; + } + + return builder.build(); + } + + public CompletableFuture login(String username, String password) { + Map formData = Map.of( + "username", username, + "password", password + ); + String form = formData.entrySet().stream() + .map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "=" + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&")); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseURL + "/basic/login/")) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(BodyPublishers.ofString(form)) + .build(); + + return sendRequest(request).thenAccept(response -> { + if (response.statusCode() != 200) { + throw new RuntimeException("Login failed: " + response.statusCode() + " " + response.body()); + } + + List cookies = response.headers().allValues("Set-Cookie"); + if (!cookies.isEmpty()) { + this.cookie = cookies.get(0); + } + }); + } + + public CompletableFuture createCatalog(Catalog catalog) { + String body = toJson(catalog); + HttpRequest request = buildRequest("/catalogs/", "POST", body); + return sendRequest(request).thenApply(response -> { + try { + return objectMapper.readValue(response.body(), CatalogResponse.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize response", e); + } + }); + } + + public CompletableFuture createEngine(Engine engine) { + String body = toJson(engine); + HttpRequest request = buildRequest("/engines/", "POST", body); + return sendRequest(request).thenApply(response -> { + try { + return objectMapper.readValue(response.body(), EngineResponse.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize response", e); + } + }); + } + + public CompletableFuture addEngineToCatalog(String catalog, Engine engine) { + String body = toJson(List.of(engine)); + HttpRequest request = buildRequest("/catalogs/" + catalog + "/engines/", "POST", body); + return sendRequest(request).thenApply(response -> { + try { + return objectMapper.readValue(response.body(), EngineResponse.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize response", e); + } + }); + } + + public CompletableFuture createNamespace(String namespace) { + HttpRequest request = buildRequest("/namespaces/" + namespace + "/", "POST", null); + return sendRequest(request).thenApply(response -> { + try { + return objectMapper.readValue(response.body(), NamespaceResponse.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize response", e); + } + }); + } + + public CompletableFuture registerTable(String catalog, String schema, String table) { + HttpRequest request = buildRequest("/register/table/" + catalog + "/" + schema + "/" + table, "POST", null); + return sendRequest(request).thenApply(response -> { + try { + return objectMapper.readValue(response.body(), NodeResponse.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize response", e); + } + }); + } + + public CompletableFuture createSource(Source source) { + String body = toJson(source); + HttpRequest request = buildRequest("/nodes/source/", "POST", body); + return sendRequest(request).thenApply(response -> { + try { + return objectMapper.readValue(response.body(), NodeResponse.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize response", e); + } + }); + } + + public CompletableFuture createTransform(Transform transform) { + String body = toJson(transform); + HttpRequest request = buildRequest("/nodes/transform/", "POST", body); + return sendRequest(request).thenApply(response -> { + try { + return objectMapper.readValue(response.body(), NodeResponse.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize response", e); + } + }); + } + + public CompletableFuture createDimension(Dimension dimension) { + String body = toJson(dimension); + HttpRequest request = buildRequest("/nodes/dimension/", "POST", body); + return sendRequest(request).thenApply(response -> { + try { + return objectMapper.readValue(response.body(), NodeResponse.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize response", e); + } + }); + } + + public CompletableFuture linkDimensionToNode(String nodeName, String nodeColumn, String dimension, String dimensionColumn) { + HttpRequest request = buildRequest( + "/nodes/" + nodeName + "/columns/" + nodeColumn + "/?dimension=" + dimension + "&dimension_column=" + dimensionColumn, + "POST", + null + ); + return sendRequest(request).thenApply(response -> { + try { + return objectMapper.readValue(response.body(), DimensionLinkResponse.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize response", e); + } + }); + } + + public CompletableFuture createMetric(Metric metric) { + String body = toJson(metric); + HttpRequest request = buildRequest("/nodes/metric/", "POST", body); + return sendRequest(request).thenApply(response -> { + try { + return objectMapper.readValue(response.body(), NodeResponse.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize response", e); + } + }); + } + + public CompletableFuture> listCommonDimensions(List metrics) { + String metricsQuery = "?" + String.join("&", metrics.stream().map(m -> "metric=" + m).toList()); + HttpRequest request = buildRequest("/metrics/common/dimensions/" + metricsQuery, "GET", null); + return sendRequest(request).thenApply(response -> { + try { + return objectMapper.readValue(response.body(), objectMapper.getTypeFactory().constructCollectionType(List.class, CommonDimensionsResponse.Dimension.class)); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize response", e); + } + }); + } + + public CompletableFuture getSQL(List metrics, List dimensions, List filters) { + String metricsQuery = "?" + String.join("&", metrics.stream().map(m -> "metrics=" + m).toList()); + String dimensionsQuery = String.join("&", dimensions.stream().map(d -> "dimensions=" + d).toList()); + String filtersQuery = String.join("&", filters.stream().map(f -> "filters=" + f).toList()); + HttpRequest request = buildRequest("/sql/" + metricsQuery + "&" + dimensionsQuery + filtersQuery, "GET", null); + return sendRequest(request).thenApply(response -> { + try { + return objectMapper.readValue(response.body(), SQLResponse.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize response", e); + } + }); + } +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Catalog.java b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Catalog.java new file mode 100644 index 000000000..166f33b42 --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Catalog.java @@ -0,0 +1,13 @@ +package io.datajunction.models.requests; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class Catalog { + private String name; + + public Catalog(String name) { + this.name = name; + } +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Column.java b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Column.java new file mode 100644 index 000000000..3e11f428c --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Column.java @@ -0,0 +1,14 @@ +package io.datajunction.models.requests; + +import lombok.Data; + +@Data +public class Column { + private String name; + private String type; + + public Column(String name, String type) { + this.name = name; + this.type = type; + } +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Dimension.java b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Dimension.java new file mode 100644 index 000000000..37434bd5c --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Dimension.java @@ -0,0 +1,32 @@ +package io.datajunction.models.requests; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import java.util.List; + +@Data +public class Dimension { + private String name; + @JsonProperty("display_name") + private String displayName; + private String description; + private String mode; + @JsonProperty("primary_key") + private List primaryKey; + private List tags; + private String query; + @JsonProperty("update_if_exists") + private boolean updateIfExists; + + public Dimension(String name, String displayName, String description, String mode, + List primaryKey, List tags, String query, boolean updateIfExists) { + this.name = name; + this.displayName = displayName; + this.description = description; + this.mode = mode; + this.primaryKey = primaryKey; + this.tags = tags; + this.query = query; + this.updateIfExists = updateIfExists; + } +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Engine.java b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Engine.java new file mode 100644 index 000000000..17aeb7c97 --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Engine.java @@ -0,0 +1,14 @@ +package io.datajunction.models.requests; + +import lombok.Data; + +@Data +public class Engine { + private String name; + private String version; + + public Engine(String name, String version) { + this.name = name; + this.version = version; + } +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Metric.java b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Metric.java new file mode 100644 index 000000000..926c4fb96 --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Metric.java @@ -0,0 +1,26 @@ +package io.datajunction.models.requests; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class Metric { + private String name; + @JsonProperty("display_name") + private String displayName; + private String description; + private String mode; + private String query; + @JsonProperty("update_if_exists") + private boolean updateIfExists; + + public Metric(String name, String displayName, String description, String mode, + String query, boolean updateIfExists) { + this.name = name; + this.displayName = displayName; + this.description = description; + this.mode = mode; + this.query = query; + this.updateIfExists = updateIfExists; + } +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Source.java b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Source.java new file mode 100644 index 000000000..038067877 --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Source.java @@ -0,0 +1,37 @@ +package io.datajunction.models.requests; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import java.util.List; + +@Data +public class Source { + private String name; + private String catalog; + private String schema_; + private String table; + @JsonProperty("display_name") + private String displayName; + private String description; + private List columns; + @JsonProperty("primary_key") + private List primaryKey; + private String mode; + @JsonProperty("update_if_exists") + private boolean updateIfExists; + + public Source(String name, String catalog, String schema_, String table, String displayName, + String description, List columns, List primaryKey, + String mode, boolean updateIfExists) { + this.name = name; + this.catalog = catalog; + this.schema_ = schema_; + this.table = table; + this.displayName = displayName; + this.description = description; + this.columns = columns; + this.primaryKey = primaryKey; + this.mode = mode; + this.updateIfExists = updateIfExists; + } +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Transform.java b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Transform.java new file mode 100644 index 000000000..1504bbe26 --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/requests/Transform.java @@ -0,0 +1,26 @@ +package io.datajunction.models.requests; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class Transform { + private String name; + @JsonProperty("display_name") + private String displayName; + private String description; + private String mode; + private String query; + @JsonProperty("update_if_exists") + private boolean updateIfExists; + + public Transform(String name, String displayName, String description, String mode, + String query, boolean updateIfExists) { + this.name = name; + this.displayName = displayName; + this.description = description; + this.mode = mode; + this.query = query; + this.updateIfExists = updateIfExists; + } +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/responses/CatalogResponse.java b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/CatalogResponse.java new file mode 100644 index 000000000..329a4592a --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/CatalogResponse.java @@ -0,0 +1,12 @@ +package io.datajunction.models.responses; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CatalogResponse { + private String name; + private List engines; +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/responses/CommonDimensionsResponse.java b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/CommonDimensionsResponse.java new file mode 100644 index 000000000..bc4c5092f --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/CommonDimensionsResponse.java @@ -0,0 +1,23 @@ +package io.datajunction.models.responses; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CommonDimensionsResponse { + private List dimensions; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Dimension { + private String name; + private String nodeName; + private String nodeDisplayName; + private List properties; + private String type; + private List path; + private boolean filterOnly; + } +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/responses/DimensionLinkResponse.java b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/DimensionLinkResponse.java new file mode 100644 index 000000000..9139341aa --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/DimensionLinkResponse.java @@ -0,0 +1,10 @@ +package io.datajunction.models.responses; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class DimensionLinkResponse { + private String message; +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/responses/EngineResponse.java b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/EngineResponse.java new file mode 100644 index 000000000..cfc999303 --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/EngineResponse.java @@ -0,0 +1,13 @@ +package io.datajunction.models.responses; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class EngineResponse { + private String name; + private String version; + private String uri; + private String dialect; +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/responses/NamespaceResponse.java b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/NamespaceResponse.java new file mode 100644 index 000000000..50aaaa403 --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/NamespaceResponse.java @@ -0,0 +1,10 @@ +package io.datajunction.models.responses; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class NamespaceResponse { + private String message; +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/responses/NodeResponse.java b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/NodeResponse.java new file mode 100644 index 000000000..07fde1a47 --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/NodeResponse.java @@ -0,0 +1,65 @@ +package io.datajunction.models.responses; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class NodeResponse { + private String namespace; + private int nodeRevisionId; + private int nodeId; + private String type; + private String name; + private String displayName; + private String version; + private String status; + private String mode; + private Catalog catalog; + @JsonProperty("schema_") + private String schema; + private String table; + private String description; + private String query; + private List columns; + private String updatedAt; + private List parents; + private String createdAt; + private CreatedBy createdBy; + private List tags; + private String currentVersion; + private boolean missingTable; + private Object customMetadata; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Catalog { + private String name; + private List engines; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Column { + private String name; + private String displayName; + private String type; + private List attributes; + private Object dimension; + private Object partition; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Parent { + private String name; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class CreatedBy { + private String username; + } +} \ No newline at end of file diff --git a/datajunction-clients/java/src/main/java/io/datajunction/models/responses/SQLResponse.java b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/SQLResponse.java new file mode 100644 index 000000000..dc7534598 --- /dev/null +++ b/datajunction-clients/java/src/main/java/io/datajunction/models/responses/SQLResponse.java @@ -0,0 +1,25 @@ +package io.datajunction.models.responses; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SQLResponse { + private String sql; + private List columns; + private String dialect; + private List upstreamTables; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Column { + private String name; + private String type; + private String column; + private String node; + private String semanticEntity; + private String semanticType; + } +} \ No newline at end of file diff --git a/datajunction-clients/java/src/test/java/io/datajunction/client/DJClientTest.java b/datajunction-clients/java/src/test/java/io/datajunction/client/DJClientTest.java new file mode 100644 index 000000000..3b9f7d648 --- /dev/null +++ b/datajunction-clients/java/src/test/java/io/datajunction/client/DJClientTest.java @@ -0,0 +1,121 @@ +package io.datajunction.client; + +import io.datajunction.models.requests.Catalog; +import io.datajunction.models.requests.Column; +import io.datajunction.models.requests.Dimension; +import io.datajunction.models.requests.Engine; +import io.datajunction.models.requests.Metric; +import io.datajunction.models.requests.Source; +import io.datajunction.models.requests.Transform; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.http.HttpClient; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public class DJClientTest { + + private static DJClient dj; + + @BeforeAll + public static void setUp() throws Exception { + HttpClient client = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .build(); + dj = new DJClient(client, "http://localhost:8000"); + + int maxRetries = 5; + int retryDelay = 5; + boolean loginSuccessful = false; + for (int attempt = 1; attempt <= maxRetries; attempt++) { + try { + dj.login("dj", "dj").join(); + loginSuccessful = true; + break; + } catch (Exception e) { + System.err.println("Login attempt " + attempt + " failed: " + e.getMessage()); + if (attempt < maxRetries) { + System.out.println("Retrying in " + retryDelay + " seconds..."); + TimeUnit.SECONDS.sleep(retryDelay); + retryDelay *= 2; + } else { + throw new RuntimeException("Failed to log in after " + maxRetries + " attempts", e); + } + } + } + + if (!loginSuccessful) { + throw new RuntimeException("Failed to log in after " + maxRetries + " attempts"); + } + } + + @Test + public void shouldReturnSomething() throws Exception { + CompletableFuture testFlow = dj.createCatalog(new Catalog("tpch")) + .thenCompose(response -> dj.createEngine(new Engine("trino", "451"))) + .thenCompose(response -> dj.addEngineToCatalog("tpch", new Engine("trino", "451"))) + .thenCompose(response -> dj.createNamespace("integration.tests.trino")) + .thenCompose(response -> dj.createSource(new Source( + "integration.tests.source1", + "unknown", + "db", + "tbl", + "Test Source with Columns", + "A test source node with columns", + List.of( + new Column("id", "int"), + new Column("name", "string"), + new Column("price", "double"), + new Column("created_at", "timestamp") + ), + List.of("id"), + "published", + true + ))) + .thenCompose(response -> dj.registerTable("tpch", "sf1", "orders")) + .thenCompose(response -> dj.createTransform(new Transform( + "integration.tests.trino.transform1", + "Filter to last 1000 records", + "The last 1000 purchases", + "published", + "select custkey, totalprice, orderdate from source.tpch.sf1.orders order by orderdate desc limit 1000", + true + ))) + .thenCompose(response -> dj.createDimension(new Dimension( + "integration.tests.trino.dimension1", + "Customer keys", + "All custkey values in the source table", + "published", + List.of("id"), + List.of(), + "select custkey as id, 'attribute' as foo from source.tpch.sf1.orders", + true + ))) + .thenCompose(response -> dj.linkDimensionToNode( + "integration.tests.trino.transform1", + "custkey", + "integration.tests.trino.dimension1", + "id" + )) + .thenCompose(response -> dj.createMetric(new Metric( + "integration.tests.trino.metric1", + "Total of last 1000 purchases", + "This is the total amount from the last 1000 purchases", + "published", + "select sum(totalprice) from integration.tests.trino.transform1", + true + ))) + .thenCompose(response -> dj.listCommonDimensions(List.of("integration.tests.trino.metric1"))) + .thenCompose(response -> dj.getSQL( + List.of("integration.tests.trino.metric1"), + List.of("integration.tests.trino.dimension1.id"), + List.of() + )) + .thenAccept(query -> assertTrue(query.getSql().contains("SELECT"))); + + testFlow.join(); + } +} diff --git a/datajunction-clients/javascript/.babelrc b/datajunction-clients/javascript/.babelrc new file mode 100644 index 000000000..dc2fcbb01 --- /dev/null +++ b/datajunction-clients/javascript/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": [ + "@babel/preset-env" + ], + "test": [ + "jest" + ] +} diff --git a/datajunction-clients/javascript/.eslintignore b/datajunction-clients/javascript/.eslintignore new file mode 100644 index 000000000..3e39984c5 --- /dev/null +++ b/datajunction-clients/javascript/.eslintignore @@ -0,0 +1 @@ +./dist/* \ No newline at end of file diff --git a/datajunction-clients/javascript/.eslintrc.js b/datajunction-clients/javascript/.eslintrc.js new file mode 100644 index 000000000..fb1c500a3 --- /dev/null +++ b/datajunction-clients/javascript/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: 'standard', + overrides: [], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: {}, +} diff --git a/datajunction-clients/javascript/.gitignore b/datajunction-clients/javascript/.gitignore new file mode 100644 index 000000000..6a7d6d8ef --- /dev/null +++ b/datajunction-clients/javascript/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* \ No newline at end of file diff --git a/src/datajunction/cli/__init__.py b/datajunction-clients/javascript/.npmignore similarity index 100% rename from src/datajunction/cli/__init__.py rename to datajunction-clients/javascript/.npmignore diff --git a/datajunction-clients/javascript/.prettierignore b/datajunction-clients/javascript/.prettierignore new file mode 100644 index 000000000..0f7a61c11 --- /dev/null +++ b/datajunction-clients/javascript/.prettierignore @@ -0,0 +1 @@ +./dist \ No newline at end of file diff --git a/datajunction-clients/javascript/.prettierrc b/datajunction-clients/javascript/.prettierrc new file mode 100644 index 000000000..e74ed9ff3 --- /dev/null +++ b/datajunction-clients/javascript/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 4, + "semi": false, + "singleQuote": true +} diff --git a/datajunction-clients/javascript/Makefile b/datajunction-clients/javascript/Makefile new file mode 100644 index 000000000..4975c8142 --- /dev/null +++ b/datajunction-clients/javascript/Makefile @@ -0,0 +1,3 @@ +dev-release: + yarn version --prerelease --preid dev --no-git-tag-version + npm publish \ No newline at end of file diff --git a/datajunction-clients/javascript/babel.config.js b/datajunction-clients/javascript/babel.config.js new file mode 100644 index 000000000..11687e217 --- /dev/null +++ b/datajunction-clients/javascript/babel.config.js @@ -0,0 +1 @@ +module.exports = {presets: ['@babel/preset-env']} \ No newline at end of file diff --git a/datajunction-clients/javascript/index.js b/datajunction-clients/javascript/index.js new file mode 100644 index 000000000..2d2f3c0bc --- /dev/null +++ b/datajunction-clients/javascript/index.js @@ -0,0 +1 @@ +module.exports = require('./dist') diff --git a/datajunction-clients/javascript/package-lock.json b/datajunction-clients/javascript/package-lock.json new file mode 100644 index 000000000..032126c58 --- /dev/null +++ b/datajunction-clients/javascript/package-lock.json @@ -0,0 +1,18296 @@ +{ + "name": "datajunction", + "version": "0.0.1-rc.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "datajunction", + "version": "0.0.1-rc.1", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.22.5", + "docker-names": "^1.2.1" + }, + "devDependencies": { + "@babel/cli": "^7.0.0", + "@babel/core": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.0.0", + "@babel/preset-env": "^7.0.0", + "babel-core": "^7.0.0-bridge.0", + "babel-jest": "^23.4.2", + "eslint": "^8.39.0", + "eslint-config-standard": "^17.0.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-n": "^15.7.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.5.0", + "prettier": "^2.8.8", + "webpack": "^5.81.0", + "webpack-cli": "^5.0.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/cli": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.22.5.tgz", + "integrity": "sha512-N5d7MjzwsQ2wppwjhrsicVDhJSqF9labEP/swYiHhio4Ca2XjEehpgPmerjnLQl7BPE59BLud0PTWGYwqFl/cQ==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.2.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0" + }, + "bin": { + "babel": "bin/babel.js", + "babel-external-helpers": "bin/babel-external-helpers.js" + }, + "engines": { + "node": ">=6.9.0" + }, + "optionalDependencies": { + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/cli/node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "optional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@babel/cli/node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@babel/cli/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "optional": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@babel/cli/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "optional": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@babel/cli/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@babel/cli/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@babel/cli/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/@babel/cli/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "optional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@babel/cli/node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@babel/cli/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/cli/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/cli/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@babel/cli/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/cli/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/cli/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "optional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@babel/cli/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@babel/cli/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/cli/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", + "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", + "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", + "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", + "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz", + "integrity": "sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.5.tgz", + "integrity": "sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.0.tgz", + "integrity": "sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz", + "integrity": "sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz", + "integrity": "sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz", + "integrity": "sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", + "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", + "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.5.tgz", + "integrity": "sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", + "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", + "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.5.tgz", + "integrity": "sha512-2edQhLfibpWpsVBx2n/GKOz6JdGQvLruZQfGr9l1qes2KQaWswjBzhQF7UDUZMNaMMQeYnQzxwOMPsbYF7wqPQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", + "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", + "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", + "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", + "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", + "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", + "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", + "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", + "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", + "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", + "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", + "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", + "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.5.tgz", + "integrity": "sha512-AconbMKOMkyG+xCng2JogMCDcqW8wedQAqpVIL4cOSescZ7+iW8utC6YDZLMCSUIReEA733gzRSaOSXMAt/4WQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", + "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", + "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", + "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator/node_modules/regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", + "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.5.tgz", + "integrity": "sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.5", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.5", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.5", + "@babel/plugin-transform-classes": "^7.22.5", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.5", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.5", + "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", + "@babel/plugin-transform-numeric-separator": "^7.22.5", + "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5", + "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.5", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.5", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.3", + "babel-plugin-polyfill-corejs3": "^0.8.1", + "babel-plugin-polyfill-regenerator": "^0.5.0", + "core-js-compat": "^3.30.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", + "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", + "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types/node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", + "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.1", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@eslint/js": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", + "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", + "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/reporters": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.5.0", + "jest-config": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-resolve-dependencies": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "jest-watcher": "^29.5.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@jest/core/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@jest/core/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", + "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "dev": true, + "dependencies": { + "expect": "^29.5.0", + "jest-snapshot": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", + "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", + "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/types": "^29.5.0", + "jest-mock": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", + "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.25.16" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", + "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.15", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", + "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", + "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", + "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@jest/transform/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@jest/transform/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@jest/transform/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "dev": true, + "optional": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", + "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", + "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/eslint": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", + "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz", + "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==", + "dev": true + }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.5.tgz", + "integrity": "sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.5.tgz", + "integrity": "sha512-1j1zTIC5EZOtCplMBG/IEwLtUojtwFVwdyVMbL/hwWqbzlQoJsWCOavrdnLkemwNoC/EOwtUFch3fuo+cbcXYQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.5.tgz", + "integrity": "sha512-L65bDPmfpY0+yFrsgz8b6LhXmbbs38OnwDCf6NpnMUYqa+ENfE5Dq9E42ny0qz/PdR0LJyq/T5YijPnU8AXEpA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.5.tgz", + "integrity": "sha512-fDKo1gstwFFSfacIeH5KfwzjykIE6ldh1iH9Y/8YkAZrhmu4TctqYjSh7t0K2VyDSXOZJ1MLhht/k9IvYGcIxg==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.5.tgz", + "integrity": "sha512-DhykHXM0ZABqfIGYNv93A5KKDw/+ywBFnuWybZZWcuzWHfbp21wUfRkbtz7dMGwGgT4iXjWuhRMA2Mzod6W4WA==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.5", + "@webassemblyjs/helper-api-error": "1.11.5", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.5.tgz", + "integrity": "sha512-oC4Qa0bNcqnjAowFn7MPCETQgDYytpsfvz4ujZz63Zu/a/v71HeCAAmZsgZ3YVKec3zSPYytG3/PrRCqbtcAvA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.5.tgz", + "integrity": "sha512-uEoThA1LN2NA+K3B9wDo3yKlBfVtC6rh0i4/6hvbz071E8gTNZD/pT0MsBf7MeD6KbApMSkaAK0XeKyOZC7CIA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-buffer": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/wasm-gen": "1.11.5" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.5.tgz", + "integrity": "sha512-37aGq6qVL8A8oPbPrSGMBcp38YZFXcHfiROflJn9jxSdSMMM5dS5P/9e2/TpaJuhE+wFrbukN2WI6Hw9MH5acg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.5.tgz", + "integrity": "sha512-ajqrRSXaTJoPW+xmkfYN6l8VIeNnR4vBOTQO9HzR7IygoCcKWkICbKFbVTNMjMgMREqXEr0+2M6zukzM47ZUfQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.5.tgz", + "integrity": "sha512-WiOhulHKTZU5UPlRl53gHR8OxdGsSOxqfpqWeA2FmcwBMaoEdz6b2x2si3IwC9/fSPLfe8pBMRTHVMk5nlwnFQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.5.tgz", + "integrity": "sha512-C0p9D2fAu3Twwqvygvf42iGCQ4av8MFBLiTb+08SZ4cEdwzWx9QeAHDo1E2k+9s/0w1DM40oflJOpkZ8jW4HCQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-buffer": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/helper-wasm-section": "1.11.5", + "@webassemblyjs/wasm-gen": "1.11.5", + "@webassemblyjs/wasm-opt": "1.11.5", + "@webassemblyjs/wasm-parser": "1.11.5", + "@webassemblyjs/wast-printer": "1.11.5" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.5.tgz", + "integrity": "sha512-14vteRlRjxLK9eSyYFvw1K8Vv+iPdZU0Aebk3j6oB8TQiQYuO6hj9s4d7qf6f2HJr2khzvNldAFG13CgdkAIfA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/ieee754": "1.11.5", + "@webassemblyjs/leb128": "1.11.5", + "@webassemblyjs/utf8": "1.11.5" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.5.tgz", + "integrity": "sha512-tcKwlIXstBQgbKy1MlbDMlXaxpucn42eb17H29rawYLxm5+MsEmgPzeCP8B1Cl69hCice8LeKgZpRUAPtqYPgw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-buffer": "1.11.5", + "@webassemblyjs/wasm-gen": "1.11.5", + "@webassemblyjs/wasm-parser": "1.11.5" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.5.tgz", + "integrity": "sha512-SVXUIwsLQlc8srSD7jejsfTU83g7pIGr2YYNb9oHdtldSxaOhvA5xwvIiWIfcX8PlSakgqMXsLpLfbbJ4cBYew==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-api-error": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/ieee754": "1.11.5", + "@webassemblyjs/leb128": "1.11.5", + "@webassemblyjs/utf8": "1.11.5" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.5.tgz", + "integrity": "sha512-f7Pq3wvg3GSPUPzR0F6bmI89Hdb+u9WXrSKc4v+N0aV0q6r42WoF92Jp2jEorBEBRoRNXgjp53nBniDXcqZYPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.0.1.tgz", + "integrity": "sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.1.tgz", + "integrity": "sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.2.tgz", + "integrity": "sha512-S9h3GmOmzUseyeFW3tYNnWS7gNUuwxZ3mmMq0JyW78Vx1SGKPSkt5bT4pB0rUnVfHjP0EL9gW2bOzmtiTfQt0A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha512-dtXTVMkh6VkEEA7OhXnN1Ecb8aAGFdZ1LFxtOCoqj4qkyOJMt7+qs6Ahdy6p/NQCPYsRSXXivhSB/J5E9jmYKA==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha512-G2n5bG5fSUCpnsXz4+8FUkYsGPkNfLn9YvS66U5qbTIXI2Ynnlo4Bi42bWv+omKUCqz+ejzfClwne0alJWJPhg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "node_modules/babel-core": { + "version": "7.0.0-bridge.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", + "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", + "dev": true, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "dependencies": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "node_modules/babel-jest": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz", + "integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==", + "dev": true, + "dependencies": { + "babel-plugin-istanbul": "^4.1.6", + "babel-preset-jest": "^23.2.0" + }, + "peerDependencies": { + "babel-core": "^6.0.0 || ^7.0.0-0" + } + }, + "node_modules/babel-jest/node_modules/babel-plugin-istanbul": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", + "integrity": "sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-object-rest-spread": "^6.13.0", + "find-up": "^2.1.0", + "istanbul-lib-instrument": "^1.10.1", + "test-exclude": "^4.2.1" + } + }, + "node_modules/babel-jest/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-jest/node_modules/istanbul-lib-coverage": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", + "dev": true + }, + "node_modules/babel-jest/node_modules/istanbul-lib-instrument": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", + "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", + "dev": true, + "dependencies": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.1", + "semver": "^5.3.0" + } + }, + "node_modules/babel-jest/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-jest/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-jest/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-jest/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-jest/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-jest/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/babel-jest/node_modules/test-exclude": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.3.tgz", + "integrity": "sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "micromatch": "^2.3.11", + "object-assign": "^4.1.0", + "read-pkg-up": "^1.0.1", + "require-main-filename": "^1.0.1" + } + }, + "node_modules/babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz", + "integrity": "sha512-N0MlMjZtahXK0yb0K3V9hWPrq5e7tThbghvDr0k3X75UuOOqwsWW6mk8XHD2QvEC0Ca9dLIfTgNU36TeJD6Hnw==", + "dev": true + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz", + "integrity": "sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.4.0", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.1.tgz", + "integrity": "sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.0", + "core-js-compat": "^3.30.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.0.tgz", + "integrity": "sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha512-C4Aq+GaAj83pRQ0EFgTvw5YO6T3Qz2KGrNRwIj9mSoNHVvdZY4KO2uA6HNtNXCw993iSZnckY1aLW8nOi8i4+w==", + "dev": true + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz", + "integrity": "sha512-AdfWwc0PYvDtwr009yyVNh72Ev68os7SsPmOFVX7zSA+STXuk5CV2iMVazZU01bEoHCSwTkgv4E4HOOcODPkPg==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^23.2.0", + "babel-plugin-syntax-object-rest-spread": "^6.13.0" + } + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dev": true, + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "node_modules/babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", + "dev": true, + "dependencies": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "node_modules/babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true, + "bin": { + "babylon": "bin/babylon.js" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==", + "dev": true, + "dependencies": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "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 + }, + "node_modules/builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/builtins/node_modules/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001482", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001482.tgz", + "integrity": "sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "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 + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, + "hasInstallScript": true + }, + "node_modules/core-js-compat": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.0.tgz", + "integrity": "sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==", + "dev": true, + "dependencies": { + "repeating": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/docker-names": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/docker-names/-/docker-names-1.2.1.tgz", + "integrity": "sha512-uh42tvWBp10fnMyJ9z0YL9kql+iolxEnQ+pGZANj+gcg/N2NxjrdHbMXT2Y2Y07A8Jf7KJp/6LkElPCfukCdWg==" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.379", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.379.tgz", + "integrity": "sha512-eRMq6Cf4PhjB14R9U6QcXM/VRQ54Gc3OL9LKnFugUIh2AXm3KJlOizlSfVIgjH76bII4zHGK4t0PVTE5qq8dZg==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz", + "integrity": "sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-module-lexer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", + "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "dev": true + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.2", + "@eslint/js": "8.39.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.0", + "espree": "^9.5.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-standard": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz", + "integrity": "sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peerDependencies": { + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0", + "eslint-plugin-promise": "^6.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/eslint-plugin-n": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", + "dev": true, + "dependencies": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-promise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", + "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", + "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha512-hxx03P2dJxss6ceIeri9cmYOT4SRs3Zk3afZwWpOsRqLqprhTR8u++SlC+sFGsQr7WGFPdMF7Gjc1njDLDK6UA==", + "dev": true, + "dependencies": { + "is-posix-bracket": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA==", + "dev": true, + "dependencies": { + "fill-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha512-1FOj1LOwn42TMrruOHGt18HemVnbwAmAak7krWk+wa93KXxGbK+2jpezm+ytJYDaBX0/SPLZFHKM7m+tKobWGg==", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha512-BTCqyBaWBTsauvnHiE8i562+EdJj+oUpkqWp2R1iCoR8f6oo8STRu3of7WJJ0TqWtxN50a5YFpzYK4Jj9esYfQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "dependencies": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA==", + "dev": true, + "dependencies": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w==", + "dev": true, + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha512-9YclgOGtN/f8zx0Pr4FQYMdibBiTaH3sn52vjYip4ZSf6C4/6RfTEZ+MR4GvKhCxdPh21Bg42/WL55f6KSnKpg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha512-0EygVC5qPvIyb+gSz7zdD5/AAoS6Qrx1e//6N4yv4oNm30kqvdmG66oZFWVlQHUWe5OjP08FuTw2IdT0EOTcYA==", + "dev": true, + "dependencies": { + "is-primitive": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha512-Yu68oeXJ7LeWNmZ3Zov/xg/oDBnBK2RNxwYY1ilNJX+tKKZqgPK+qOn/Gs9jEu66KDY9Netf5XLKNGzas/vPfQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha512-N3w1tFaRfk3UrPfqeRyD+GYDASU3W5VinKhlORy8EWVf/sIdDL9GAcew85XmktCfH+ngG7SRXEVDoO18WMdB/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/istanbul-lib-source-maps/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "dev": true, + "dependencies": { + "@jest/core": "^29.5.0", + "@jest/types": "^29.5.0", + "import-local": "^3.0.2", + "jest-cli": "^29.5.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", + "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", + "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.5.0", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.5.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", + "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", + "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.5.0", + "@jest/types": "^29.5.0", + "babel-jest": "^29.5.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.5.0", + "jest-environment-node": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/babel-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", + "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.5.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.5.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/jest-config/node_modules/babel-plugin-jest-hoist": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", + "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/babel-preset-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", + "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.5.0", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/jest-config/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-config/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/jest-config/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", + "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", + "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "jest-util": "^29.5.0", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-node": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", + "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", + "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/jest-haste-map/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-haste-map/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-haste-map/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/jest-haste-map/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-haste-map/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", + "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.5.0", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-message-util/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/jest-message-util/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/jest-mock": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", + "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", + "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", + "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", + "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.4.3", + "jest-snapshot": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", + "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/environment": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.4.3", + "jest-environment-node": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-leak-detector": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-resolve": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-util": "^29.5.0", + "jest-watcher": "^29.5.0", + "jest-worker": "^29.5.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-runner/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", + "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/globals": "^29.5.0", + "@jest/source-map": "^29.4.3", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", + "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.5.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", + "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "leven": "^3.1.0", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", + "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.5.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-sdsl": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-random": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==", + "dev": true, + "dependencies": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha512-UiAM5mhmIuKLsOvrL+B0U2d1hXHF3bFYWIuH1LMpuV2EJEHG1Ntz06PgLEHjm6VFd87NpH8rastvPoyv6UW2fA==", + "dev": true, + "dependencies": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha512-FC5TeK0AwXzq3tUBFtH74naWkPQCEWs4K+xMxWZBlKDWu0bVHXGZa+KKqxKidd7xwhdZ19ZNuF2uO1M/r196HA==", + "dev": true, + "dependencies": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-type/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha512-s/46sYeylUfHNjI+sA/78FAHlmIuKqI9wNnzEOGehAlUUYeObv5C2mOinXBjyUyWmJ2SfcS2/ydApH4hTF4WXQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", + "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dev": true, + "dependencies": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/randomatic/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/randomatic/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "dev": true, + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "dev": true, + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "dev": true, + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dev": true, + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "dependencies": { + "is-equal-shallow": "^0.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", + "dev": true, + "dependencies": { + "is-finite": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/schema-utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", + "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz", + "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", + "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/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, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.81.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.81.0.tgz", + "integrity": "sha512-AAjaJ9S4hYCVODKLQTgG5p5e11hiMawBwV2v8MYLE0C/6UAGLuAF4n1qa9GOwdxnicaP+5k6M5HrLmD4+gIB8Q==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.13.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.2.tgz", + "integrity": "sha512-4y3W5Dawri5+8dXm3+diW6Mn1Ya+Dei6eEVAdIduAmYNLzv1koKVAqsfgrrc9P2mhrYHQphx5htnGkcNwtubyQ==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.0.1", + "@webpack-cli/info": "^2.0.1", + "@webpack-cli/serve": "^2.0.2", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/cli": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.22.5.tgz", + "integrity": "sha512-N5d7MjzwsQ2wppwjhrsicVDhJSqF9labEP/swYiHhio4Ca2XjEehpgPmerjnLQl7BPE59BLud0PTWGYwqFl/cQ==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.4.0", + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.2.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0" + }, + "dependencies": { + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "optional": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "optional": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "optional": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "optional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "optional": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "optional": true + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "optional": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "optional": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.5" + } + }, + "@babel/compat-data": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", + "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "dev": true + }, + "@babel/core": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", + "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "dependencies": { + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", + "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", + "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz", + "integrity": "sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "semver": "^6.3.0" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.5.tgz", + "integrity": "sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + }, + "regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "requires": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + } + } + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.0.tgz", + "integrity": "sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz", + "integrity": "sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-replace-supers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz", + "integrity": "sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz", + "integrity": "sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helpers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", + "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "dev": true + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", + "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "requires": {} + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-async-generator-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.5.tgz", + "integrity": "sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", + "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-class-static-block": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", + "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.5.tgz", + "integrity": "sha512-2edQhLfibpWpsVBx2n/GKOz6JdGQvLruZQfGr9l1qes2KQaWswjBzhQF7UDUZMNaMMQeYnQzxwOMPsbYF7wqPQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", + "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-dynamic-import": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", + "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-export-namespace-from": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", + "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", + "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-json-strings": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", + "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", + "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", + "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", + "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", + "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-transform-numeric-separator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", + "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-transform-object-rest-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", + "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.5" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + } + }, + "@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", + "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-transform-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.5.tgz", + "integrity": "sha512-AconbMKOMkyG+xCng2JogMCDcqW8wedQAqpVIL4cOSescZ7+iW8utC6YDZLMCSUIReEA733gzRSaOSXMAt/4WQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", + "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-private-property-in-object": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", + "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", + "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.1" + }, + "dependencies": { + "regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + } + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", + "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/preset-env": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.5.tgz", + "integrity": "sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.5", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.5", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.5", + "@babel/plugin-transform-classes": "^7.22.5", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.5", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.5", + "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", + "@babel/plugin-transform-numeric-separator": "^7.22.5", + "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5", + "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.5", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.5", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.3", + "babel-plugin-polyfill-corejs3": "^0.8.1", + "babel-plugin-polyfill-regenerator": "^0.5.0", + "core-js-compat": "^3.30.2", + "semver": "^6.3.0" + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "@babel/runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", + "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.11" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + } + } + }, + "@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/traverse": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", + "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + } + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", + "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.1", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@eslint/js": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", + "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/core": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", + "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "dev": true, + "requires": { + "@jest/console": "^29.5.0", + "@jest/reporters": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.5.0", + "jest-config": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-resolve-dependencies": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "jest-watcher": "^29.5.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@jest/environment": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", + "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0" + } + }, + "@jest/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "dev": true, + "requires": { + "expect": "^29.5.0", + "jest-snapshot": "^29.5.0" + } + }, + "@jest/expect-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "dev": true, + "requires": { + "jest-get-type": "^29.4.3" + } + }, + "@jest/fake-timers": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", + "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + } + }, + "@jest/globals": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", + "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/types": "^29.5.0", + "jest-mock": "^29.5.0" + } + }, + "@jest/reporters": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", + "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.25.16" + } + }, + "@jest/source-map": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", + "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.15", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", + "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "dev": true, + "requires": { + "@jest/console": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", + "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "dev": true, + "requires": { + "@jest/test-result": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "slash": "^3.0.0" + }, + "dependencies": { + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + } + } + }, + "@jest/transform": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", + "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "dev": true, + "optional": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@types/babel__core": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", + "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", + "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/eslint": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", + "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "@types/node": { + "version": "18.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz", + "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==", + "dev": true + }, + "@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.5.tgz", + "integrity": "sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.5.tgz", + "integrity": "sha512-1j1zTIC5EZOtCplMBG/IEwLtUojtwFVwdyVMbL/hwWqbzlQoJsWCOavrdnLkemwNoC/EOwtUFch3fuo+cbcXYQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.5.tgz", + "integrity": "sha512-L65bDPmfpY0+yFrsgz8b6LhXmbbs38OnwDCf6NpnMUYqa+ENfE5Dq9E42ny0qz/PdR0LJyq/T5YijPnU8AXEpA==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.5.tgz", + "integrity": "sha512-fDKo1gstwFFSfacIeH5KfwzjykIE6ldh1iH9Y/8YkAZrhmu4TctqYjSh7t0K2VyDSXOZJ1MLhht/k9IvYGcIxg==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.5.tgz", + "integrity": "sha512-DhykHXM0ZABqfIGYNv93A5KKDw/+ywBFnuWybZZWcuzWHfbp21wUfRkbtz7dMGwGgT4iXjWuhRMA2Mzod6W4WA==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.5", + "@webassemblyjs/helper-api-error": "1.11.5", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.5.tgz", + "integrity": "sha512-oC4Qa0bNcqnjAowFn7MPCETQgDYytpsfvz4ujZz63Zu/a/v71HeCAAmZsgZ3YVKec3zSPYytG3/PrRCqbtcAvA==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.5.tgz", + "integrity": "sha512-uEoThA1LN2NA+K3B9wDo3yKlBfVtC6rh0i4/6hvbz071E8gTNZD/pT0MsBf7MeD6KbApMSkaAK0XeKyOZC7CIA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-buffer": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/wasm-gen": "1.11.5" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.5.tgz", + "integrity": "sha512-37aGq6qVL8A8oPbPrSGMBcp38YZFXcHfiROflJn9jxSdSMMM5dS5P/9e2/TpaJuhE+wFrbukN2WI6Hw9MH5acg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.5.tgz", + "integrity": "sha512-ajqrRSXaTJoPW+xmkfYN6l8VIeNnR4vBOTQO9HzR7IygoCcKWkICbKFbVTNMjMgMREqXEr0+2M6zukzM47ZUfQ==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.5.tgz", + "integrity": "sha512-WiOhulHKTZU5UPlRl53gHR8OxdGsSOxqfpqWeA2FmcwBMaoEdz6b2x2si3IwC9/fSPLfe8pBMRTHVMk5nlwnFQ==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.5.tgz", + "integrity": "sha512-C0p9D2fAu3Twwqvygvf42iGCQ4av8MFBLiTb+08SZ4cEdwzWx9QeAHDo1E2k+9s/0w1DM40oflJOpkZ8jW4HCQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-buffer": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/helper-wasm-section": "1.11.5", + "@webassemblyjs/wasm-gen": "1.11.5", + "@webassemblyjs/wasm-opt": "1.11.5", + "@webassemblyjs/wasm-parser": "1.11.5", + "@webassemblyjs/wast-printer": "1.11.5" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.5.tgz", + "integrity": "sha512-14vteRlRjxLK9eSyYFvw1K8Vv+iPdZU0Aebk3j6oB8TQiQYuO6hj9s4d7qf6f2HJr2khzvNldAFG13CgdkAIfA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/ieee754": "1.11.5", + "@webassemblyjs/leb128": "1.11.5", + "@webassemblyjs/utf8": "1.11.5" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.5.tgz", + "integrity": "sha512-tcKwlIXstBQgbKy1MlbDMlXaxpucn42eb17H29rawYLxm5+MsEmgPzeCP8B1Cl69hCice8LeKgZpRUAPtqYPgw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-buffer": "1.11.5", + "@webassemblyjs/wasm-gen": "1.11.5", + "@webassemblyjs/wasm-parser": "1.11.5" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.5.tgz", + "integrity": "sha512-SVXUIwsLQlc8srSD7jejsfTU83g7pIGr2YYNb9oHdtldSxaOhvA5xwvIiWIfcX8PlSakgqMXsLpLfbbJ4cBYew==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-api-error": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/ieee754": "1.11.5", + "@webassemblyjs/leb128": "1.11.5", + "@webassemblyjs/utf8": "1.11.5" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.5.tgz", + "integrity": "sha512-f7Pq3wvg3GSPUPzR0F6bmI89Hdb+u9WXrSKc4v+N0aV0q6r42WoF92Jp2jEorBEBRoRNXgjp53nBniDXcqZYPA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.5", + "@xtuc/long": "4.2.2" + } + }, + "@webpack-cli/configtest": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.0.1.tgz", + "integrity": "sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A==", + "dev": true, + "requires": {} + }, + "@webpack-cli/info": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.1.tgz", + "integrity": "sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA==", + "dev": true, + "requires": {} + }, + "@webpack-cli/serve": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.2.tgz", + "integrity": "sha512-S9h3GmOmzUseyeFW3tYNnWS7gNUuwxZ3mmMq0JyW78Vx1SGKPSkt5bT4pB0rUnVfHjP0EL9gW2bOzmtiTfQt0A==", + "dev": true, + "requires": {} + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + }, + "acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "requires": {} + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha512-dtXTVMkh6VkEEA7OhXnN1Ecb8aAGFdZ1LFxtOCoqj4qkyOJMt7+qs6Ahdy6p/NQCPYsRSXXivhSB/J5E9jmYKA==", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha512-G2n5bG5fSUCpnsXz4+8FUkYsGPkNfLn9YvS66U5qbTIXI2Ynnlo4Bi42bWv+omKUCqz+ejzfClwne0alJWJPhg==", + "dev": true + }, + "array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "babel-core": { + "version": "7.0.0-bridge.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", + "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", + "dev": true, + "requires": {} + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "babel-jest": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz", + "integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==", + "dev": true, + "requires": { + "babel-plugin-istanbul": "^4.1.6", + "babel-preset-jest": "^23.2.0" + }, + "dependencies": { + "babel-plugin-istanbul": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", + "integrity": "sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.13.0", + "find-up": "^2.1.0", + "istanbul-lib-instrument": "^1.10.1", + "test-exclude": "^4.2.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "istanbul-lib-coverage": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", + "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", + "dev": true, + "requires": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.1", + "semver": "^5.3.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "test-exclude": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.3.tgz", + "integrity": "sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "micromatch": "^2.3.11", + "object-assign": "^4.1.0", + "read-pkg-up": "^1.0.1", + "require-main-filename": "^1.0.1" + } + } + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz", + "integrity": "sha512-N0MlMjZtahXK0yb0K3V9hWPrq5e7tThbghvDr0k3X75UuOOqwsWW6mk8XHD2QvEC0Ca9dLIfTgNU36TeJD6Hnw==", + "dev": true + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz", + "integrity": "sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.4.0", + "semver": "^6.1.1" + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.1.tgz", + "integrity": "sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.4.0", + "core-js-compat": "^3.30.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.0.tgz", + "integrity": "sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.4.0" + } + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha512-C4Aq+GaAj83pRQ0EFgTvw5YO6T3Qz2KGrNRwIj9mSoNHVvdZY4KO2uA6HNtNXCw993iSZnckY1aLW8nOi8i4+w==", + "dev": true + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz", + "integrity": "sha512-AdfWwc0PYvDtwr009yyVNh72Ev68os7SsPmOFVX7zSA+STXuk5CV2iMVazZU01bEoHCSwTkgv4E4HOOcODPkPg==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^23.2.0", + "babel-plugin-syntax-object-rest-spread": "^6.13.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + } + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "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 + }, + "builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "requires": { + "semver": "^7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001482", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001482.tgz", + "integrity": "sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "dev": true + }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "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 + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "dev": true + }, + "core-js-compat": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.0.tgz", + "integrity": "sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==", + "dev": true, + "requires": { + "browserslist": "^4.21.5" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true + }, + "docker-names": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/docker-names/-/docker-names-1.2.1.tgz", + "integrity": "sha512-uh42tvWBp10fnMyJ9z0YL9kql+iolxEnQ+pGZANj+gcg/N2NxjrdHbMXT2Y2Y07A8Jf7KJp/6LkElPCfukCdWg==" + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "electron-to-chromium": { + "version": "1.4.379", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.379.tgz", + "integrity": "sha512-eRMq6Cf4PhjB14R9U6QcXM/VRQ54Gc3OL9LKnFugUIh2AXm3KJlOizlSfVIgjH76bII4zHGK4t0PVTE5qq8dZg==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "enhanced-resolve": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz", + "integrity": "sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + } + }, + "es-module-lexer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", + "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "dev": true + }, + "es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "eslint": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.2", + "@eslint/js": "8.39.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.0", + "espree": "^9.5.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "eslint-config-standard": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz", + "integrity": "sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==", + "dev": true, + "requires": {} + }, + "eslint-import-resolver-node": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "eslint-plugin-n": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", + "dev": true, + "requires": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" + }, + "dependencies": { + "semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "eslint-plugin-promise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", + "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", + "dev": true, + "requires": {} + }, + "eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "dev": true + }, + "espree": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", + "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha512-hxx03P2dJxss6ceIeri9cmYOT4SRs3Zk3afZwWpOsRqLqprhTR8u++SlC+sFGsQr7WGFPdMF7Gjc1njDLDK6UA==", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA==", + "dev": true, + "requires": { + "fill-range": "^2.1.0" + } + }, + "expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha512-1FOj1LOwn42TMrruOHGt18HemVnbwAmAak7krWk+wa93KXxGbK+2jpezm+ytJYDaBX0/SPLZFHKM7m+tKobWGg==", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha512-BTCqyBaWBTsauvnHiE8i562+EdJj+oUpkqWp2R1iCoR8f6oo8STRu3of7WJJ0TqWtxN50a5YFpzYK4Jj9esYfQ==", + "dev": true + }, + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA==", + "dev": true, + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w==", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha512-9YclgOGtN/f8zx0Pr4FQYMdibBiTaH3sn52vjYip4ZSf6C4/6RfTEZ+MR4GvKhCxdPh21Bg42/WL55f6KSnKpg==", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha512-0EygVC5qPvIyb+gSz7zdD5/AAoS6Qrx1e//6N4yv4oNm30kqvdmG66oZFWVlQHUWe5OjP08FuTw2IdT0EOTcYA==", + "dev": true, + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "dev": true + }, + "is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha512-Yu68oeXJ7LeWNmZ3Zov/xg/oDBnBK2RNxwYY1ilNJX+tKKZqgPK+qOn/Gs9jEu66KDY9Netf5XLKNGzas/vPfQ==", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha512-N3w1tFaRfk3UrPfqeRyD+GYDASU3W5VinKhlORy8EWVf/sIdDL9GAcew85XmktCfH+ngG7SRXEVDoO18WMdB/Q==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "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 + } + } + }, + "istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "dev": true, + "requires": { + "@jest/core": "^29.5.0", + "@jest/types": "^29.5.0", + "import-local": "^3.0.2", + "jest-cli": "^29.5.0" + } + }, + "jest-changed-files": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", + "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + } + }, + "jest-circus": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", + "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "dev": true, + "requires": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.5.0", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.5.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-cli": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", + "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "dev": true, + "requires": { + "@jest/core": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-config": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", + "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.5.0", + "@jest/types": "^29.5.0", + "babel-jest": "^29.5.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.5.0", + "jest-environment-node": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "babel-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", + "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "dev": true, + "requires": { + "@jest/transform": "^29.5.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.5.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", + "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", + "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.5.0", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-docblock": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", + "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", + "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "jest-util": "^29.5.0", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-environment-node": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", + "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "dev": true, + "requires": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + } + }, + "jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true + }, + "jest-haste-map": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", + "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "dependencies": { + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-leak-detector": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", + "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "dev": true, + "requires": { + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + } + }, + "jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-message-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.5.0", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-mock": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", + "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-util": "^29.5.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", + "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "dev": true + }, + "jest-resolve": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", + "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-resolve-dependencies": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", + "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "dev": true, + "requires": { + "jest-regex-util": "^29.4.3", + "jest-snapshot": "^29.5.0" + } + }, + "jest-runner": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", + "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "dev": true, + "requires": { + "@jest/console": "^29.5.0", + "@jest/environment": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.4.3", + "jest-environment-node": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-leak-detector": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-resolve": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-util": "^29.5.0", + "jest-watcher": "^29.5.0", + "jest-worker": "^29.5.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "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 + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-runtime": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", + "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", + "dev": true, + "requires": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/globals": "^29.5.0", + "@jest/source-map": "^29.4.3", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-snapshot": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", + "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.5.0", + "semver": "^7.3.5" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-validate": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", + "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "leven": "^3.1.0", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-watcher": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", + "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "dev": true, + "requires": { + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.5.0", + "string-length": "^4.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-sdsl": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "math-random": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha512-UiAM5mhmIuKLsOvrL+B0U2d1hXHF3bFYWIuH1LMpuV2EJEHG1Ntz06PgLEHjm6VFd87NpH8rastvPoyv6UW2fA==", + "dev": true, + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + } + }, + "object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha512-FC5TeK0AwXzq3tUBFtH74naWkPQCEWs4K+xMxWZBlKDWu0bVHXGZa+KKqxKidd7xwhdZ19ZNuF2uO1M/r196HA==", + "dev": true, + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } + } + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha512-s/46sYeylUfHNjI+sA/78FAHlmIuKqI9wNnzEOGehAlUUYeObv5C2mOinXBjyUyWmJ2SfcS2/ydApH4hTF4WXQ==", + "dev": true + }, + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true + }, + "pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "pure-rand": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", + "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dev": true, + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + } + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + } + } + }, + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "requires": { + "resolve": "^1.20.0" + } + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regexp.prototype.flags": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "dev": true + }, + "resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "requires": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "schema-utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", + "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + } + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true + }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "terser": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz", + "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "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 + }, + "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, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, + "terser-webpack-plugin": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", + "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.5" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==", + "dev": true + }, + "tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "webpack": { + "version": "5.81.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.81.0.tgz", + "integrity": "sha512-AAjaJ9S4hYCVODKLQTgG5p5e11hiMawBwV2v8MYLE0C/6UAGLuAF4n1qa9GOwdxnicaP+5k6M5HrLmD4+gIB8Q==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.13.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "webpack-cli": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.2.tgz", + "integrity": "sha512-4y3W5Dawri5+8dXm3+diW6Mn1Ya+Dei6eEVAdIduAmYNLzv1koKVAqsfgrrc9P2mhrYHQphx5htnGkcNwtubyQ==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.0.1", + "@webpack-cli/info": "^2.0.1", + "@webpack-cli/serve": "^2.0.2", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true + } + } + }, + "webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + } + }, + "wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/datajunction-clients/javascript/package.json b/datajunction-clients/javascript/package.json new file mode 100644 index 000000000..2552fce3e --- /dev/null +++ b/datajunction-clients/javascript/package.json @@ -0,0 +1,50 @@ +{ + "name": "datajunction", + "version": "0.0.1a87", + "description": "A Javascript client for interacting with a DataJunction server", + "module": "src/index.js", + "scripts": { + "test": "jest", + "test-watch": "jest --watch", + "build": "rm -rf dist/* && webpack && babel src -d dist", + "lint": "prettier \"src/**/*.{js,jsx}\"", + "format": "prettier --write \"src/**/*.{js,jsx}\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/DataJunction/dj.git" + }, + "keywords": [ + "datajunction", + "metrics", + "metrics-platform", + "semantic-layer" + ], + "author": "DataJunction Authors", + "license": "MIT", + "bugs": { + "url": "https://github.com/DataJunction/dj/issues" + }, + "homepage": "https://github.com/DataJunction/dj#readme", + "devDependencies": { + "@babel/cli": "^7.0.0", + "@babel/core": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.0.0", + "@babel/preset-env": "^7.0.0", + "babel-core": "^7.0.0-bridge.0", + "babel-jest": "^23.4.2", + "eslint": "^8.39.0", + "eslint-config-standard": "^17.0.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-n": "^15.7.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.5.0", + "prettier": "^2.8.8", + "webpack": "^5.81.0", + "webpack-cli": "^5.0.2" + }, + "dependencies": { + "@babel/core": "^7.22.5", + "docker-names": "^1.2.1" + } +} diff --git a/datajunction-clients/javascript/src/httpclient.js b/datajunction-clients/javascript/src/httpclient.js new file mode 100644 index 000000000..bd920d3e7 --- /dev/null +++ b/datajunction-clients/javascript/src/httpclient.js @@ -0,0 +1,122 @@ +export default class HttpClient { + constructor(options = {}) { + this._baseURL = options.baseURL || ''; + this._headers = options.headers || {}; + this._cookie = ''; + } + + async _fetchJSON(endpoint, options = {}) { + const res = await fetch(this._baseURL + endpoint, { + ...options, + headers: { + ...this._headers, + 'Cookie': this._cookie, + }, + credentials: 'include', + }); + + const setCookieHeader = res.headers.get('Set-Cookie'); + if (setCookieHeader) { + this._cookie = setCookieHeader; + this._headers['Cookie'] = this._cookie; + } + + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Request failed: ${res.status} ${errorText}`); + } + + if (options.parseResponse !== false && res.status !== 204) { + return res.json(); + } + + return undefined; + } + + async login(username, password) { + const body = new URLSearchParams({ + grant_type: 'password', + username: username, + password: password, + }); + + const response = await fetch(this._baseURL + '/basic/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + ...this._headers, + }, + body: body.toString(), + credentials: 'include', + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Login failed: ${response.status} ${errorText}`); + } + + const setCookieHeader = response.headers.get('Set-Cookie'); + if (setCookieHeader) { + this._cookie = setCookieHeader; + this._headers['Cookie'] = this._cookie; + } + } + setHeader(key, value) { + this._headers[key] = value + return this + } + + getHeader(key) { + return this._headers[key] + } + + setBasicAuth(username, password) { + this._headers.Authorization = `Basic ${btoa(`${username}:${password}`)}` + return this + } + + setBearerAuth(token) { + this._headers.Authorization = `Bearer ${token}` + return this + } + + async get(endpoint, options = {}) { + return this._fetchJSON(endpoint, { + ...options, + method: 'GET', + }) + } + + async post(endpoint, body, options = {}) { + return this._fetchJSON(endpoint, { + ...options, + body: body ? JSON.stringify(body) : undefined, + method: 'POST', + }) + } + + async put(endpoint, body, options = {}) { + return this._fetchJSON(endpoint, { + ...options, + body: body ? JSON.stringify(body) : undefined, + method: 'PUT', + }) + } + + async patch(endpoint, operations, options = {}) { + return this._fetchJSON(endpoint, { + parseResponse: false, + ...options, + body: JSON.stringify(operations), + method: 'PATCH', + }) + } + + async delete(endpoint, options = {}) { + return this._fetchJSON(endpoint, { + parseResponse: false, + ...options, + method: 'DELETE', + }) + } +} diff --git a/datajunction-clients/javascript/src/index.js b/datajunction-clients/javascript/src/index.js new file mode 100644 index 000000000..ed8d124ab --- /dev/null +++ b/datajunction-clients/javascript/src/index.js @@ -0,0 +1,314 @@ +import HttpClient from './httpclient.js' + +export class DJClient extends HttpClient { + constructor( + baseURL, + namespace, + engineName = null, + engineVersion = null, + httpAgent = null, + ) { + super( + { + baseURL, + }, + httpAgent + ) + this.namespace = namespace + this.engineName = engineName + this.engineVersion = engineVersion + } + + get healthcheck() { + return { + get: () => this.get('/health/'), + } + } + + get catalogs() { + return { + list: () => this.get('/catalogs/'), + get: (catalog) => this.get(`/catalogs/${catalog}/`), + create: (catalog) => + this.setHeader('Content-Type', 'application/json').post( + `/catalogs/`, + catalog + ), + addEngine: (catalog, engineName, engineVersion) => + this.setHeader('Content-Type', 'application/json').post( + `/catalogs/${catalog}/engines/`, + [ + { + name: engineName, + version: engineVersion, + }, + ] + ), + } + } + + get engines() { + return { + list: () => this.get('/engines/'), + get: (engineName, engineVersion) => + this.get(`/engines/${engineName}/${engineVersion}/`), + create: (engine) => this.post('/engines/', engine), + } + } + + get addEngineToCatalog() { + return { + set: (catalogName, engine) => + this.setHeader('Content-Type', 'application/json').post( + `/catalogs/${catalogName}/engines/`, + [engine] + ), + } + } + + get namespaces() { + return { + list: () => this.get('/namespaces/'), + nodes: (namespace) => this.get(`/namespaces/${namespace}/`), + create: (namespace) => + this.setHeader('Content-Type', 'application/json').post( + `/namespaces/${namespace}/` + ), + } + } + + get commonDimensions() { + return { + list: (metrics) => { + const metricsQuery = + '?' + metrics.map((m) => `metric=${m}`).join('&') + return this.get('/metrics/common/dimensions/' + metricsQuery) + }, + } + } + + get nodes() { + return { + get: (nodeName) => this.get(`/nodes/${nodeName}/`), + validate: (nodeDetails) => + this.setHeader('Content-Type', 'application/json').post( + '/nodes/validate/', + nodeDetails + ), + update: (nodeName, nodeDetails) => + this.setHeader('Content-Type', 'application/json').patch( + `/nodes/${nodeName}/`, + nodeDetails + ), + revisions: (nodeName) => this.get(`/nodes/${nodeName}/revisions/`), + downstream: (nodeName) => + this.get(`/nodes/${nodeName}/downstream/`), + upstream: (nodeName) => this.get(`/nodes/${nodeName}/upstream/`), + publish: (nodeName) => this.patch(`/nodes/${nodeName}/`, {'mode': 'published'}) + } + } + + get sources() { + return { + create: (sourceDetails) => + this.setHeader('Content-Type', 'application/json').post( + '/nodes/source/', + sourceDetails + ), + list: () => this.get(`/namespaces/${this.namespace}/?type_=source`), + } + } + + get transforms() { + return { + create: (transformDetails) => + this.setHeader('Content-Type', 'application/json').post( + '/nodes/transform/', + transformDetails + ), + list: () => + this.get(`/namespaces/${this.namespace}/?type_=transform`), + } + } + + get dimensions() { + return { + create: (dimensionDetails) => + this.setHeader('Content-Type', 'application/json').post( + '/nodes/dimension/', + dimensionDetails + ), + list: () => + this.get(`/namespaces/${this.namespace}/?type_=dimension`), + link: (nodeName, nodeColumn, dimension, dimensionColumn) => + this.post( + `/nodes/${nodeName}/columns/${nodeColumn}/?dimension=${dimension}&dimension_column=${dimensionColumn}` + ), + } + } + + get metrics() { + return { + get: (metricName) => this.get(`/metrics/${metricName}/`), + create: (metricDetails) => + this.setHeader('Content-Type', 'application/json').post( + '/nodes/metric/', + metricDetails + ), + list: () => this.get(`/namespaces/${this.namespace}/?type_=metric`), + all: () => this.get(`/metrics/`), + } + } + + get cubes() { + return { + get: (cubeName) => this.get(`/cubes/${cubeName}/`), + create: (cubeDetails) => + this.setHeader('Content-Type', 'application/json').post( + '/nodes/cube/', + cubeDetails + ), + } + } + + get tags() { + return { + list: () => this.get('/tags/'), + get: (tagName) => this.get(`/tags/${tagName}/`), + create: (tagData) => + this.setHeader('Content-Type', 'application/json').post( + '/tags/', + tagData + ), + update: (tagName, tagData) => + this.setHeader('Content-Type', 'application/json').patch( + `/tags/${tagName}/`, + tagData + ), + set: (nodeName, tagName) => + this.post(`/nodes/${nodeName}/tag/?tag_name=${tagName}`), + listNodes: (tagName) => this.get(`/tags/${tagName}/nodes/`), + } + } + + get attributes() { + return { + list: () => this.get('/attributes/'), + create: (attributeData) => + this.setHeader('Content-Type', 'application/json').post( + '/attributes/', + attributeData + ), + } + } + + get materializationConfigs() { + return { + update: (nodeName, materializationDetails) => + this.setHeader('Content-Type', 'application/json').post( + `/nodes/${nodeName}/materialization/`, + materializationDetails + ), + } + } + + get columnAttributes() { + return { + set: (nodeName, columnAttribute) => + this.setHeader('Content-Type', 'application/json').post( + `/nodes/${nodeName}/attributes/`, + [columnAttribute] + ), + } + } + + get availabilityState() { + return { + set: (nodeName, availabilityState) => + this.setHeader('Content-Type', 'application/json').post( + `/data/${nodeName}/availability/`, + availabilityState + ), + } + } + + get sql() { + return { + get: ( + metrics, + dimensions, + filters, + engineName = null, + engineVersion = null + ) => { + const metricsQuery = + '?' + metrics.map((m) => `metrics=${m}`).join('&') + const dimensionsQuery = dimensions + .map((d) => `dimensions=${d}`) + .join('&') + const filtersQuery = filters + .map((f) => `filters=${f}`) + .join('&') + const engineNameP = engineName ? `&engine=${engineName}` : '' + const engineVersionP = engineVersion + ? `&engine_version=${engineVersion}` + : '' + return this.get( + `/sql/${metricsQuery}&${dimensionsQuery}${filtersQuery}${engineNameP}${engineVersionP}` + ) + }, + } + } + + get data() { + return { + get: ( + metrics, + dimensions, + filters, + async_ = false, + engineName = null, + engineVersion = null + ) => { + const metricsQuery = + '?' + metrics.map((m) => `metrics=${m}`).join('&') + const dimensionsQuery = dimensions + .map((d) => `dimensions=${d}`) + .join('&') + const filtersQuery = filters + .map((f) => `filters=${f}`) + .join('&') + const asyncP = async_ ? `&async_=${async_}` : '' + const engineNameP = engineName ? `&engine=${engineName}` : '' + const engineVersionP = engineVersion + ? `&engine_version=${engineVersion}` + : '' + const data = this.get( + `/data/${metricsQuery}&${dimensionsQuery}${filtersQuery}${asyncP}${engineNameP}${engineVersionP}` + ).then((data) => { + return { + columns: data.results[0].columns, + data: data.results[0].rows, + } + }) + return data + }, + } + } + + get register() { + return { + table: (catalog, schema, table) => + this.setHeader('Content-Type', 'application/json').post( + `/register/table/${catalog}/${schema}/${table}` + ), + view: (catalog, schema, view, query, replace = false) => { + const replaceQuery = replace ? `?replace=${replace}` : ''; + return this.setHeader('Content-Type', 'application/json').post( + `/register/view/${catalog}/${schema}/${view}${replaceQuery}`, + { query } + ); + } + } + } +} diff --git a/datajunction-clients/javascript/src/index.test.js b/datajunction-clients/javascript/src/index.test.js new file mode 100644 index 000000000..4483430bb --- /dev/null +++ b/datajunction-clients/javascript/src/index.test.js @@ -0,0 +1,80 @@ +const { DJClient } = require('./index') +var dockerNames = require('docker-names') + +test('should return something', async () => { + const dj = new DJClient('http://localhost:8000', 'integration.tests', 'dj', 'dj'); + await dj.login("dj", "dj") + + await dj.catalogs.create({ name: 'tpch' }); + await dj.engines.create({ name: 'trino', version: '451' }); + await dj.catalogs.addEngine('tpch', 'trino', '451'); + + await dj.namespaces.create('integration.tests'); + await dj.namespaces.create('integration.tests.trino'); + + const source = await dj.sources.create({ + name: 'integration.tests.source1', + catalog: 'unknown', + schema_: 'db', + table: 'tbl', + display_name: 'Test Source with Columns', + description: 'A test source node with columns', + columns: [ + { name: 'id', type: 'int' }, + { name: 'name', type: 'string' }, + { name: 'price', type: 'double' }, + { name: 'created_at', type: 'timestamp' }, + ], + primary_key: ['id'], + mode: 'published', + update_if_exists: true, + }); + + await dj.register.table('tpch', 'sf1', 'orders'); + + const transform = await dj.transforms.create({ + name: 'integration.tests.trino.transform1', + display_name: 'Filter to last 1000 records', + description: 'The last 1000 purchases', + mode: 'published', + query: 'select custkey, totalprice, orderdate from source.tpch.sf1.orders order by orderdate desc limit 1000', + update_if_exists: true, + }); + + const dimension = await dj.dimensions.create({ + name: 'integration.tests.trino.dimension1', + display_name: 'Customer keys', + description: 'All custkey values in the source table', + mode: 'published', + primary_key: ['id'], + tags: [], + query: "select custkey as id, 'attribute' as foo from source.tpch.sf1.orders", + update_if_exists: true, + }); + + await dj.dimensions.link( + 'integration.tests.trino.transform1', + 'custkey', + 'integration.tests.trino.dimension1', + 'id', + ); + + const metric = await dj.metrics.create({ + name: 'integration.tests.trino.metric1', + display_name: 'Total of last 1000 purchases', + description: 'This is the total amount from the last 1000 purchases', + mode: 'published', + query: 'select sum(totalprice) from integration.tests.trino.transform1', + update_if_exists: true, + }); + + await dj.commonDimensions.list(['integration.tests.trino.metric1']); + + const query = await dj.sql.get( + ['integration.tests.trino.metric1'], + ['integration.tests.trino.dimension1.id'], + [] + ); + + expect(query.sql).toContain('SELECT'); +}, 60000) diff --git a/datajunction-clients/javascript/webpack.config.js b/datajunction-clients/javascript/webpack.config.js new file mode 100644 index 000000000..ce3b2ef43 --- /dev/null +++ b/datajunction-clients/javascript/webpack.config.js @@ -0,0 +1,15 @@ +const path = require('path') + +module.exports = { + entry: './src/index.js', + mode: 'production', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'datajunction.js', + globalObject: 'this', + library: { + name: 'datajunction', + type: 'umd', + }, + }, +} diff --git a/datajunction-clients/python/.coveragerc b/datajunction-clients/python/.coveragerc new file mode 100644 index 000000000..fc5f1571b --- /dev/null +++ b/datajunction-clients/python/.coveragerc @@ -0,0 +1,28 @@ +# .coveragerc to control coverage.py +[run] +branch = True +source = djclient +# omit = bad_file.py + +[paths] +source = + src/ + */site-packages/ + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: diff --git a/datajunction-clients/python/.gitignore b/datajunction-clients/python/.gitignore new file mode 100644 index 000000000..43c7dc4ea --- /dev/null +++ b/datajunction-clients/python/.gitignore @@ -0,0 +1,55 @@ +# Temporary and binary files +*~ +*.py[cod] +*.so +*.cfg +!.isort.cfg +!setup.cfg +*.orig +*.log +*.pot +__pycache__/* +.cache/* +.*.swp +*/.ipynb_checkpoints/* +.DS_Store + +# Project files +.ropeproject +.project +.pydevproject +.settings +.idea +.vscode +tags + +# Package files +*.egg +*.eggs/ +.installed.cfg +*.egg-info + +# Unittest and coverage +htmlcov/* +.coverage +.coverage.* +.tox +junit*.xml +coverage.xml +.pytest_cache/ + +# Build and docs folder/files +build/* +dist/* +sdist/* +docs/api/* +docs/_rst/* +docs/_build/* +cover/* +MANIFEST + +# Per-project virtualenvs +.venv*/ +.conda*/ +.python-version +.pdm-python diff --git a/datajunction-clients/python/.isort.cfg b/datajunction-clients/python/.isort.cfg new file mode 100644 index 000000000..c8a204d9c --- /dev/null +++ b/datajunction-clients/python/.isort.cfg @@ -0,0 +1,3 @@ +[settings] +profile = black +known_first_party = datajunction diff --git a/datajunction-clients/python/.pre-commit-config.yaml b/datajunction-clients/python/.pre-commit-config.yaml new file mode 100644 index 000000000..e35eee1aa --- /dev/null +++ b/datajunction-clients/python/.pre-commit-config.yaml @@ -0,0 +1,63 @@ +files: ^datajunction-clients/python/ +exclude: ^datajunction-clients/python/dj.project.schema.json + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: check-ast + - id: check-json + - id: check-merge-conflict + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: mixed-line-ending + args: ['--fix=auto'] # replace 'auto' with 'lf' to enforce Linux/Mac line endings or 'crlf' for Windows +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort +- repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black + language_version: python3 +- repo: https://github.com/PyCQA/flake8 + rev: 3.9.2 + hooks: + - id: flake8 +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v0.981' # Use the sha / tag you want to point at + hooks: + - id: mypy + additional_dependencies: + - types-requests + - types-freezegun + - types-python-dateutil + - types-setuptools + - types-PyYAML + - types-tabulate +- repo: https://github.com/asottile/add-trailing-comma + rev: v2.2.1 + hooks: + - id: add-trailing-comma +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort +## Removing this for now due to this bug: https://github.com/hadialqattan/pycln/issues/249 +# - repo: https://github.com/hadialqattan/pycln +# rev: v2.4.0 # Possible releases: https://github.com/hadialqattan/pycln/tags +# hooks: +# - id: pycln +# args: [--config=pyproject.toml] +- repo: local + hooks: + - id: pylint + name: pylint + entry: pylint --disable=duplicate-code,use-implicit-booleaness-not-comparison,wrong-import-order + language: system + types: [python] diff --git a/datajunction-clients/python/LICENSE.txt b/datajunction-clients/python/LICENSE.txt new file mode 100644 index 000000000..c4b4b0a2e --- /dev/null +++ b/datajunction-clients/python/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Beto Dealmeida + +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. diff --git a/datajunction-clients/python/Makefile b/datajunction-clients/python/Makefile new file mode 100644 index 000000000..60a6b0996 --- /dev/null +++ b/datajunction-clients/python/Makefile @@ -0,0 +1,13 @@ +check: + pdm run pre-commit run --all-files + +lint: + make check + +test: + pdm run pytest -n auto --cov=datajunction -vv tests/ --doctest-modules datajunction --without-integration --without-slow-integration ${PYTEST_ARGS} + +dev-release: + hatch version dev + hatch build + hatch publish diff --git a/datajunction-clients/python/README.md b/datajunction-clients/python/README.md new file mode 100644 index 000000000..5f1c89925 --- /dev/null +++ b/datajunction-clients/python/README.md @@ -0,0 +1,448 @@ +# DataJunction Python Client + +This is a short introduction into the Python version of the DataJunction (DJ) client. +For a full comprehensive intro into the DJ functionality please check out [datajunction.io](https://datajunction.io/). + +## Installation + +To install: +``` +pip install datajunction +``` + +## Intro + +We have three top level client classes that help you choose the right path for your DataJunction actions. + +1. `DJClient` for basic read only access to metrics, dimensions, SQL and data. +2. `DJBuilder` for those who would like to modify their DJ data model, build new nodes and/or modify the existing ones. +3. `DJAdmin` for the administrators of the system to define the connections to your data catalog and engines. + +## DJ Client : Basic Access + +Here you can see how to access and use the most common DataJunction features. + +### Examples + +To initialize the client: + +```python +from datajunction import DJClient + +dj = DJClient("http://localhost:8000") +``` + +**NOTE** +If you are running in our demo docker environment please change the above URL to "http://dj:8000". + +You are now connected to your DJ service and you can start looking around. Let's see what namespaces we have in the system: + +```python +dj.list_namespaces() + +['default'] +``` + +Next let's see what metrics and dimensions exist in the `default` namespace: + +```python +dj.list_metrics(namespace="default") + +['default.num_repair_orders', + 'default.avg_repair_price', + 'default.total_repair_cost', + 'default.avg_length_of_employment', + 'default.total_repair_order_discounts', + 'default.avg_repair_order_discounts', + 'default.avg_time_to_dispatch'] + +dj.list_dimensions(namespace="default") + +['default.date_dim', + 'default.repair_order', + 'default.contractor', + 'default.hard_hat', + 'default.local_hard_hats', + 'default.us_state', + 'default.dispatcher', + 'default.municipality_dim'] +``` + +Now let's pick two metrics and see what dimensions they have in common: + +```python +dj.common_dimensions( + metrics=["default.num_repair_orders", "default.total_repair_order_discounts"], + name_only=True +) + +['default.dispatcher.company_name', + 'default.dispatcher.dispatcher_id', + 'default.dispatcher.phone', + 'default.hard_hat.address', + 'default.hard_hat.birth_date', + 'default.hard_hat.city', + ... +``` + +And finally let's ask DJ to show us some data for these metrics and some dimensions: + +```python +dj.data( + metrics=["default.num_repair_orders", "default.total_repair_order_discounts"], + dimensions=["default.hard_hat.city"] +) + +| default_DOT_num_repair_orders | default_DOT_total_repair_order_discounts | city | +| ----------------------------- | ---------------------------------------- | ----------- | +| 4 | 5475.110138 | Jersey City | +| 3 | 11483.300049 | Billerica | +| 5 | 6725.170074 | Southgate | +... +``` + +### Reference + +List of all available DJ client methods: + +- DJClient: + + ### list + - list_namespaces( prefix: Optional[str]) + - list_dimensions( namespace: Optional[str]) + - list_metrics( namespace: Optional[str]) + - list_cubes( namespace: Optional[str]) + - list_sources( namespace: Optional[str]) + - list_transforms( namespace: Optional[str]) + - list_nodes( namespace: Optional[str], type_: Optional[NodeType]) + - list_nodes_with_tags( tag_names: List[str], node_type: Optional[NodeType]) + + - list_catalogs() + - list_engines() + + ### find + - common_dimensions( metrics: List[str], name_only: bool = False) + - common_metrics( dimensions: List[str], name_only: bool = False) + + ### execute + - sql( metrics: List[str], + dimensions: Optional[List[str]], + filters: Optional[List[str]], + engine_name: Optional[str], + engine_version: Optional[str]) + - node_sql( node_name: str, + dimensions: Optional[List[str]], + filters: Optional[List[str]], + engine_name: Optional[str], + engine_version: Optional[str]) + - data( metrics: List[str], + dimensions: Optional[List[str]], + filters: Optional[List[str]], + engine_name: Optional[str], + engine_version: Optional[str], + async_: bool = True) + - node_data( node_name: str, + dimensions: Optional[List[str]], + filters: Optional[List[str]], + engine_name: Optional[str], + engine_version: Optional[str], + async_: bool = True) + +## DJ Builder : Data Modelling + +In this section we'll show you few examples to modify the DJ data model and its nodes. + +### Start Here + +To initialize the DJ builder: + +```python +from datajunction import DJBuilder + +djbuilder = DJBuilder("http://localhost:8000") +``` + +**NOTE** +If you are running in our demo docker container please change the above URL to "http://dj:8000". + +### Namespaces + +To access a namespace or check if it exists you can use the same simple call: + +```python +djbuilder.namespace("default") + +Namespace(dj_client=..., namespace='default') +``` +```python +djbuilder.namespace("foo") + +[DJClientException]: Namespace `foo` does not exist. +``` + +To create a namespace: + +```python +djbuilder.create_namespace("foo") + +Namespace(dj_client=..., namespace='foo') +``` + +To delete (or restore) a namespace: + +```python +djbuilder.delete_namespace("foo") + +djbuilder.restore_namespace("foo") +``` + +**NOTE:** +The `cascade` parameter in both of above methods allows for cascading +effect applied to all underlying nodes and namespaces. Use it with caution! + +### Tags + +You can read existing tags as well as create new ones. +```python +djbuilder.tag(name="deprecated", description="This node has been deprecated.", tag_type="standard", tag_metadata={"contact": "Foo Bar"}) + +Tag(dj_client=..., name='deprecated', description='This node has been deprecated.', tag_type='standard', tag_metadata={"contact": "Foo Bar"}) +``` +```python +djbuilder.tag("official") + +[DJClientException]: Tag `official` does not exist. +``` + +To create a tag: + +```python +djbuilder.create_tag(name="deprecated", description="This node has been deprecated.", tag_type="standard", tag_metadata={"contact": "Foo Bar"}) + +Tag(dj_client=..., name="deprecated", description="This node has been deprecated.", tag_type="standard", tag_metadata={"contact": "Foo Bar"}) +``` + +To add a tag to a node: + +```python +repair_orders = djbuilder.source("default.repair_orders") +repair_orders.tags.append(djbuilder.tag("deprecated")) +repair_orders.save() +``` + +And to list the node names with a specific tag (or set of tags): + +```python +djbuilder.list_nodes_with_tags(tag_names=["deprecated"]) # works with DJClient() as well + +["default.repair_orders"] +``` + + +### Nodes + +To learn what **Node** means in the context of DJ, please check out [this datajuntion.io page](https://datajunction.io/docs/0.1.0/dj-concepts/nodes/). + +To list all (or some) nodes in the system you can use the `list_()` methods described +in the **DJ Client : Basic Access** section or you can use the namespace based method: + +All nodes for a given namespace can be found with: +```python +djbuilder.namespace("default").nodes() +``` + +Specific node types can be retrieved with: +```python +djbuilder.namespace("default").sources() +djbuilder.namespace("default").dimensions() +djbuilder.namespace("default").metrics() +djbuilder.namespace("default").transforms() +djbuilder.namespace("default").cubes() +``` + +To create a source node: + +```python +repair_orders = djbuilder.create_source( + name="repair_orders", + display_name="Repair Orders", + description="Repair orders", + catalog="dj", + schema_="roads", + table="repair_orders", +) +``` + +Nodes can also be created in draft mode: + +```python +repair_orders = djbuilder.create_source( + ..., + mode=NodeMode.DRAFT +) +``` + +To create a dimension node: + +```python +repair_order = djbuilder.create_dimension( + name="default.repair_order_dim", + query=""" + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + dispatcher_id + FROM default.repair_orders + """, + description="Repair order dimension", + primary_key=["repair_order_id"], +) +``` + +To create a transform node: +```python +large_revenue_payments_only = djbuilder.create_transform( + name="default.large_revenue_payments_only", + query=""" + SELECT + payment_id, + payment_amount, + customer_id, + account_type + FROM default.revenue + WHERE payment_amount > 1000000 + """, + description="Only large revenue payments", +) +``` + +To create a metric: +```python +num_repair_orders = djbuilder.create_metric( + name="default.num_repair_orders", + query=""" + SELECT + count(repair_order_id) + FROM repair_orders + """, + description="Number of repair orders", +) +``` + +### Reference + +List of all available DJ builder methods: + +- DJBuilder: + + ### namespaces + - namespace( namespace: str) + - create_namespace( namespace: str) + - delete_namespace(self, namespace: str, cascade: bool = False) + - restore_namespace(self, namespace: str, cascade: bool = False) + + ### nodes + - delete_node(self, node_name: str) + - restore_node(self, node_name: str) + + ### nodes: source + - source(self, node_name: str) + - create_source( ..., mode: Optional[NodeMode] = NodeMode.PUBLISHED) + - register_table( catalog: str, schema: str, table: str) + - register_view( catalog: str, schema: str, view: str, query: str, replace: bool = False) + + ### nodes: transform + - transform(self, node_name: str) + - create_transform( ..., mode: Optional[NodeMode] = NodeMode.PUBLISHED) + + ### nodes: dimension + - dimension(self, node_name: str) + - create_dimension( ..., mode: Optional[NodeMode] = NodeMode.PUBLISHED) + + ### nodes: metric + - metric(self, node_name: str) + - create_metric( ..., mode: Optional[NodeMode] = NodeMode.PUBLISHED) + + ### nodes: cube + - cube(self, node_name: str) + - create_cube( ..., mode: Optional[NodeMode] = NodeMode.PUBLISHED) + + +## DJ System Administration + +In this section we'll describe how to manage your catalog and engines. + +### Start Here + +To initialize the DJ admin: + +```python +from datajunction import DJAdmin + +djadmin = DJAdmin("http://localhost:8000") +``` + +**NOTE** +If you are running in our demo docker container please change the above URL to "http://dj:8000". + +### Examples + +To list available catalogs: + +```python +djadmin.list_catalogs() + +['warehouse'] +``` + +To list available engines: + +```python +djadmin.list_engines() + +[{'name': 'duckdb', 'version': '0.7.1'}] +``` + +To create a catalog: + +```python +djadmin.add_catalog(name="my-new-catalog") +``` + +To create a new engine: + +```python +djadmin.add_engine( + name="Spark", + version="3.2.1", + uri="http:/foo", + dialect="spark" +) +``` + +To linke an engine to a catalog: +```python +djadmin.link_engine_to_catalog( + engine="Spark", version="3.2.1", catalog="my-new-catalog" +) +``` + +### Reference + +List of all available DJ builder methods: + +- DJAdmin: + + ### Catalogs + - list_catalogs() # in DJClient + - get_catalog( name: str) + - add_catalog( name: str) + + ### Engines + - list_engines() # in DJClient + - get_engine( name: str) + - add_engine( name: str,version: str, uri: Optional[str], dialect: Optional[str]) + + ### Together + - link_engine_to_catalog( engine_name: str, engine_version: str, catalog: str) diff --git a/datajunction-clients/python/datajunction/__about__.py b/datajunction-clients/python/datajunction/__about__.py new file mode 100644 index 000000000..2788edf4b --- /dev/null +++ b/datajunction-clients/python/datajunction/__about__.py @@ -0,0 +1,4 @@ +""" +Version for Hatch +""" +__version__ = "0.0.1a87" diff --git a/datajunction-clients/python/datajunction/__init__.py b/datajunction-clients/python/datajunction/__init__.py new file mode 100644 index 000000000..604f62f44 --- /dev/null +++ b/datajunction-clients/python/datajunction/__init__.py @@ -0,0 +1,65 @@ +""" +A DataJunction client for connecting to a DataJunction server +""" +from importlib.metadata import PackageNotFoundError, version # pragma: no cover + +from datajunction.admin import DJAdmin +from datajunction.builder import DJBuilder +from datajunction.client import DJClient +from datajunction.compile import Project +from datajunction.models import ( + AvailabilityState, + ColumnAttribute, + Engine, + Materialization, + MaterializationJobType, + MaterializationStrategy, + MetricDirection, + MetricMetadata, + MetricUnit, + NodeMode, +) +from datajunction.nodes import ( + Cube, + Dimension, + Metric, + Namespace, + Node, + Source, + Transform, +) +from datajunction.tags import Tag + +try: + # Change here if project is renamed and does not equal the package name + DIST_NAME = __name__ + __version__ = version(DIST_NAME) +except PackageNotFoundError: # pragma: no cover + __version__ = "unknown" +finally: + del version, PackageNotFoundError + + +__all__ = [ + "DJClient", + "DJBuilder", + "DJAdmin", + "AvailabilityState", + "ColumnAttribute", + "Source", + "Dimension", + "Transform", + "Materialization", + "MaterializationJobType", + "MaterializationStrategy", + "Metric", + "MetricDirection", + "MetricUnit", + "Cube", + "Node", + "NodeMode", + "Namespace", + "Engine", + "Project", + "Tag", +] diff --git a/datajunction-clients/python/datajunction/_base.py b/datajunction-clients/python/datajunction/_base.py new file mode 100644 index 000000000..749b4ee65 --- /dev/null +++ b/datajunction-clients/python/datajunction/_base.py @@ -0,0 +1,120 @@ +"""Base client dataclasses.""" +from dataclasses import MISSING, fields, is_dataclass +from types import UnionType +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Optional, + Type, + TypeVar, + Union, + get_args, + get_origin, +) + +if TYPE_CHECKING: # pragma: no cover + from datajunction.client import DJClient + + +T = TypeVar("T") + + +class SerializableMixin: # pylint: disable=too-few-public-methods + """ + Mixin for serializing dictionaries to dataclasses + """ + + @staticmethod + def _serialize_nested( + field_type: Type, + field_value: Any, + dj_client: Optional["DJClient"], + ): + """ + Handle nested field serialization + """ + if is_dataclass(field_type) and isinstance(field_value, dict): + return field_type.from_dict(dj_client, field_value) + return field_value + + @staticmethod + def _serialize_list( + field_type: Type, + field_value: Any, + dj_client: Optional["DJClient"], + ): + """ + Handle serialization of lists of both primitive and dataclass object types + """ + if not isinstance(field_value, list): + return field_value # Not a list, return as-is + + list_inner_type = get_args(field_type)[0] + type_candidates = ( + list(get_args(list_inner_type)) + if get_origin(list_inner_type) in (Union, UnionType) + else [list_inner_type] + ) + + def serialize_item(item): + for candidate in type_candidates: + try: + return ( + candidate.from_dict(dj_client, item) + if isinstance(item, dict) + else item + ) + except TypeError: # Ignore and try the next candidate + pass + return item # pragma: no cover + + return [serialize_item(item) for item in field_value] + + @classmethod + def from_dict( + cls: Type[T], + dj_client: Optional["DJClient"], + data: Dict[str, Any], + ) -> T: + """ + Create an instance of the given dataclass `cls` from a dictionary `data`. + This will handle nested dataclasses and optional types. + """ + if not is_dataclass(cls): + return cls(**data) + + field_values = {} + for field in fields(cls): + if field.name == "dj_client": + continue + + # Resolve optional types to their inner type + field_type = field.type + origin = get_origin(field_type) + if origin in (Union, UnionType): + field_type = next( # pragma: no cover + typ for typ in get_args(field_type) if typ is not type(None) # noqa + ) + + # Serialize field value + field_value = data.get(field.name) + serialization_func = ( + SerializableMixin._serialize_list + if get_origin(field_type) is list + else SerializableMixin._serialize_nested + ) + field_values[field.name] = serialization_func( + field_type, + field_value, + dj_client, + ) + + # Apply default if necessary + if ( + field.name not in field_values or field_values[field.name] is None + ) and field.default is not MISSING: + field_values[field.name] = field.default + if is_dataclass(cls) and "dj_client" in cls.__dataclass_fields__.keys(): # type: ignore + return cls(dj_client=dj_client, **field_values) # type: ignore + return cls(**field_values) diff --git a/datajunction-clients/python/datajunction/_internal.py b/datajunction-clients/python/datajunction/_internal.py new file mode 100644 index 000000000..a43fabedb --- /dev/null +++ b/datajunction-clients/python/datajunction/_internal.py @@ -0,0 +1,672 @@ +"""DataJunction base client setup.""" + +# pylint: disable=redefined-outer-name, import-outside-toplevel, too-many-lines +import logging +import os +import platform +import warnings +from dataclasses import asdict, dataclass +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, TypedDict, Union +from urllib.parse import urljoin + +try: + import pandas as pd +except ImportError: # pragma: no cover + warnings.warn( + ( + "Optional dependency `pandas` not found, data retrieval" + "disabled. You can install pandas by running `pip install pandas`." + ), + ImportWarning, + ) +import requests +from requests.adapters import CaseInsensitiveDict, HTTPAdapter + +from datajunction import models +from datajunction._base import SerializableMixin +from datajunction.exceptions import ( + DJClientException, + DJTagAlreadyExists, + DJTagDoesNotExist, +) + +if TYPE_CHECKING: # pragma: no cover + from datajunction.admin import DJAdmin + from datajunction.builder import DJBuilder + from datajunction.nodes import Node + from datajunction.tags import Tag + +DEFAULT_NAMESPACE = "default" +_logger = logging.getLogger(__name__) + + +# +# Helpers +# +def from_jupyter() -> bool: # pragma: no cover + """ + Checks whether we're running from an IPython interactive console + """ + try: + from IPython import get_ipython + except ImportError: + return False + return get_ipython() is not None + + +class Results(TypedDict): + """ + Results in a completed DJ Query + """ + + columns: Tuple[str] + data: Tuple[Tuple] + + +class RequestsSessionWithEndpoint(requests.Session): # pragma: no cover + """ + Creates a requests session that comes with an endpoint that all + subsequent requests will use as a prefix. + """ + + def __init__(self, endpoint: str = None, show_traceback: bool = False): + super().__init__() + self.endpoint = endpoint + self.mount("http://", HTTPAdapter()) + self.mount("https://", HTTPAdapter()) + + self.headers = CaseInsensitiveDict( + { + "User-Agent": ( + f"datajunction;;N/A;" + f"{platform.processor() or platform.machine()};" + f"{platform.system()};" + f"{platform.release()} {platform.version()}" + ), + }, + ) + + self._show_traceback = show_traceback + + if from_jupyter() and not self._show_traceback: + from IPython import get_ipython # pylint: disable=import-error + + def shortened_error(*args, **kwargs): # pylint: disable=unused-argument + import sys + + etype, value, _ = sys.exc_info() + _logger.error("[%s]: %s", etype.__name__, value) + + get_ipython().showtraceback = shortened_error + + def request(self, method, url, *args, **kwargs): + """ + Make the request with the full URL. + """ + url = self.construct_url(url) + try: + response = super().request(method, url, *args, **kwargs) + response.raise_for_status() + return response + except requests.exceptions.RequestException as exc: + if exc.response.headers.get("Content-Type") == "application/json": + error_message = exc.response.json() + else: + error_message = f"Request failed {exc.response.status_code}: {str(exc)}" + raise DJClientException(error_message) from exc + + def prepare_request(self, request, *args, **kwargs): + """ + Prepare the request with the full URL. + """ + request.url = self.construct_url(request.url) + return super().prepare_request( + request, + *args, + **kwargs, + ) + + def construct_url(self, url): + """ + Construct full URL based off the endpoint. + """ + return urljoin(self.endpoint, url) + + +# +# Main DJClient (internal) +# +class DJClient: + """ + Internal client class with non-user facing methods. + """ + + def __init__( # pylint: disable=too-many-arguments + self, + uri: str = "http://localhost:8000", + engine_name: str = None, + engine_version: str = None, + requests_session: RequestsSessionWithEndpoint = None, + target_namespace: str = DEFAULT_NAMESPACE, + timeout: int = 2 * 60, + debug: bool = False, + ): + self.target_namespace = target_namespace + self.uri = uri + self.engine_name = engine_name + self.engine_version = engine_version + self._debug = debug + + if not requests_session: # pragma: no cover + self._session = RequestsSessionWithEndpoint( + endpoint=self.uri, + show_traceback=self._debug, + ) + else: # pragma: no cover + self._session = requests_session + self._timeout = timeout + + # + # Authentication + # + def create_user(self, email: str, username: str, password: str): + """ + Create basic user. + """ + response = self._session.post( + "/basic/user/", + data={"email": email, "username": username, "password": password}, + ) + return response.json() + + def basic_login( + self, + username: Optional[str] = None, + password: Optional[str] = None, + ): + """ + Login with basic authentication. + """ + response = self._session.post( + "/basic/login/", + data={ + "username": username or os.getenv("DJ_USER"), + "password": password or os.getenv("DJ_PWD"), + }, + ) + return response + + @staticmethod + def _primary_key_from_columns(columns) -> List[str]: + """ + Extracts the primary key from the columns + """ + return [ + column["name"] + for column in columns + if any( + attr["attribute_type"]["name"] == "primary_key" + for attr in column["attributes"] + if attr + ) + ] + + @staticmethod + def process_results(results) -> "pd.DataFrame": + """ + Return a pandas dataframe of the results if pandas is installed + """ + if "results" in results and results["results"]: + columns = results["results"][0]["columns"] + rows = results["results"][0]["rows"] + try: + return pd.DataFrame( + rows, + columns=[col["name"] for col in columns], + ) + except NameError: # pragma: no cover + return Results( + data=rows, + columns=tuple(col["name"] for col in columns), # type: ignore + ) + raise DJClientException("No data for query!") + + # + # Node methods + # + def _get_nodes_in_namespace( + self, + namespace: str, + type_: Optional[models.NodeType] = None, + ): + """ + Retrieves all nodes in given namespace. + """ + response = self._session.get( + f"/namespaces/{namespace}/" + (f"?type_={type_.value}" if type_ else ""), + ) + node_details_list = response.json() + nodes = [n["name"] for n in node_details_list] + return nodes + + def _get_all_nodes( + self, + type_: Optional[models.NodeType] = None, + ): + """ + Retrieve all nodes of a given type. + """ + response = self._session.get( + "/nodes/" + (f"?node_type={type_.value}" if type_ else ""), + ) + return response.json() + + def _verify_node_exists( + self, + node_name: str, + type_: Optional[str] = None, + ) -> Dict[str, Any]: + """ + Retrieves a node and verifies that it exists and has the expected node type. + """ + node = self._get_node(node_name) + if "name" not in node: + raise DJClientException(f"No node with name {node_name} exists!") + if type_ and "name" in node and node["type"] != type_: + raise DJClientException( + f"A node with name {node_name} exists, but it is not a {type_} node!", + ) + return node + + def _validate_node(self, node: "Node"): + """ + Check if a locally defined node is valid. + """ + node_copy = node.to_dict() + node_copy["mode"] = models.NodeMode.PUBLISHED + response = self._session.post( + "/nodes/validate/", + json=node_copy, + timeout=self._timeout, + ) + return response.json() + + def _create_node( + self, + node: "Node", + mode: Optional[models.NodeMode] = models.NodeMode.PUBLISHED, + ): + """ + Helper function to create a node. + Raises an error if node already exists and is active. + """ + node.mode = mode + response = self._session.post( + f"/nodes/{node.type}/", + timeout=self._timeout, + json=node.to_dict(exclude=["type"]), + ) + return response + + def _update_node( + self, + node_name: str, + update_input: models.UpdateNode, + ) -> requests.Response: + """ + Call node update API with attributes to update. + """ + return self._session.patch(f"/nodes/{node_name}/", json=asdict(update_input)) + + def _publish_node(self, node_name: str, update_input: models.UpdateNode): + """ + Retrieves a node. + """ + response = self._session.patch( + f"/nodes/{node_name}/", + json=asdict(update_input), + ) + return response.json() + + def _get_node(self, node_name: str): + """ + Retrieves a node. + """ + response = self._session.get(f"/nodes/{node_name}/") + return response.json() + + def _get_node_upstreams(self, node_name: str): + """ + Retrieves a node's upstreams + """ + response = self._session.get(f"/nodes/{node_name}/upstream") + return response.json() + + def _get_node_downstreams(self, node_name: str): + """ + Retrieves a node's downstreams + """ + response = self._session.get(f"/nodes/{node_name}/downstream") + return response.json() + + def _get_node_dimensions(self, node_name: str): + """ + Retrieves a node's dimensions + """ + response = self._session.get(f"/nodes/{node_name}/dimensions") + return response.json() + + def _get_cube(self, node_name: str): + """ + Retrieves a Cube node. + """ + response = self._session.get(f"/cubes/{node_name}/") + return response.json() + + def get_metric(self, node_name: str): + """ + Helper function to retrieve metadata for the given metric node. + """ + response = self._session.get(f"/metrics/{node_name}/") + return response.json() + + def _get_node_revisions(self, node_name: str): + """ + Retrieve all revisions of the node + """ + response = self._session.get(f"/nodes/{node_name}/revisions") + return response.json() + + def _link_dimension_to_node( + self, + node_name: str, + column_name: str, + dimension_name: str, + dimension_column: Optional[str], + ): + """ + Helper function to link a dimension to the node. + """ + params = {"dimension": dimension_name} + if dimension_column: + params["dimension_column"] = dimension_column + response = self._session.post( + f"/nodes/{node_name}/columns/{column_name}/", + timeout=self._timeout, + params=params, + ) + return response.json() + + def _add_reference_dimension_link( # pylint: disable=too-many-arguments + self, + node_name: str, + node_column: str, + dimension_node: str, + dimension_column: str, + role: Optional[str] = None, + ): + """ + Helper function to link a dimension to the node. + """ + params = { + "dimension_node": dimension_node, + "dimension_column": dimension_column, + **({"role": role} if role else {}), + } + response = self._session.post( + f"/nodes/{node_name}/columns/{node_column}/link", + timeout=self._timeout, + params=params, + ) + return response.json() + + def _link_complex_dimension_to_node( # pylint: disable=too-many-arguments + self, + node_name: str, + dimension_node: str, + join_type: Optional[str] = None, + *, + join_on: str, + join_cardinality: Optional[str] = None, + role: Optional[str] = None, + ): + """ + Helper function to link a complex dimension to the node. + """ + params = { + "dimension_node": dimension_node, + "join_type": join_type or "LEFT", + "join_on": join_on, + "join_cardinality": join_cardinality or "one_to_many", + "role": role, + } + response = self._session.post( + f"/nodes/{node_name}/link/", + timeout=self._timeout, + json=params, + ) + return response.json() + + def _unlink_dimension_from_node( + self, + node_name: str, + column_name: str, + dimension_name: str, + dimension_column: Optional[str] = None, + ): + """ + Helper function to un-link a dimension to the node. + """ + response = self._session.delete( + f"/nodes/{node_name}/columns/{column_name}/" + f"?dimension={dimension_name}&dimension_column={dimension_column}", + timeout=self._timeout, + ) + return response.json() + + def _remove_complex_dimension_link( + self, + node_name: str, + dimension_node: str, + role: Optional[str] = None, + ): + """ + Helper function to remove a complex dimension link. + """ + response = self._session.request( + "DELETE", + f"/nodes/{node_name}/link/", + timeout=self._timeout, + json={ + "dimension_node": dimension_node, + "role": role, + }, + ) + return response.json() + + def _remove_reference_dimension_link( + self, + node_name: str, + node_column: str, + ): + """ + Helper function to remove a reference dimension link from a node column. + """ + response = self._session.delete( + f"/nodes/{node_name}/columns/{node_column}/link", + timeout=self._timeout, + ) + return response.json() + + def _upsert_materialization( + self, + node_name: str, + config: models.Materialization, + ): + """ + Upserts a materialization config for the node. + """ + response = self._session.post( + f"/nodes/{node_name}/materialization/", + json=config.to_dict(), + ) + return response.json() + + def _deactivate_materialization( + self, + node_name: str, + materialization_name: str, + ): + """ + Upserts a materialization config for the node. + """ + response = self._session.delete( + f"/nodes/{node_name}/materializations/", + params={ + "materialization_name": materialization_name, + }, + ) + return response.json() + + def _add_availability_state( + self, + node_name: str, + availability: models.AvailabilityState, + ): + """ + Adds an availability state for the node + """ + response = self._session.post( + f"/data/{node_name}/availability/", + json=asdict(availability), + ) + return response.json() + + def _set_column_attributes( + self, + node_name, + column_name, + attributes: List[models.ColumnAttribute], + ): + """ + Sets attributes for columns on the node + """ + response = self._session.post( + f"/nodes/{node_name}/columns/{column_name}/attributes/", + json=[asdict(attribute) for attribute in attributes], + ) + return response.json() + + def _set_column_display_name( + self, + node_name, + column_name, + display_name: str, + ): + """ + Sets display name for the column on the node + """ + response = self._session.patch( + f"/nodes/{node_name}/columns/{column_name}/", + params={"display_name": display_name}, + ) + return response.json() + + def _find_nodes_with_dimension( + self, + node_name, + ): + """ + Find all nodes with this dimension + """ + response = self._session.get(f"/dimensions/{node_name}/nodes/") + return response.json() + + def _refresh_source_node( + self, + node_name, + ): + """ + Find all nodes with this dimension + """ + response = self._session.post(f"/nodes/{node_name}/refresh/") + return response.json() + + def _export_namespace(self, namespace): + """ + Export an array of definitions contained within a namespace + """ + response = self._session.get(f"/namespaces/{namespace}/export/") + return response.json() + + # + # Methods for Tags + # + def _update_tag(self, tag_name: str, update_input: models.UpdateTag): + """ + Call tag update API with attributes to update. + """ + return self._session.patch(f"/tags/{tag_name}/", json=asdict(update_input)) + + def _update_node_tags(self, node_name: str, tags: Optional[List[str]]): + """ + Update tags on a node + """ + return self._session.post( + f"/nodes/{node_name}/tags/", + params={"tag_names": tags} if tags else None, + ) + + def _get_tag(self, tag_name: str): + """ + Retrieves a tag. + """ + try: + response = self._session.get(f"/tags/{tag_name}/") + return response.json() + except DJClientException as exc: # pragma: no cover + return exc.__dict__ + + def _create_tag( + self, + tag: "Tag", + ): + """ + Helper function to create a tag. + Raises an error if tag already exists. + """ + existing_tag = self._get_tag(tag_name=tag.name) + if "name" in existing_tag: + raise DJTagAlreadyExists(tag_name=tag.name) + response = self._session.post( + "/tags/", + timeout=self._timeout, + json=tag.to_dict(), + ) + return response + + def _list_nodes_with_tag( + self, + tag_name: str, + node_type: Optional[models.NodeType] = None, + ): + """ + Retrieves all nodes with a given tag. + """ + response = self._session.get( + f"/tags/{tag_name}/nodes" + + (f"?node_type={node_type.value}" if node_type else ""), + ) + if response.status_code == 404: + raise DJTagDoesNotExist(tag_name) + return [n["name"] for n in response.json()] + + +@dataclass +class ClientEntity(SerializableMixin): + """ + Any entity that uses the DJ client. + """ + + dj_client: Union[DJClient, "DJBuilder", "DJAdmin"] + exclude = ["dj_client"] diff --git a/datajunction-clients/python/datajunction/admin.py b/datajunction-clients/python/datajunction/admin.py new file mode 100644 index 000000000..13b9cf805 --- /dev/null +++ b/datajunction-clients/python/datajunction/admin.py @@ -0,0 +1,102 @@ +"""DataJunction admin client module.""" + +from typing import Optional + +from datajunction.builder import DJBuilder +from datajunction.exceptions import DJClientException + + +class DJAdmin(DJBuilder): # pylint: disable=too-many-public-methods + """ + Client class for DJ system administration. + """ + + # + # Data Catalogs + # + def get_catalog(self, name: str) -> dict: + """ + Get catalog by name. + """ + response = self._session.get(f"/catalogs/{name}/", timeout=self._timeout) + return response.json() + + def add_catalog(self, name: str) -> None: + """ + Add a catalog. + """ + response = self._session.post( + "/catalogs/", + json={"name": f"{name}"}, + timeout=self._timeout, + ) + json_response = response.json() + if not response.status_code < 400: + raise DJClientException( + f"Adding catalog `{name}` failed: {json_response}", + ) # pragma: no cover + + # + # Database Engines + # + def get_engine(self, name: str, version: str) -> dict: + """ + Get engine by name. + """ + response = self._session.get( + f"/engines/{name}/{version}", + timeout=self._timeout, + ) + return response.json() + + def add_engine( + self, + name: str, + version: str, + uri: Optional[str], + dialect: Optional[str], + ) -> None: + """ + Add an engine. + """ + response = self._session.post( + "/engines/", + json={ + "name": f"{name}", + "version": f"{version}", + "uri": f"{uri}", + "dialect": f"{dialect}", + }, + timeout=self._timeout, + ) + json_response = response.json() + if not response.status_code < 400: + raise DJClientException( + f"Adding engine failed: {json_response}", + ) # pragma: no cover + + def link_engine_to_catalog( + self, + engine: str, + version: str, + catalog: str, + ) -> None: + """ + Add/link a particular engine to a particular catalog. + """ + response = self._session.post( + f"/catalogs/{catalog}/engines/", + json=[ + { + "name": f"{engine}", + "version": f"{version}", + }, + ], + timeout=self._timeout, + ) + json_response = response.json() + if not response.status_code < 400: + raise DJClientException( + f"Linking engine (name: {engine}, version: {version}) " + f"to catalog `{catalog}` failed: {json_response}", + ) # pragma: no cover diff --git a/datajunction-clients/python/datajunction/builder.py b/datajunction-clients/python/datajunction/builder.py new file mode 100644 index 000000000..ae9ac92f2 --- /dev/null +++ b/datajunction-clients/python/datajunction/builder.py @@ -0,0 +1,519 @@ +"""DataJunction builder client module.""" +# pylint: disable=protected-access +import re +from dataclasses import fields +from http import HTTPStatus +from typing import TYPE_CHECKING, Dict, List, Optional + +from datajunction import models +from datajunction.client import DJClient +from datajunction.exceptions import ( + DJClientException, + DJNamespaceAlreadyExists, + DJTableAlreadyRegistered, + DJTagAlreadyExists, + DJViewAlreadyRegistered, +) +from datajunction.nodes import ( + Cube, + Dimension, + Metric, + Namespace, + Node, + Source, + Transform, +) +from datajunction.tags import Tag + +if TYPE_CHECKING: + from datajunction.compile import ColumnYAML # pragma: no cover + + +class DJBuilder(DJClient): # pylint: disable=too-many-public-methods + """ + Client class for DJ dag and node modifications. + """ + + # + # Namespace + # + def namespace(self, namespace: str) -> "Namespace": + """ + Returns the specified node namespace. + """ + namespaces = self.list_namespaces(prefix=namespace) + if namespace not in namespaces: + raise DJClientException(f"Namespace `{namespace}` does not exist.") + return Namespace(namespace=namespace, dj_client=self) + + def create_namespace( + self, + namespace: str, + skip_if_exists: bool = False, + ) -> "Namespace": + """ + Create a namespace with a given name. + """ + response = self._session.post( + f"/namespaces/{namespace}/", + timeout=self._timeout, + ) + json_response = response.json() + if response.status_code == 409 and not skip_if_exists: + raise DJNamespaceAlreadyExists(json_response["message"]) + return Namespace(namespace=namespace, dj_client=self) + + def delete_namespace(self, namespace: str, cascade: bool = False) -> None: + """ + Delete a namespace by name. + """ + response = self._session.request( + "DELETE", + f"/namespaces/{namespace}/", + timeout=self._timeout, + params={ + "cascade": cascade, + }, + ) + if not response.status_code < 400: + raise DJClientException(response.json()["message"]) + + def restore_namespace(self, namespace: str, cascade: bool = False) -> None: + """ + Restore a namespace by name. + """ + response = self._session.post( + f"/namespaces/{namespace}/restore/", + timeout=self._timeout, + params={ + "cascade": cascade, + }, + ) + if response.status_code != HTTPStatus.CREATED: + raise DJClientException(response.json()["message"]) + + # + # Nodes: all + # + def make_node_of_type( + self, + type_: models.NodeType, + data: Dict, + ): + """ + Make a new node of the given type. + """ + # common arguments + common_args = [ + field.name + for field in fields(Node) + if field.name not in ["dj_client", "type"] + ] + + def type_to_class(type_: str): + if type_ == models.NodeType.SOURCE: + return Source + if type_ == models.NodeType.METRIC: + return Metric + if type_ == models.NodeType.DIMENSION: + return Dimension + if type_ == models.NodeType.TRANSFORM: + return Transform + if type_ == models.NodeType.CUBE: + return Cube + raise DJClientException(f"Unknown node type: {type_}") # pragma: no cover + + class_ = type_to_class(type_) + + args = common_args + [ + field.name for field in fields(class_) if field.name not in common_args + ] + data_ = {k: v for k, v in data.items() if k in args} + return class_(dj_client=self, **data_) + + def create_node( + self, + type_: models.NodeType, + name: str, + data: Dict, + update_if_exists: bool = False, + ): + """ + Create or update a new node + """ + + data = { + k: v + for k, v in data.items() + if k not in ["dj_client", "type"] and v is not None + } + + try: + existing_node_dict = self._get_node(name) + if type_ == models.NodeType.CUBE.value: + cube_dict = self._get_cube(name) + # This check is for the unit tests, which don't raise an exception + # for >= 400 status codes + if "name" in cube_dict: + existing_node_dict["metrics"] = cube_dict["cube_node_metrics"] + existing_node_dict["dimensions"] = cube_dict["cube_node_dimensions"] + except DJClientException as e: # pragma: no cover # pytest fixture doesn't raise + if re.search(r"node .* does not exist", str(e)): + existing_node_dict = None + else: + raise + + # Checking for "name" in existing_node_dict is a workaround + # to accommodate pytest mock client, which return a error message dict (no "name") + # instead of raising like the real client. + if existing_node_dict and "name" in existing_node_dict: + # update + if update_if_exists: + existing_node_dict.update(data) + new_node = self.make_node_of_type( + type_=type_, + data=existing_node_dict, + ) + new_node._update_tags() + new_node._update() + else: + raise DJClientException( + f"Node `{name}` already exists. " + f"Use `update_if_exists=True` to update the node.", + ) + else: + # create + new_node = self.make_node_of_type(type_=type_, data=data) + response = self._create_node(node=new_node, mode=data.get("mode")) + if not response.status_code < 400: + raise DJClientException( + f"Creating node `{name}` failed: {response.json()}", + ) # pragma: no cover + new_node._update_tags() + new_node.refresh() + return new_node + + def delete_node(self, node_name: str) -> None: + """ + Delete (aka deactivate) this node. + """ + response = self._session.delete( + f"/nodes/{node_name}/", + timeout=self._timeout, + ) + json_response = response.json() + if not response.status_code < 400: + raise DJClientException( + f"Deleting node `{node_name}` failed: {json_response}", + ) # pragma: no cover + + def restore_node(self, node_name: str) -> None: + """ + Restore (aka reactivate) this node. + """ + response = self._session.post( + f"/nodes/{node_name}/restore/", + timeout=self._timeout, + ) + json_response = response.json() + if not response.status_code < 400: + raise DJClientException( + f"Restoring node `{node_name}` failed: {json_response}", + ) # pragma: no cover + + # + # Nodes: SOURCE + # + + def create_source( # pylint: disable=too-many-arguments + self, + name: str, + catalog: Optional[str] = None, + schema: Optional[str] = None, + table: Optional[str] = None, + display_name: Optional[str] = None, + description: Optional[str] = None, + columns: Optional[List["ColumnYAML"]] = None, + primary_key: Optional[List[str]] = None, + tags: Optional[List[str]] = None, + mode: Optional[models.NodeMode] = models.NodeMode.PUBLISHED, + update_if_exists: bool = False, + ) -> "Source": + """ + Creates a new Source node with given parameters. + """ + + return self.create_node( + type_=models.NodeType.SOURCE, + name=name, + data={ + "name": name, + "catalog": catalog, + "schema_": schema, + "table": table, + "display_name": display_name, + "description": description, + "columns": [ + {"name": column.name, "type": column.type} for column in columns + ] + if columns + else [], + "primary_key": primary_key, + "tags": tags, + "mode": mode, + }, + update_if_exists=update_if_exists, + ) + + def register_table(self, catalog: str, schema: str, table: str) -> Source: + """ + Register a table as a source node. This will create a source node under the configured + `source_node_namespace` (a server-side setting), which defaults to the `source` namespace. + """ + try: + response = self._session.post( + f"/register/table/{catalog}/{schema}/{table}/", + ) + except Exception as exc: + if "409 Client Error" in str(exc): + raise DJTableAlreadyRegistered(catalog, schema, table) from exc + raise DJClientException( + f"Failed to register table `{catalog}.{schema}.{table}`: {exc}", + ) from exc + data = response.json() + source_node = Source( + dj_client=self, + name=data["name"], + catalog=data["catalog"], + schema_=data["schema_"], + table=data["table"], + columns=data["columns"], + description=data["description"], + mode=data["mode"], + status=data["status"], + display_name=data["display_name"], + availability=data["availability"], + tags=data["tags"], + materializations=data["materializations"], + version=data["version"], + current_version=data["current_version"], + ) + return source_node + + def register_view( # pylint: disable=too-many-arguments + self, + catalog: str, + schema: str, + view: str, + query: str, + replace: bool = False, + ) -> Source: + """ + Register a table as a source node. This will create a source node under the configured + `source_node_namespace` (a server-side setting), which defaults to the `source` namespace. + """ + try: + response = self._session.post( + f"/register/view/{catalog}/{schema}/{view}/", + params={"query": query, "replace": str(replace)}, + ) + except Exception as exc: + if "409 Client Error" in str(exc): + raise DJViewAlreadyRegistered(catalog, schema, view) from exc + raise DJClientException( + f"Failed to register view `{catalog}.{schema}.{view}`: {exc}", + ) from exc + data = response.json() + source_node = Source( + dj_client=self, + name=data["name"], + catalog=data["catalog"], + schema_=data["schema_"], + table=data["table"], + columns=data["columns"], + description=data["description"], + mode=data["mode"], + status=data["status"], + display_name=data["display_name"], + availability=data["availability"], + tags=data["tags"], + materializations=data["materializations"], + version=data["version"], + current_version=data["current_version"], + ) + return source_node + + # + # Nodes: TRANSFORM + # + def create_transform( # pylint: disable=too-many-arguments + self, + name: str, + query: Optional[str] = None, + description: Optional[str] = None, + display_name: Optional[str] = None, + primary_key: Optional[List[str]] = None, + tags: Optional[List[str]] = None, + mode: Optional[models.NodeMode] = models.NodeMode.PUBLISHED, + custom_metadata: Optional[Dict] = None, + update_if_exists: bool = False, + ) -> "Transform": + """ + Creates or update a Transform node with given parameters. + """ + return self.create_node( + type_=models.NodeType.TRANSFORM, + name=name, + data={ + "name": name, + "query": query, + "description": description, + "display_name": display_name, + "primary_key": primary_key, + "tags": tags, + "mode": mode, + "custom_metadata": custom_metadata, + }, + update_if_exists=update_if_exists, + ) + + # + # Nodes: DIMENSION + # + def create_dimension( # pylint: disable=too-many-arguments + self, + name: str, + query: Optional[str] = None, + primary_key: Optional[List[str]] = None, + description: Optional[str] = None, + display_name: Optional[str] = None, + tags: Optional[List[str]] = None, + mode: Optional[models.NodeMode] = models.NodeMode.PUBLISHED, + update_if_exists: bool = False, + ) -> "Dimension": + """ + Creates or update a Dimension node with given parameters. + """ + return self.create_node( + type_=models.NodeType.DIMENSION, + name=name, + data={ + "name": name, + "query": query, + "description": description, + "display_name": display_name, + "primary_key": primary_key, + "tags": tags, + "mode": mode, + }, + update_if_exists=update_if_exists, + ) + + # + # Nodes: METRIC + # + def create_metric( # pylint: disable=too-many-arguments + self, + name: str, + query: Optional[str] = None, + description: Optional[str] = None, + display_name: Optional[str] = None, + required_dimensions: Optional[List[str]] = None, + direction: Optional[models.MetricDirection] = None, + unit: Optional[models.MetricUnit] = None, + tags: Optional[List[str]] = None, + mode: Optional[models.NodeMode] = models.NodeMode.PUBLISHED, + update_if_exists: bool = False, + ) -> "Metric": + """ + Creates or update a Metric node with given parameters. + """ + return self.create_node( + type_=models.NodeType.METRIC, + name=name, + data={ + "name": name, + "query": query, + "description": description, + "display_name": display_name, + "required_dimensions": required_dimensions, + **( + { + "metric_metadata": models.MetricMetadata( + direction=direction, + unit=unit, + ), + } + if direction or unit + else {} + ), + "tags": tags, + "mode": mode, + }, + update_if_exists=update_if_exists, + ) + + # + # Nodes: CUBE + # + def create_cube( # pylint: disable=too-many-arguments + self, + name: str, + metrics: Optional[List[str]] = None, + dimensions: Optional[List[str]] = None, + filters: Optional[List[str]] = None, + description: Optional[str] = None, + display_name: Optional[str] = None, + mode: Optional[models.NodeMode] = models.NodeMode.PUBLISHED, + tags: Optional[List[str]] = None, + update_if_exists: bool = False, + ) -> "Cube": + """ + Create or update a cube with the given parameters. + """ + return self.create_node( + type_=models.NodeType.CUBE, + name=name, + data={ + "name": name, + "metrics": metrics, + "dimensions": dimensions, + "filters": filters or [], + "description": description, + "display_name": display_name, + "mode": mode, + "tags": tags, + }, + update_if_exists=update_if_exists, + ) + + # + # Tag + # + def create_tag( # pylint: disable=too-many-arguments + self, + name: str, + description: Optional[str], + tag_metadata: Dict, + tag_type: str, + update_if_exists: bool = False, + ) -> Tag: + """ + Create a tag with a given name. + """ + new_tag = Tag( + dj_client=self, + name=name, + description=description, + tag_type=tag_type, + tag_metadata=tag_metadata, + ) + try: + self._create_tag(tag=new_tag) + except DJTagAlreadyExists as exc: + if update_if_exists: + new_tag._update() + else: + raise exc + new_tag.refresh() + return new_tag diff --git a/datajunction-clients/python/datajunction/cli.py b/datajunction-clients/python/datajunction/cli.py new file mode 100644 index 000000000..62009ec0f --- /dev/null +++ b/datajunction-clients/python/datajunction/cli.py @@ -0,0 +1,102 @@ +"""DataJunction command-line tool""" + +import argparse + +from datajunction import DJBuilder, Project + + +class DJCLI: + """DJ command-line tool""" + + def __init__(self, builder_client: DJBuilder | None = None): + """ + Initialize the CLI with a builder client. + """ + self.builder_client = builder_client or DJBuilder() + + def deploy(self, directory: str, dryrun: bool): + """ + Deploy nodes from the specified directory. + """ + project = Project.load(directory) + compiled_project = project.compile() + if dryrun: + compiled_project.validate(client=self.builder_client) + else: + compiled_project.deploy(client=self.builder_client) + + def pull(self, namespace: str, directory: str): + """ + Export nodes from a specific namespace. + """ + print(f"Exporting namespace {namespace} to {directory}...") + Project.pull( + client=self.builder_client, + namespace=namespace, + target_path=directory, + ) + print(f"Finished exporting namespace {namespace} to {directory}.") + + def create_parser(self): + """Creates the CLI arg parser""" + parser = argparse.ArgumentParser(prog="dj", description="DataJunction CLI") + subparsers = parser.add_subparsers(dest="command", required=True) + + # `dj deploy --dryrun` + deploy_parser = subparsers.add_parser( + "deploy", + help="Deploy node YAML definitions from a directory", + ) + deploy_parser.add_argument( + "directory", + help="Path to the directory containing YAML files", + ) + deploy_parser.add_argument( + "--dryrun", + action="store_true", + help="Perform a dry run", + ) + + # `dj pull ` + pull_parser = subparsers.add_parser( + "pull", + help="Pull nodes from a namespace to YAML", + ) + pull_parser.add_argument("namespace", help="The namespace to pull from") + pull_parser.add_argument( + "directory", + help="Path to the directory to output YAML files", + ) + return parser + + def dispatch_command(self, args, parser): + """ + Dispatches the command based on the parsed args + """ + if args.command == "deploy": + self.deploy(args.directory, args.dryrun) + elif args.command == "pull": + self.pull(args.namespace, args.directory) + else: + parser.print_help() # pragma: no cover + + def run(self): + """ + Parse arguments and run the appropriate command. + """ + parser = self.create_parser() + args = parser.parse_args() + self.builder_client.basic_login() + self.dispatch_command(args, parser) + + +def main(builder_client: DJBuilder | None = None): + """ + Main entrypoint for DJ CLI + """ + cli = DJCLI(builder_client=builder_client) + cli.run() + + +if __name__ == "__main__": + main() diff --git a/datajunction-clients/python/datajunction/client.py b/datajunction-clients/python/datajunction/client.py new file mode 100644 index 000000000..ac1aaf78e --- /dev/null +++ b/datajunction-clients/python/datajunction/client.py @@ -0,0 +1,502 @@ +# pylint: disable=too-many-public-methods +"""DataJunction main client module.""" + +import time +from typing import List, Optional, Set, Union +from urllib.parse import urlencode + +from alive_progress import alive_bar + +from datajunction import _internal, models +from datajunction.exceptions import DJClientException, DJTagDoesNotExist +from datajunction.nodes import Cube, Dimension, Metric, Source, Transform +from datajunction.tags import Tag + + +class DJClient(_internal.DJClient): + """ + Client class for basic DJ dag and data access. + """ + + # + # List basic objects: namespaces, dimensions, metrics, cubes + # + def list_namespaces(self, prefix: Optional[str] = None) -> List[str]: + """ + List namespaces starting with a given prefix. + """ + namespaces = self._session.get("/namespaces/").json() + namespace_list = [n["namespace"] for n in namespaces] + if prefix: + namespace_list = [n for n in namespace_list if n.startswith(prefix)] + return namespace_list + + def list_dimensions(self, namespace: Optional[str] = None) -> List[str]: + """ + List dimension nodes for a given namespace or all. + """ + if namespace: + return self._get_nodes_in_namespace( + namespace=namespace, + type_=models.NodeType.DIMENSION, + ) + return self._get_all_nodes(type_=models.NodeType.DIMENSION) + + def list_metrics(self, namespace: Optional[str] = None) -> List[str]: + """ + List metric nodes for a given namespace or all. + """ + if namespace: + return self._get_nodes_in_namespace( + namespace=namespace, + type_=models.NodeType.METRIC, + ) + return self._get_all_nodes(type_=models.NodeType.METRIC) + + def list_cubes(self, namespace: Optional[str] = None) -> List[str]: + """ + List cube nodes for a given namespace or all. + """ + if namespace: + return self._get_nodes_in_namespace( + namespace=namespace, + type_=models.NodeType.CUBE, + ) + return self._get_all_nodes(type_=models.NodeType.CUBE) + + # + # List other nodes: sources, transforms, all. + # + def list_sources(self, namespace: Optional[str] = None) -> List[str]: + """ + List source nodes for a given namespace or all. + """ + if namespace: + return self._get_nodes_in_namespace( + namespace=namespace, + type_=models.NodeType.SOURCE, + ) + return self._get_all_nodes(type_=models.NodeType.SOURCE) + + def list_transforms(self, namespace: Optional[str] = None) -> List[str]: + """ + List transform nodes for a given namespace or all. + """ + if namespace: + return self._get_nodes_in_namespace( + namespace=namespace, + type_=models.NodeType.TRANSFORM, + ) + return self._get_all_nodes(type_=models.NodeType.TRANSFORM) + + def list_nodes( + self, + type_: Optional[models.NodeType] = None, + namespace: Optional[str] = None, + ) -> List[str]: + """ + List any nodes for a given node type and/or namespace. + """ + if namespace: + return self._get_nodes_in_namespace( + namespace=namespace, + type_=type_, + ) + return self._get_all_nodes(type_=type_) + + # + # Get common metrics and dimensions + # + def common_dimensions( + self, + metrics: List[str], + name_only: bool = False, + ) -> List[Union[str, dict]]: # pragma: no cover # Tested in integration tests + """ + Return common dimensions for a set of metrics. + """ + query_params = [] + for metric in metrics: + query_params.append((models.NodeType.METRIC.value, metric)) + json_response = self._session.get( + f"/metrics/common/dimensions/?{urlencode(query_params)}", + ).json() + if name_only: + return [dimension["name"] for dimension in json_response] + return json_response + + def common_metrics( + self, + dimensions: List[str], + name_only: bool = False, + ) -> List[Union[str, dict]]: # pragma: no cover # Tested in integration tests + """ + Return common metrics for a set of dimensions. + """ + query_params = [("node_type", models.NodeType.METRIC.value)] + for dim in dimensions: + query_params.append((models.NodeType.DIMENSION.value, dim)) + json_response = self._session.get( + f"/dimensions/common/?{urlencode(query_params)}", + ).json() + if name_only: + return [metric["name"] for metric in json_response] + return [ + { + "name": metric["name"], + "display_name": metric["display_name"], + "description": metric["description"], + "query": metric["query"], + # perhaps we should also provide `paths` like we do with common dimensions + } + for metric in json_response + ] + + # + # Get SQL + # + def sql( # pylint: disable=too-many-arguments + self, + metrics: List[str], + dimensions: Optional[List[str]] = None, + filters: Optional[List[str]] = None, + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + measures: bool = False, + use_materialized: bool = True, + ): + """ + Builds SQL for one or more metrics with the provided group by dimensions and filters. + """ + endpoint = "/sql/" + if measures: + endpoint = "/sql/measures/v2" + response = self._session.get( + endpoint, + params={ + "metrics": metrics, + "dimensions": dimensions or [], + "filters": filters or [], + "engine_name": engine_name or self.engine_name, + "engine_version": engine_version or self.engine_version, + "use_materialized": use_materialized, + }, + ) + if response.status_code != 200: + return response.json() + if not measures: + return response.json()["sql"] + return response.json() + + # + # Get SQL for a node + # + def node_sql( # pylint: disable=too-many-arguments + self, + node_name: str, + dimensions: Optional[List[str]] = None, + filters: Optional[List[str]] = None, + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + use_materialized: bool = True, + ): + """ + Builds SQL for a node with the provided dimensions and filters. + """ + response = self._session.get( + f"/sql/{node_name}", + params={ + "dimensions": dimensions or [], + "filters": filters or [], + "engine_name": engine_name or self.engine_name, + "engine_version": engine_version or self.engine_version, + "use_materialized": use_materialized, + }, + ) + if response.status_code == 200: + return response.json()["sql"] + return response.json() + + # + # Get data + # + def data( # pylint: disable=too-many-arguments,too-many-locals + self, + metrics: List[str], + dimensions: Optional[List[str]] = None, + filters: Optional[List[str]] = None, + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + async_: bool = True, + ): + """ + Retrieves the data for one or more metrics with the provided dimensions and filters. + """ + return self._data( + metrics=metrics, + dimensions=dimensions, + filters=filters, + engine_name=engine_name, + engine_version=engine_version, + async_=async_, + ) + + def node_data( # pylint: disable=too-many-arguments,too-many-locals + self, + node_name: str, + dimensions: Optional[List[str]] = None, + filters: Optional[List[str]] = None, + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + async_: bool = True, + ): + """ + Retrieves the data for the node with the provided dimensions and filters. + """ + return self._data( + node_name=node_name, + dimensions=dimensions, + filters=filters, + engine_name=engine_name, + engine_version=engine_version, + async_=async_, + ) + + def _data( # pylint: disable=too-many-arguments,too-many-locals + self, + node_name: Optional[str] = None, + metrics: Optional[List[str]] = None, + dimensions: Optional[List[str]] = None, + filters: Optional[List[str]] = None, + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + async_: bool = True, + ): + """ + Fetch data for a node or a set of metrics with dimensions and filters. + """ + printed_links = False + with alive_bar( + title="Processing", + length=20, + bar="smooth", + force_tty=True, + calibrate=5e40, + ) as progress_bar: + poll_interval = 1 # Initial polling interval in seconds + job_state = models.QueryState.UNKNOWN + results = None + path = "/data/" + params = { + "dimensions": dimensions or [], + "filters": filters or [], + "engine_name": engine_name or self.engine_name, + "engine_version": engine_version or self.engine_version, + "async_": async_, + } + + if (node_name and metrics) or not (node_name or metrics): + raise DJClientException( + "Must supply either 'node_name' or 'metrics' to fetch data.", + ) + + if node_name: + path = f"{path}{node_name}" + elif metrics: # pragma: no cover + params["metrics"] = metrics # pragma: no cover + + print(f"Fetching data for '{node_name}' or '{metrics}'") # pragma: no cover + + while job_state not in models.END_JOB_STATES: + progress_bar() # pylint: disable=not-callable + response = self._session.get( + path, + params=params, + ) + results = response.json() + + # Raise errors if any + if not response.status_code < 400: + raise DJClientException( + f"Error retrieving data: {response.text}", + ) # pragma: no cover # pylint: disable=line-too-long + if results["state"] not in models.QueryState.list(): + raise DJClientException( # pragma: no cover + f"Query state {results['state']} is not a DJ-parseable query state!" + " Please reach out to your server admin to make sure DJ is configured" + " correctly.", + ) + + # Update the query state and print links if any + job_state = models.QueryState(results["state"]) + if not printed_links and results["links"]: # pragma: no cover + print( + "Links:\n" + + "\n".join([f"\t* {link}" for link in results["links"]]), + ) + printed_links = True + progress_bar.title = f"Status: {job_state.value}" + + # Update the polling interval + time.sleep(poll_interval) + poll_interval *= 2 + + # Return results if the job has finished + if job_state == models.QueryState.FINISHED: + return self.process_results(results) + if job_state == models.QueryState.CANCELED: # pragma: no cover + raise DJClientException("Query execution was canceled!") + raise DJClientException( # pragma: no cover + f"Error retrieving data: {response.text}", + ) + + # + # Data Catalog and Engines + # + def list_catalogs(self) -> List[str]: + """ + List all catalogs. + """ + json_response = self._session.get("/catalogs/", timeout=self._timeout).json() + return [catalog["name"] for catalog in json_response] + + def list_engines(self) -> List[dict]: + """ + List all engines. + """ + json_response = self._session.get("/engines/", timeout=self._timeout).json() + return [ + {"name": engine["name"], "version": engine["version"]} + for engine in json_response + ] + + # + # Nodes + # + def source(self, node_name: str) -> Source: + """ + Retrieves a source node with that name if one exists. + """ + node_dict = self._verify_node_exists( + node_name, + type_=models.NodeType.SOURCE.value, + ) + node = Source.from_dict(dj_client=self, data=node_dict) + node.primary_key = self._primary_key_from_columns(node_dict["columns"]) + return node + + def transform(self, node_name: str) -> Transform: + """ + Retrieves a transform node with that name if one exists. + """ + node_dict = self._verify_node_exists( + node_name, + type_=models.NodeType.TRANSFORM.value, + ) + node = Transform.from_dict(dj_client=self, data=node_dict) + node.primary_key = self._primary_key_from_columns(node_dict["columns"]) + return node + + def dimension(self, node_name: str) -> "Dimension": + """ + Retrieves a Dimension node with that name if one exists. + """ + node_dict = self._verify_node_exists( + node_name, + type_=models.NodeType.DIMENSION.value, + ) + node = Dimension.from_dict(dj_client=self, data=node_dict) + node.primary_key = self._primary_key_from_columns(node_dict["columns"]) + return node + + def metric(self, node_name: str) -> "Metric": + """ + Retrieves a Metric node with that name if one exists. + """ + node_dict = self._verify_node_exists( + node_name, + type_=models.NodeType.METRIC.value, + ) + metric_dict = self.get_metric(node_name) + node_dict["required_dimensions"] = metric_dict["required_dimensions"] + node = Metric.from_dict(dj_client=self, data=node_dict) + node.primary_key = self._primary_key_from_columns(node_dict["columns"]) + return node + + def cube(self, node_name: str) -> "Cube": # pragma: no cover + """ + Retrieves a Cube node with that name if one exists. + """ + node_dict = self._get_cube(node_name) + if "name" not in node_dict: + raise DJClientException(f"Cube `{node_name}` does not exist") + dimensions = node_dict["cube_node_dimensions"] + metrics = node_dict["cube_node_metrics"] + node_dict["metrics"] = metrics + node_dict["dimensions"] = dimensions + return Cube.from_dict(dj_client=self, data=node_dict) + + def node(self, node_name: str): + """ + Retrieves a node with the name if one exists + """ + node_dict = self._verify_node_exists(node_name) + if not node_dict or "type" not in node_dict: + raise DJClientException( + f"Node `{node_name}` does not exist.", + ) # pragma: no cover + if node_dict["type"] == models.NodeType.SOURCE.value: + return self.source(node_name) + if node_dict["type"] == models.NodeType.DIMENSION.value: + return self.dimension(node_name) + if node_dict["type"] == models.NodeType.TRANSFORM.value: + return self.transform(node_name) + if node_dict["type"] == models.NodeType.METRIC.value: + return self.metric(node_name) + if node_dict["type"] == models.NodeType.CUBE.value: # pragma: no cover + return self.cube(node_name) + + raise DJClientException( # pragma: no cover + f"Node `{node_name}` is of unknown type: {node_dict['type']}", + ) + + # + # Tags + # + def list_nodes_with_tags( + self, + tag_names: List[str], + node_type: Optional[models.NodeType] = None, + skip_missing: bool = False, + ) -> List[str]: + """ + Find all nodes with given tags. The nodes must have all the tags. + """ + node_names: Set[str] = set() + for tag_name in tag_names: + try: + node_names_with_tag = self._list_nodes_with_tag( + tag_name, + node_type=node_type, + ) + except DJTagDoesNotExist as exc: + if skip_missing: + continue + raise exc + if not node_names: + node_names = set(node_names_with_tag) + else: + node_names = node_names.intersection(node_names_with_tag) + return list(node_names) + + def tag(self, tag_name: str) -> "Tag": # pragma: no cover + """ + Retrieves a Tag with that name if one exists. + """ + tag_dict = self._get_tag(tag_name) + if "name" not in tag_dict: + raise DJClientException(f"Tag `{tag_name}` does not exist") + return Tag( + **tag_dict, + dj_client=self, + ) diff --git a/datajunction-clients/python/datajunction/compile.py b/datajunction-clients/python/datajunction/compile.py new file mode 100644 index 000000000..3c0c51003 --- /dev/null +++ b/datajunction-clients/python/datajunction/compile.py @@ -0,0 +1,1168 @@ +# pylint: disable=too-many-lines +""" +Compile a metrics repository. + +This will: + + 1. Build graph of nodes. + 2. Retrieve the schema of source nodes. + 3. Infer the schema of downstream nodes. + 4. Save everything to the DB. + +""" +import asyncio +import logging +import os +import random +import string +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy +from dataclasses import asdict, dataclass, field +from enum import Enum +from pathlib import Path +from typing import Any, Dict, List, Literal, Optional, Union + +import yaml +from rich import box +from rich.align import Align +from rich.console import Console +from rich.live import Live +from rich.table import Table +from yaml.resolver import BaseResolver + +from datajunction import DJBuilder, DJClient +from datajunction._base import SerializableMixin +from datajunction.exceptions import ( + DJClientException, + DJDeploymentFailure, + DJNamespaceAlreadyExists, +) +from datajunction.models import ( + ColumnAttribute, + MetricDirection, + MetricUnit, + NodeMode, + NodeType, +) +from datajunction.tags import Tag + +_logger = logging.getLogger(__name__) + +CONFIG_FILENAME = "dj.yaml" + + +def str_presenter(dumper, data): + """ + YAML representer that uses the | (pipe) character for multiline strings + """ + if len(data.splitlines()) > 1 or "\n" in data: + text_list = [line.rstrip() for line in data.splitlines()] + fixed_data = "\n".join(text_list) + return dumper.represent_scalar( + BaseResolver.DEFAULT_SCALAR_TAG, + fixed_data, + style="|", + ) + return dumper.represent_scalar(BaseResolver.DEFAULT_SCALAR_TAG, data) + + +yaml.add_representer(str, str_presenter) + + +def _parent_dir(path: Union[str, Path]): + """ + Returns the parent directory + """ + return os.path.dirname(os.path.abspath(path)) + + +def _conf_exists(path: Union[str, Path]): + """ + Returns True if a config exists in the Path + """ + return os.path.isfile(os.path.join(path, CONFIG_FILENAME)) + + +def find_project_root(directory: Optional[str] = None): + """ + Returns the project root, identified by a root config file + """ + if directory and not os.path.isdir(directory): + raise DJClientException(f"Directory {directory} does not exist") + checked_dir = directory or os.getcwd() + while not _conf_exists(checked_dir): + checked_dir = _parent_dir(checked_dir) + if checked_dir == "/" and not _conf_exists(checked_dir): + raise DJClientException( + "Cannot find project root, make sure you've " + f"defined a project in a {CONFIG_FILENAME} file", + ) + + return checked_dir + + +@dataclass +class TagYAML: + """ + YAML representation of a tag + """ + + name: str + description: str = "" + tag_type: str = "" + tag_metadata: Optional[Dict] = None + + +class JoinType(str, Enum): + """ + Join type + """ + + LEFT = "left" + RIGHT = "right" + INNER = "inner" + FULL = "full" + CROSS = "cross" + + +class LinkType(str, Enum): + """ + There are two types of dimensions links supported: join links or reference links + """ + + JOIN = "join" + REFERENCE = "reference" + + +@dataclass +class DimensionJoinLinkYAML( + SerializableMixin, +): # pylint: disable=too-many-instance-attributes + """ + YAML representation of a dimension join link + + If a custom `join_on` clause is not specified, DJ will automatically set + this clause to be on the selected column and the dimension node's primary key + """ + + dimension_node: str + type: LinkType = LinkType.JOIN + + node_column: Optional[str] = None + join_type: JoinType = JoinType.LEFT + join_on: Optional[str] = None + role: Optional[str] = None + + @classmethod + def from_dict( + cls, + dj_client: Optional[DJClient], + data: Dict[str, Any], + ) -> "DimensionJoinLinkYAML": + """ + Create an instance of the given dataclass `cls` from a dictionary `data`. + This will handle nested dataclasses and optional types. + """ + if LinkType(data["type"].lower()) != LinkType.JOIN: + raise TypeError("Wrong dimension link type: " + data["type"]) + return super().from_dict(dj_client, data) + + +@dataclass +class DimensionReferenceLinkYAML( + SerializableMixin, +): # pylint: disable=too-many-instance-attributes + """ + YAML representation of a dimension reference link + + The `dimension` input should be a fully qualified dimension attribute name, + e.g., "." + """ + + node_column: str + dimension: str + type: LinkType = LinkType.REFERENCE + role: Optional[str] = None + + @classmethod + def from_dict( + cls, + dj_client: Optional[DJClient], + data: Dict[str, Any], + ) -> "DimensionReferenceLinkYAML": + """ + Create an instance of the given dataclass `cls` from a dictionary `data`. + This will handle nested dataclasses and optional types. + """ + if LinkType(data["type"].lower()) != LinkType.REFERENCE: + raise TypeError( + "Wrong dimension link type: " + data["type"], + ) # pragma: no cover + return super().from_dict(dj_client, data) + + +@dataclass +class NodeYAML(SerializableMixin): + """ + YAML represention of a node + """ + + deploy_order: int = 0 + + +@dataclass +class ColumnYAML(SerializableMixin): + """ + Represents a column + """ + + name: str + type: str + display_name: str | None = None + attributes: list[str] | None = None + + +@dataclass +class LinkableNodeYAML(NodeYAML): + """ + YAML represention of a node type that can be linked to dimension nodes: + source, transform, dimension + """ + + columns: list[ColumnYAML] | None = None + dimension_links: list[ + DimensionJoinLinkYAML | DimensionReferenceLinkYAML + ] | None = None + + def _deploy_column_settings(self, node): + """ + Deploy any column-level settings (e.g., attributes or display name) for the + columns on this node. + """ + if not self.columns: + return + + for column in self.columns: + # Deploy column attributes if present + if column.attributes: + node.set_column_attributes( + column_name=column.name, + attributes=[ + ColumnAttribute(name=attr) for attr in column.attributes + ], + ) + # Deploy display name if present + if column.display_name: + node.set_column_display_name( + column_name=column.name, + display_name=column.display_name, + ) + + def _deploy_dimension_links( # pylint: disable=too-many-locals + self, + name: str, + node_init, + prefix: str, + table: Table, + ): + """ + Deploy any links from columns on this node to columns on dimension nodes + """ + prefixed_name = f"{prefix}.{name}" + node = node_init(prefixed_name) + existing_join_links = {link.dimension.name for link in node.dimension_links} + existing_reference_links = {col.name for col in node.columns if col.dimension} + if self.dimension_links: + for link in self.dimension_links: + prefixed_dimension = render_prefixes( + link.dimension_node + if isinstance(link, DimensionJoinLinkYAML) + else link.dimension, + prefix, + ) + if isinstance(link, DimensionJoinLinkYAML): + if prefixed_dimension in existing_join_links: + existing_join_links.remove( # pragma: no cover + prefixed_dimension, + ) + if link.join_on: + prefixed_join_on = render_prefixes(link.join_on, prefix) + node.link_complex_dimension( + dimension_node=prefixed_dimension, + join_type=link.join_type or JoinType.LEFT, + join_on=prefixed_join_on, + role=link.role, + ) + else: + node.link_dimension( + link.node_column, + prefixed_dimension, + ) + else: + if link.node_column in existing_reference_links: + existing_reference_links.remove( # pragma: no cover + link.node_column, + ) + split_dim = prefixed_dimension.rsplit(".", 1) + node.add_reference_dimension_link( + node_column=link.node_column, + dimension_node=split_dim[0], + dimension_column=split_dim[1], + role=link.role, + ) + + message = f"[green]Dimension {link.type} link created between " + ( + f"{prefixed_name} and {prefixed_dimension}." + if link.type == LinkType.JOIN + else f"{link.node_column} and {prefixed_dimension}" + ) + table.add_row(*[prefixed_name, "[b]link", message]) + + for dim_node in existing_join_links: # pragma: no cover + node.remove_complex_dimension_link(dim_node) + message = f"[i][yellow]Dimension join link removed to {dim_node}" + table.add_row(*[prefixed_name, "[b]link", message]) + for node_col in existing_reference_links: # pragma: no cover + node.remove_reference_dimension_link(node_col) + message = f"[i][yellow]Dimension reference link removed on {node_col}" + table.add_row(*[prefixed_name, "[b]link", message]) + + +@dataclass +class SourceYAML(LinkableNodeYAML): # pylint: disable=too-many-instance-attributes + """ + YAML representation of a source node + """ + + node_type: Literal[NodeType.SOURCE] = NodeType.SOURCE + display_name: Optional[str] = None + table: str = "" + description: Optional[str] = None + primary_key: Optional[List[str]] = None + tags: Optional[List[str]] = None + mode: NodeMode = NodeMode.PUBLISHED + query: Optional[str] = None + deploy_order: int = 1 + + def __post_init__(self): + """ + Validate that the table name is fully qualified + """ + if ( + self.table.count(".") != 2 + or not self.table.replace(".", "").replace("_", "").isalnum() + ): + raise DJClientException( + f"Invalid table name {self.table}, table " + "name must be fully qualified: " + "..", + ) + + def deploy(self, name: str, prefix: str, client: DJBuilder): + """ + Validate a node by deploying it to a temporary system space + """ + catalog, schema, table = self.table.split(".") + node = client.create_source( + display_name=self.display_name, + name=f"{prefix}.{name}", + catalog=catalog, + schema=schema, + table=table, + columns=self.columns, + description=self.description, + primary_key=self.primary_key, + tags=self.tags, + mode=self.mode, + update_if_exists=True, + ) + self._deploy_column_settings(node) + return node + + def deploy_dimension_links( + self, + name: str, + prefix: str, + client: DJBuilder, + table: Table, + ): + """ + Deploy any links from columns on this node to columns on dimension nodes + """ + return self._deploy_dimension_links(name, client.source, prefix, table) + + +@dataclass +class TransformYAML(LinkableNodeYAML): # pylint: disable=too-many-instance-attributes + """ + YAML representation of a transform node + """ + + node_type: Literal[NodeType.TRANSFORM] = NodeType.TRANSFORM + query: str = "" + display_name: Optional[str] = None + description: Optional[str] = None + primary_key: Optional[List[str]] = None + tags: Optional[List[str]] = None + mode: NodeMode = NodeMode.PUBLISHED + custom_metadata: Optional[Dict] = None + deploy_order: int = 2 + + def deploy(self, name: str, prefix: str, client: DJBuilder): + """ + Validate a node by deploying it to a temporary system space + """ + node = client.create_transform( + name=f"{prefix}.{name}", + display_name=self.display_name, + query=self.query, + description=self.description, + primary_key=self.primary_key, + tags=self.tags, + mode=self.mode, + custom_metadata=self.custom_metadata, + update_if_exists=True, + ) + self._deploy_column_settings(node) + return node + + def deploy_dimension_links( + self, + name: str, + prefix: str, + client: DJBuilder, + table: Table, + ): + """ + Deploy any links from columns on this node to columns on dimension nodes + """ + return self._deploy_dimension_links(name, client.transform, prefix, table) + + +@dataclass +class DimensionYAML(LinkableNodeYAML): # pylint: disable=too-many-instance-attributes + """ + YAML representation of a dimension node + """ + + node_type: Literal[NodeType.DIMENSION] = NodeType.DIMENSION + query: str = "" + display_name: Optional[str] = None + description: Optional[str] = None + primary_key: Optional[List[str]] = None + tags: Optional[List[str]] = None + mode: NodeMode = NodeMode.PUBLISHED + deploy_order: int = 3 + + def deploy(self, name: str, prefix: str, client: DJBuilder): + """ + Validate a node by deploying it to a temporary system space + """ + node = client.create_dimension( + name=f"{prefix}.{name}", + display_name=self.display_name, + query=self.query, + description=self.description, + primary_key=self.primary_key, + tags=self.tags, + mode=self.mode, + update_if_exists=True, + ) + self._deploy_column_settings(node) + return node + + def deploy_dimension_links( + self, + name: str, + prefix: str, + client: DJBuilder, + table: Table, + ): + """ + Deploy any links from columns on this node to columns on dimension nodes + """ + return self._deploy_dimension_links(name, client.dimension, prefix, table) + + +@dataclass +class MetricYAML(NodeYAML): # pylint: disable=too-many-instance-attributes + """ + YAML representation of a metric node + """ + + node_type: Literal[NodeType.METRIC] = NodeType.METRIC + query: str = "" + display_name: Optional[str] = None + description: Optional[str] = None + tags: Optional[List[str]] = None + required_dimensions: list[str] | None = None + direction: MetricDirection | None = None + unit: MetricUnit | None = None + mode: NodeMode = NodeMode.PUBLISHED + deploy_order: int = 4 + + def deploy(self, name: str, prefix: str, client: DJBuilder): + """ + Validate a node by deploying it to a temporary system space + """ + node = client.create_metric( + name=f"{prefix}.{name}", + display_name=self.display_name, + query=self.query, + description=self.description, + required_dimensions=self.required_dimensions, + direction=self.direction, + unit=self.unit, + tags=self.tags, + mode=self.mode, + update_if_exists=True, + ) + return node + + +@dataclass +class CubeYAML(NodeYAML): # pylint: disable=too-many-instance-attributes + """ + YAML representation of a cube node + """ + + node_type: Literal[NodeType.CUBE] = NodeType.CUBE + display_name: Optional[str] = None + metrics: List[str] = field(default_factory=list) + dimensions: List[str] = field(default_factory=list) + filters: Optional[List[str]] = None + description: Optional[str] = None + mode: NodeMode = NodeMode.PUBLISHED + tags: Optional[List[str]] = None + deploy_order: int = 5 + + def deploy(self, name: str, prefix: str, client: DJBuilder): + """ + Validate a node by deploying it to a temporary system space + """ + prefixed_metrics = [ + render_prefixes(metric_name, prefix) for metric_name in self.metrics + ] + prefixed_dimensions = [ + render_prefixes(dimension_name, prefix) + for dimension_name in self.dimensions + ] + node = client.create_cube( + name=f"{prefix}.{name}", + display_name=self.display_name, + metrics=prefixed_metrics, + dimensions=prefixed_dimensions, + filters=self.filters, + description=self.description, + mode=self.mode, + tags=self.tags, + update_if_exists=True, + ) + return node + + +@dataclass +class NodeConfig: + """ + A single node configuration + """ + + name: str + definition: Union[SourceYAML, TransformYAML, DimensionYAML, MetricYAML, CubeYAML] + path: str + + +@dataclass +class BuildConfig: + """ + A build configuration for a project + """ + + priority: List[str] = field(default_factory=list[str]) + + +@dataclass +class Project: + """ + A project configuration + """ + + name: str + prefix: str + root_path: str = "" + description: str = "" + build: BuildConfig = field(default_factory=BuildConfig) + tags: List[TagYAML] = field(default_factory=list[TagYAML]) + mode: NodeMode = NodeMode.PUBLISHED + + @classmethod + def load_current(cls): + """ + Return's the nearest project configuration + """ + return cls.load() + + @classmethod + def load(cls, directory: Optional[str] = None): + """ + Return's the nearest project configuration + """ + root = find_project_root(directory) + config_file_path = os.path.join(root, CONFIG_FILENAME) + with open(config_file_path, encoding="utf-8") as f_config: + config_dict = yaml.safe_load(f_config) + config = cls(**config_dict) + config.root_path = root + config.build = ( + BuildConfig(**config.build) # pylint: disable=not-a-mapping + if isinstance(config.build, dict) + else config.build + ) + config.tags = ( + [ + TagYAML(**tag) if isinstance(tag, dict) else tag + for tag in config.tags + ] + if config.tags + else [] + ) + return config + + def compile(self) -> "CompiledProject": + """ + Compile a loaded project by reading all of the node definition files + """ + definitions = load_node_configs_notebook_safe( + repository=Path(self.root_path), + priority=self.build.priority, + ) + compiled = asdict(self) + compiled.update( + {"namespaces": collect_namespaces(definitions), "definitions": definitions}, + ) + compiled_project = CompiledProject(**compiled) + compiled_project.build = self.build + compiled_project.tags = self.tags + return compiled_project + + @staticmethod + def pull( + client: DJBuilder, + namespace: str, + target_path: Union[str, Path], + ignore_existing_files: bool = False, + ): + """ + Pull down a namespace to a local project. + """ + path = Path(target_path) + if any(path.iterdir()) and not ignore_existing_files: + raise DJClientException("The target path must be empty") + node_definitions = client._export_namespace( # pylint: disable=protected-access + namespace=namespace, + ) + with open( + path / Path("dj.yaml"), + "w", + encoding="utf-8", + ) as yaml_file: + yaml.dump( + { + "name": f"Project {namespace} (Autogenerated)", + "description": f"This is an autogenerated project for namespace {namespace}", + "prefix": namespace, + "build": { + "priority": [node["build_name"] for node in node_definitions], + }, + }, + yaml_file, + sort_keys=False, + ) + for node in node_definitions: + del node["build_name"] + node_definition_dir = path / Path(node.pop("directory")) + Path.mkdir(node_definition_dir, parents=True, exist_ok=True) + if ( + node["filename"].endswith(".dimension.yaml") + or node["filename"].endswith(".transform.yaml") + or node["filename"].endswith(".metric.yaml") + ): + node["query"] = inject_prefixes(node["query"], namespace) + elif node["filename"].endswith(".cube.yaml"): + node["metrics"] = [ + inject_prefixes(metric, namespace) for metric in node["metrics"] + ] + node["dimensions"] = [ + inject_prefixes(dimension, namespace) + for dimension in node["dimensions"] + ] + if node.get("dimension_links"): + for link in node["dimension_links"]: # pragma: no cover + if "dimension_node" in link: + link["dimension_node"] = inject_prefixes( + link["dimension_node"], + namespace, + ) + if "join_on" in link: + link["join_on"] = inject_prefixes(link["join_on"], namespace) + if "dimension" in link: + link["dimension"] = inject_prefixes( + link["dimension"], + namespace, + ) + with open( + node_definition_dir / Path(node.pop("filename")), + "w", + encoding="utf-8", + ) as yaml_file: + yaml.dump(node, yaml_file, sort_keys=False) + + +def collect_namespaces(node_configs: List[NodeConfig], prefix: str = ""): + """ + Collect all namespaces that are needed to define a set of nodes + """ + namespaces = set() + prefixed_node_names = ( + [f"{prefix}.{config.name}" for config in node_configs] + if prefix + else [config.name for config in node_configs] + ) + for name in prefixed_node_names: + parts = name.split(".") + num_parts = len(parts) + for i in range(1, num_parts): + namespace = ".".join(parts[:i]) + namespaces.add(namespace) + return namespaces + + +def render_prefixes(parameterized_string: str, prefix: str): + """ + Replaces ${prefix} in a string + """ + return parameterized_string.replace("${prefix}", f"{prefix}.") + + +def inject_prefixes(unparameterized_string: str, prefix: str) -> str: + """ + Replaces a namespace in a string with ${prefix} + """ + return unparameterized_string.replace(f"{prefix}.", "${prefix}") + + +@dataclass +class CompiledProject(Project): + """ + A compiled project with all node definitions loaded + """ + + namespaces: List[str] = field(default_factory=list) + definitions: List[NodeConfig] = field(default_factory=list) + validated: bool = False + errors: List[dict] = field(default_factory=list) + + def _deploy_tags(self, prefix: str, table: Table, client: DJBuilder): + """ + Deploy tags + """ + if not self.tags: + return table + for tag in self.tags: + prefixed_name = f"{prefix}.{tag.name}" + try: + new_tag = Tag( + name=prefixed_name, + description=tag.description, + tag_type=tag.tag_type, + tag_metadata=tag.tag_metadata, + dj_client=client, + ) + new_tag.save() + table.add_row( + *[ + prefixed_name, + "[b][#3A4F6C]tag", + f"[green]Tag {prefixed_name} successfully created (or updated)", + ] + ) + except DJClientException as exc: # pragma: no cover + table.add_row(*[tag.name, "tag", f"[i][red]{str(exc)}"]) + self.errors.append( + {"name": prefixed_name, "type": "tag", "error": str(exc)}, + ) + return table + + def _deploy_namespaces(self, prefix: str, table: Table, client: DJBuilder): + """ + Deploy namespaces + """ + namespaces_to_create = self.namespaces + if prefix: # pragma: no cover + namespaces_to_create = [prefix] + [ + f"{prefix}.{ns}" for ns in list(self.namespaces) + ] + for namespace in namespaces_to_create: + try: + client.create_namespace( + namespace=namespace, + ) + table.add_row( + *[ + namespace, + "[b][#3A4F6C]namespace", + f"[green]Namespace {namespace} successfully created", + ] + ) + except DJNamespaceAlreadyExists: + table.add_row( + *[ + namespace, + "namespace", + f"[i][yellow]Namespace {namespace} already exists", + ] + ) + except DJClientException as exc: + # This is a just-in-case code for some older client versions. + if "already exists" in str(exc): + table.add_row( + *[ + namespace, + "namespace", + f"[i][yellow]Namespace {namespace} already exists", + ] + ) + else: + # pragma: no cover + table.add_row(*[namespace, "namespace", f"[i][red]{str(exc)}"]) + self.errors.append( + { + "name": namespace, + "type": "namespace", + "error": str(exc), + }, + ) + return table + + def _deploy_nodes( + self, + node_configs: List[NodeConfig], + prefix: str, + table: Table, + client: DJBuilder, + ): + """ + Deploy nodes + """ + for node_config in node_configs: + style = ( + "[b][#01B268]" + if isinstance(node_config.definition, SourceYAML) + else "[b][#0162B4]" + if isinstance(node_config.definition, TransformYAML) + else "[b][#A96622]" + if isinstance(node_config.definition, DimensionYAML) + else "[b][#A2293E]" + if isinstance(node_config.definition, MetricYAML) + else "[b][#580075]" + if isinstance(node_config.definition, CubeYAML) + else "" + ) + try: + rendered_node_config = deepcopy(node_config) + prefixed_name = f"{prefix}.{node_config.name}" + # pre-fix the query + if isinstance( + node_config.definition, + (TransformYAML, DimensionYAML, MetricYAML), + ): + rendered_node_config.definition.query = render_prefixes( # type: ignore + rendered_node_config.definition.query or "", # type: ignore + prefix, + ) + # pre-fix the tags + project_tags = [tag.name for tag in self.tags] + if node_config.definition.tags: + rendered_node_config.definition.tags = [ + f"{prefix}.{tag}" + for tag in node_config.definition.tags + if tag in project_tags + ] + created_node = rendered_node_config.definition.deploy( + name=rendered_node_config.name, + prefix=prefix, + client=client, + ) + table.add_row( + *[ + prefixed_name, + f"{style}{created_node.type}", + f"[green]Node {created_node.name} successfully created (or updated)", + ] + ) + except DJClientException as exc: + table.add_row( + *[ + prefixed_name, + f"{style}{node_config.definition.node_type}", + f"[i][red]{str(exc)}", + ] + ) + self.errors.append( + {"name": prefixed_name, "type": "node", "error": str(exc)}, + ) + + def _deploy_dimension_links(self, prefix: str, table: Table, client: DJBuilder): + """ + Deploy any dimension links defined within any node definition + """ + for node_config in self.definitions: + if isinstance( + node_config.definition, + (SourceYAML, TransformYAML, DimensionYAML), + ): + try: + node_config.definition.deploy_dimension_links( + name=node_config.name, + prefix=prefix, + client=client, + table=table, + ) + except DJClientException as exc: + table.add_row( + *[node_config.name, "[b]link[/]", f"[i][red]{str(exc)}"] + ) + self.errors.append( + {"name": node_config.name, "type": "link", "error": str(exc)}, + ) + + def _deploy( + self, + client: DJBuilder, + prefix: str, + console: Console = Console(), + ): + """ + Deploy the compiled project + """ + self.errors = [] + + # Split out cube nodes to be deployed after dimensional graph + cubes = [ + node_config + for node_config in self.definitions + if isinstance(node_config.definition, CubeYAML) + ] + non_cubes = [ + node_config + for node_config in self.definitions + if not isinstance(node_config.definition, CubeYAML) + ] + + table = Table(show_footer=False) + table_centered = Align.center(table) + with Live(table_centered, console=console, screen=False, refresh_per_second=20): + table.title = f"{self.name}\nDeployment for Prefix: [bold green]{prefix}[/ bold green]" + table.box = box.SIMPLE_HEAD + table.add_column("Name", no_wrap=True) + table.add_column("Type", no_wrap=True) + table.add_column("Message", no_wrap=False) + self._deploy_tags(prefix=prefix, table=table, client=client) + self._deploy_namespaces(prefix=prefix, table=table, client=client) + self._deploy_nodes( + node_configs=non_cubes, + prefix=prefix, + table=table, + client=client, + ) + self._deploy_dimension_links(prefix=prefix, table=table, client=client) + self._deploy_nodes( + node_configs=cubes, + prefix=prefix, + table=table, + client=client, + ) + + def _cleanup_namespace( + self, + client: DJBuilder, + prefix: str, + console: Console = Console(), + ): + """ + Cleanup a prefix + """ + table = Table(show_footer=False) + table_centered = Align.center(table) + with Live(table_centered, console=console, screen=False, refresh_per_second=20): + table.title = ( + f"{self.name}\nCleanup for Prefix: [bold red]{prefix}[/ bold red]" + ) + table.box = box.SIMPLE_HEAD + table.add_column("Name", no_wrap=True) + table.add_column("Type", no_wrap=True) + table.add_column("Message", no_wrap=False) + try: + client.delete_namespace(namespace=prefix, cascade=True) + table.add_row( + *[ + prefix, + "[b][#3A4F6C]namespace", + f"[green]Namespace {prefix} successfully deleted.", + ] + ) + except DJClientException as exc: + table.add_row(*[prefix, "namespace", f"[i][red]{str(exc)}"]) + self.errors.append( + { + "name": prefix, + "type": "namespace", + "error": str(exc), + }, + ) + + def validate(self, client, console: Console = Console(), with_cleanup: bool = True): + """ + Validate the compiled project + """ + self.errors = [] + console.clear() + validation_id = "".join(random.choices(string.ascii_letters, k=16)) + system_prefix = f"system.temp.{validation_id}.{self.prefix}" + self._deploy(client=client, prefix=system_prefix, console=console) + if with_cleanup: # pragma: no cover + self._cleanup_namespace( + client=client, + prefix=system_prefix, + console=console, + ) + if self.errors: + raise DJDeploymentFailure(project_name=self.name, errors=self.errors) + self.validated = True + + def deploy(self, client: DJBuilder, console: Console = Console()): + """ + Validate and deploy the compiled project + """ + console.clear() + if not self.validated: + self.validate(client=client, console=console) + self._deploy(client=client, prefix=self.prefix, console=console) + if self.errors: # pragma: no cover + # .deploy() requires .validate() to have been called first so + # theoretically this exception should never or rarely ever be + # hit. This is just a safe fallback in cases where deploying + # worked during validation but failed by a subsequent deployment + # of the same set of definitions + raise DJDeploymentFailure(project_name=self.name, errors=self.errors) + + +def get_name_from_path(repository: Path, path: Path) -> str: + """ + Compute the name of a node given its path and the repository path. + """ + # strip anything before the repository + relative_path = path.relative_to(repository).with_suffix("") + + # Check that there are no additional dots in the node filename + if relative_path.stem.count("."): + raise DJClientException( + f"Invalid node definition filename stem {relative_path.stem}, " + "stem must only have a single dot separator and end with a node type " + "i.e. my_node.source.yaml", + ) + name = str(relative_path).split(".", maxsplit=1)[0] + return name.replace(os.path.sep, ".") + + +async def load_data( + repository: Path, + path: Path, +) -> Optional[NodeConfig]: + """ + Load data from a YAML file. + """ + yaml_cls = ( + SourceYAML + if path.stem.endswith(".source") + else TransformYAML + if path.stem.endswith(".transform") + else DimensionYAML + if path.stem.endswith(".dimension") + else MetricYAML + if path.stem.endswith(".metric") + else CubeYAML + if path.stem.endswith(".cube") + else None + ) + if not yaml_cls: + raise DJClientException( + f"Invalid node definition filename {path.stem}, " + "node definition filename must end with a node type i.e. my_node.source.yaml", + ) + with open(path, encoding="utf-8") as f_yaml: + yaml_dict = yaml.safe_load(f_yaml) + definition = yaml_cls.from_dict(None, yaml_dict) + + return NodeConfig( + name=get_name_from_path(repository=repository, path=path), + definition=definition, + path=str(path), + ) + + +def load_node_configs_notebook_safe(repository: Path, priority: List[str]): + """ + Notebook safe wrapper for load_node_configs function + """ + try: + asyncio.get_running_loop() + with ThreadPoolExecutor(1) as pool: # pragma: no cover + node_configs = pool.submit( + lambda: asyncio.run( + load_node_configs( + repository=repository, + priority=priority, + ), + ), + ).result() + except RuntimeError: + node_configs = asyncio.run( + load_node_configs(repository=repository, priority=priority), + ) + return node_configs + + +async def load_node_configs( + repository: Path, + priority: List[str], +) -> List[Optional[NodeConfig]]: + """ + Load all configs from a repository. + """ + + # load all nodes and their dependencies, exclude CONFIG_FILENAME + paths = { + get_name_from_path(repository=repository, path=path): path + for path in ( + set(repository.glob("**/*.yaml")) - set(repository.glob(CONFIG_FILENAME)) + ) + } + node_configs = [] + for node_name in priority: + try: + node_configs.append( + await load_data(repository=repository, path=paths.pop(node_name)), + ) + except KeyError as exc: + raise DJClientException( + f"Build priority list includes node name {node_name} " + "which has no corresponding definition " + f"{paths.keys()}", + ) from exc + + tasks = [load_data(repository=repository, path=path) for _, path in paths.items()] + non_prioritized_nodes = [node for node in await asyncio.gather(*tasks) if node] + non_prioritized_nodes.sort(key=lambda config: config.definition.deploy_order) + node_configs.extend(non_prioritized_nodes) + return node_configs diff --git a/datajunction-clients/python/datajunction/exceptions.py b/datajunction-clients/python/datajunction/exceptions.py new file mode 100644 index 000000000..10c1f866e --- /dev/null +++ b/datajunction-clients/python/datajunction/exceptions.py @@ -0,0 +1,72 @@ +"""DJ client exceptions""" +from typing import List + + +class DJClientException(Exception): + """ + Base class for client errors. + """ + + +class DJNamespaceAlreadyExists(DJClientException): + """ + Raised when a namespace to be created already exists. + """ + + def __init__(self, ns_name: str, *args) -> None: + self.message = f"Namespace `{ns_name}` already exists." + super().__init__(self.message, *args) + + +class DJTagAlreadyExists(DJClientException): + """ + Raised when a tag to be created already exists. + """ + + def __init__(self, tag_name: str, *args) -> None: + self.message = f"Tag `{tag_name}` already exists." + super().__init__(self.message, *args) + + +class DJTagDoesNotExist(DJClientException): + """ + Raised when a referenced tag does not exist. + """ + + def __init__(self, tag_name: str, *args) -> None: + self.message = f"Tag `{tag_name}` does not exist." + super().__init__(self.message, *args) + + +class DJDeploymentFailure(DJClientException): + """ + Raised when a deployment of a project includes any errors + """ + + def __init__(self, project_name: str, errors: List[dict], *args) -> None: + self.errors = errors + self.message = f"Some failures while deploying project `{project_name}`" + super().__init__(self.message, *args) + + +class DJTableAlreadyRegistered(DJClientException): + """ + Raised when a table is already regsistered in DJ. + """ + + def __init__(self, catalog: str, schema: str, table: str, *args) -> None: + self.message = f"Table `{catalog}.{schema}.{table}` is already registered." + super().__init__(self.message, *args) + + +class DJViewAlreadyRegistered(DJClientException): + """ + Raised when a view is already regsistered in DJ. + """ + + def __init__(self, catalog: str, schema: str, view: str, *args) -> None: + self.message = ( + f"View `{catalog}.{schema}.{view}` is already registered, " + "use replace=True to force a refresh." + ) + super().__init__(self.message, *args) diff --git a/datajunction-clients/python/datajunction/models.py b/datajunction-clients/python/datajunction/models.py new file mode 100644 index 000000000..0d8e2a029 --- /dev/null +++ b/datajunction-clients/python/datajunction/models.py @@ -0,0 +1,263 @@ +"""Models used by the DJ client.""" +import enum +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +from datajunction._base import SerializableMixin + +if TYPE_CHECKING: # pragma: no cover + from datajunction.client import DJClient + + +@dataclass +class Engine(SerializableMixin): + """ + Represents an engine + """ + + name: str + version: Optional[str] + + +class MetricDirection(str, enum.Enum): + """ + The direction of the metric that's considered good, i.e., higher is better + """ + + HIGHER_IS_BETTER = "higher_is_better" + LOWER_IS_BETTER = "lower_is_better" + NEUTRAL = "neutral" + + +class MetricUnit(str, enum.Enum): + """ + Unit + """ + + UNKNOWN = "unknown" + UNITLESS = "unitless" + PERCENTAGE = "percentage" + PROPORTION = "proportion" + DOLLAR = "dollar" + SECOND = "second" + MINUTE = "minute" + HOUR = "hour" + DAY = "day" + WEEK = "week" + MONTH = "month" + YEAR = "year" + + +@dataclass +class MetricMetadata(SerializableMixin): + """ + Metric metadata output + """ + + direction: Optional[MetricDirection] + unit: Optional[MetricUnit] + + @classmethod + def from_dict( + cls, + dj_client: Optional["DJClient"], + data: Dict[str, Any], + ) -> "MetricMetadata": + """ + Create an instance of the given dataclass `cls` from a dictionary `data`. + This will handle nested dataclasses and optional types. + """ + return cls( + direction=MetricDirection(data["direction"].lower()), + unit=MetricUnit(data["unit"]["name"].lower()), + ) + + +class MaterializationJobType(str, enum.Enum): + """ + Materialization job types + """ + + SPARK_SQL = "spark_sql" + DRUID_CUBE = "druid_cube" + + +class MaterializationStrategy(str, enum.Enum): + """ + Materialization strategies + """ + + FULL = "full" + SNAPSHOT = "snapshot" + INCREMENTAL_TIME = "incremental_time" + VIEW = "view" + + +@dataclass +class Materialization(SerializableMixin): + """ + A node's materialization config + """ + + job: MaterializationJobType + strategy: MaterializationStrategy + schedule: str + config: Dict + + def to_dict(self) -> Dict: + """ + Convert to a dict + """ + return { + "job": self.job.value, + "strategy": self.strategy.value, + "schedule": self.schedule, + "config": self.config, + } + + +class NodeMode(str, enum.Enum): + """ + DJ node's mode + """ + + DRAFT = "draft" + PUBLISHED = "published" + + +class NodeStatus(str, enum.Enum): + """ + DJ node's status + """ + + VALID = "valid" + INVALID = "invalid" + + +class NodeType(str, enum.Enum): + """ + DJ node types + """ + + METRIC = "metric" + DIMENSION = "dimension" + SOURCE = "source" + TRANSFORM = "transform" + CUBE = "cube" + + +@dataclass +class ColumnAttribute(SerializableMixin): + """ + Represents a column attribute + """ + + name: str + namespace: Optional[str] = "system" + + @classmethod + def from_dict( + cls, + dj_client: Optional["DJClient"], + data: Dict[str, Any], + ) -> "ColumnAttribute": + """ + Create an instance of the given dataclass `cls` from a dictionary `data`. + This will handle nested dataclasses and optional types. + """ + return ColumnAttribute(**data["attribute_type"]) + + +@dataclass +class Column(SerializableMixin): + """ + Represents a column + """ + + name: str + type: str + display_name: Optional[str] = None + attributes: Optional[List[ColumnAttribute]] = None + dimension: Optional[str] = None + dimension_column: Optional[str] = None + + +@dataclass +class UpdateNode(SerializableMixin): # pylint: disable=too-many-instance-attributes + """ + Fields for updating a node + """ + + display_name: Optional[str] = None + description: Optional[str] = None + mode: Optional[NodeMode] = None + primary_key: Optional[List[str]] = None + query: Optional[str] = None + # this is a problem .... fails many tests + custom_metadata: Optional[Dict] = None + + # source nodes only + catalog: Optional[str] = None + schema_: Optional[str] = None + table: Optional[str] = None + columns: Optional[List[Column]] = field(default_factory=list[Column]) + + # cube nodes only + metrics: Optional[List[str]] = None + dimensions: Optional[List[str]] = None + filters: Optional[List[str]] = None + orderby: Optional[List[str]] = None + limit: Optional[int] = None + + # metric nodes only + required_dimensions: Optional[List[str]] = None + metric_metadata: Optional[MetricMetadata] = None + + +@dataclass +class UpdateTag(SerializableMixin): + """ + Model for a tag update + """ + + description: Optional[str] + tag_metadata: Optional[Dict] + + +class QueryState(str, enum.Enum): + """ + Different states of a query. + """ + + UNKNOWN = "UNKNOWN" + ACCEPTED = "ACCEPTED" + SCHEDULED = "SCHEDULED" + RUNNING = "RUNNING" + FINISHED = "FINISHED" + CANCELED = "CANCELED" + FAILED = "FAILED" + + @classmethod + def list(cls) -> List[str]: + """ + List of available query states as strings + """ + return list(map(lambda c: c.value, cls)) # type: ignore + + +@dataclass +class AvailabilityState(SerializableMixin): + """ + Represents the availability state for a node. + """ + + catalog: str + schema_: Optional[str] + table: str + valid_through_ts: int + + min_temporal_partition: Optional[List[str]] = None + max_temporal_partition: Optional[List[str]] = None + + +END_JOB_STATES = [QueryState.FINISHED, QueryState.CANCELED, QueryState.FAILED] diff --git a/datajunction-clients/python/datajunction/nodes.py b/datajunction-clients/python/datajunction/nodes.py new file mode 100644 index 000000000..756559590 --- /dev/null +++ b/datajunction-clients/python/datajunction/nodes.py @@ -0,0 +1,631 @@ +"""DataJunction base client setup.""" +import abc + +# pylint: disable=redefined-outer-name, import-outside-toplevel, too-many-lines, protected-access +from dataclasses import asdict, dataclass +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +import requests + +from datajunction import models +from datajunction._base import SerializableMixin +from datajunction._internal import ClientEntity +from datajunction.exceptions import DJClientException +from datajunction.tags import Tag + +if TYPE_CHECKING: # pragma: no cover + from datajunction.client import DJClient + + +@dataclass +class Namespace(ClientEntity): # pylint: disable=protected-access + """ + Represents a namespace + """ + + namespace: str + + # + # List + # + def nodes(self): + """ + Retrieves all nodes under this namespace. + """ + return self.dj_client._get_nodes_in_namespace( + self.namespace, + ) + + def sources(self): + """ + Retrieves source nodes under this namespace. + """ + return self.dj_client._get_nodes_in_namespace( + self.namespace, + type_=models.NodeType.SOURCE, + ) + + def transforms(self): + """ + Retrieves transform nodes under this namespace. + """ + return self.dj_client._get_nodes_in_namespace( + self.namespace, + type_=models.NodeType.TRANSFORM, + ) + + def cubes(self): + """ + Retrieves cubes under this namespace. + """ + return self.dj_client._get_nodes_in_namespace( + self.namespace, + type_=models.NodeType.CUBE, + ) + + +@dataclass +class NodeName(SerializableMixin): + """Node name""" + + name: str + + +@dataclass +class DimensionLink(SerializableMixin): + """ + Dimension join links + """ + + dimension: NodeName + join_type: str + join_sql: str + join_cardinality: str + role: str | None + foreign_keys: dict[str, str | None] + + +@dataclass +class Node(ClientEntity): # pylint: disable=too-many-instance-attributes + """ + Represents a DJ node object + """ + + name: str + type: str + description: Optional[str] = None + mode: Optional[models.NodeMode] = None + status: Optional[str] = None + display_name: Optional[str] = None + availability: Optional[models.AvailabilityState] = None + tags: Optional[List[Tag]] = None + primary_key: Optional[List[str]] = None + materializations: Optional[List[Dict[str, Any]]] = None + version: Optional[str] = None + deactivated_at: Optional[int] = None + current_version: Optional[str] = None + columns: Optional[List[models.Column]] = None + query: Optional[str] = None + dimension_links: list[DimensionLink] | None = None + custom_metadata: Optional[Dict[str, Any]] = None + + def to_dict(self, exclude: Optional[List[str]] = None) -> Dict[str, Any]: + """ + Convert the source node to a dictionary. We need to make this method because + the default asdict() method from dataclasses does not handle nested dataclasses. + """ + dict_ = { + "name": self.name, + "type": self.type, + "description": self.description, + "mode": self.mode, + "status": self.status, + "display_name": self.display_name, + "availability": self.availability, + "tags": [ + # asdict() is not used to avoid dataclasses circular serialization error + tag.to_dict() if isinstance(tag, Tag) else tag + for tag in self.tags + ] + if self.tags + else None, + "primary_key": self.primary_key, + "materializations": self.materializations, + "version": self.version, + "deactivated_at": self.deactivated_at, + "current_version": self.current_version, + "columns": [ + asdict(col) if isinstance(col, models.Column) else col + for col in self.columns + ] + if self.columns + else None, + "query": self.query if hasattr(self, "query") else None, + "custom_metadata": self.custom_metadata + if hasattr(self, "custom_metadata") + else None, + } + exclude = exclude + self.exclude if exclude else self.exclude + dict_ = {k: v for k, v in dict_.items() if k not in exclude} + return dict_ + + # + # Node level actions + # + def list_revisions(self) -> List[dict]: + """ + List all revisions of this node + """ + return self.dj_client._get_node_revisions(self.name) + + @abc.abstractmethod + def _update(self) -> requests.Response: + """ + Update the node for fields that have changed. + """ + + def _update_tags(self) -> None: + """ + Update the tags on a node + """ + response = self.dj_client._update_node_tags( + node_name=self.name, + tags=[ + tag.name + if isinstance(tag, Tag) + else tag["name"] + if isinstance(tag, dict) + else tag + if isinstance(tag, str) + else None + for tag in self.tags + ] + if self.tags + else [], + ) + if not response.status_code < 400: # pragma: no cover + raise DJClientException( + f"Error updating tags for node {self.name}: {response.text}", + ) + + def save(self, mode: Optional[models.NodeMode] = models.NodeMode.PUBLISHED) -> dict: + """ + Saves the node to DJ, whether it existed before or not. + """ + existing_node = self.dj_client._get_node(node_name=self.name) + if "name" in existing_node: + # update + self._update_tags() + response = self._update() + if not response.status_code < 400: # pragma: no cover + raise DJClientException( + f"Error updating node `{self.name}`: {response.text}", + ) + self.refresh() + else: + # create + response = self.dj_client._create_node( + node=self, + mode=mode, + ) # pragma: no cover + if not response.status_code < 400: # pragma: no cover + raise DJClientException( + f"Error creating new node `{self.name}`: {response.text}", + ) + self._update_tags() + self.refresh() + + return response.json() + + def refresh(self): + """ + Refreshes a node with its latest version from the database. + """ + refreshed_node = self.dj_client._get_node(self.name) + for key, value in refreshed_node.items(): + if hasattr(self, key): + setattr(self, key, value) + return self + + def delete(self): + """ + Deletes the node (softly). We still keep it in an inactive state. + """ + return self.dj_client.delete_node(self.name) + + def restore(self): + """ + Restores (aka reactivates) the node. + """ + return self.dj_client.restore_node(self.name) + + # + # Node attributes level actions + # + def link_dimension( + self, + column: str, + dimension: str, + dimension_column: Optional[str] = None, + ): + """ + Links the dimension to this node via the node's `column` and the dimension's + `dimension_column`. If no `dimension_column` is provided, the dimension's + primary key will be used automatically. + """ + link_response = self.dj_client._link_dimension_to_node( + self.name, + column, + dimension, + dimension_column, + ) + self.refresh() + return link_response + + def add_reference_dimension_link( + self, + node_column: str, + dimension_node: str, + dimension_column: str, + role: Optional[str] = None, + ): + """ + Adds a reference dimension link from the node's `column` to the dimension node's + `dimension_column` + """ + link_response = self.dj_client._add_reference_dimension_link( + self.name, + node_column, + dimension_node, + dimension_column, + role, + ) + self.refresh() + return link_response + + def link_complex_dimension( # pylint: disable=too-many-arguments + self, + dimension_node: str, + join_type: Optional[str] = None, + *, + join_on: str, + join_cardinality: Optional[str] = None, + role: Optional[str] = None, + ): + """ + Links the dimension to this node via the specified join SQL. + """ + link_response = self.dj_client._link_complex_dimension_to_node( + node_name=self.name, + dimension_node=dimension_node, + join_type=join_type, + join_on=join_on, + join_cardinality=join_cardinality, + role=role, + ) + self.refresh() + return link_response + + def unlink_dimension( + self, + column: str, + dimension: str, + dimension_column: Optional[str], + ): + """ + Removes the dimension link on the node's `column` to the dimension. + """ + link_response = self.dj_client._unlink_dimension_from_node( # pylint: disable=protected-access + self.name, + column, + dimension, + dimension_column, + ) + self.refresh() + return link_response + + def remove_complex_dimension_link( + self, + dimension_node: str, + role: Optional[str] = None, + ): + """ + Removes a complex dimension link from this node + """ + unlink_response = self.dj_client._remove_complex_dimension_link( + node_name=self.name, + dimension_node=dimension_node, + role=role, + ) + self.refresh() + return unlink_response + + def remove_reference_dimension_link( + self, + node_column: str, + ): + """ + Remove a reference dimension linkk from the node's `column`. + """ + link_response = self.dj_client._remove_reference_dimension_link( + self.name, + node_column, + ) + self.refresh() + return link_response + + def add_materialization(self, config: models.Materialization): + """ + Adds a materialization for the node. This will not work for source nodes + as they don't need to be materialized. + """ + upsert_response = self.dj_client._upsert_materialization( + self.name, + config, + ) + self.refresh() + return upsert_response + + def deactivate_materialization(self, materialization_name: str): + """ + Deactivate a materialization for the node. + """ + response = self.dj_client._deactivate_materialization( + self.name, + materialization_name, + ) + self.refresh() + return response + + def add_availability(self, availability: models.AvailabilityState): + """ + Adds an availability state to the node + """ + return self.dj_client._add_availability_state(self.name, availability) + + def set_column_attributes( + self, + column_name: str, + attributes: List[models.ColumnAttribute], + ): + """ + Sets attributes for columns on the node + """ + return self.dj_client._set_column_attributes(self.name, column_name, attributes) + + def set_column_display_name( + self, + column_name: str, + display_name: str, + ): + """ + Sets the display name for a column on the node + """ + return self.dj_client._set_column_display_name( + self.name, + column_name, + display_name, + ) + + +@dataclass +class Source(Node): + """ + DJ source node + """ + + type: str = "source" + catalog: Optional[str] = None + schema_: Optional[str] = None + table: Optional[str] = None + + def to_dict(self, exclude: Optional[List[str]] = None) -> Dict[str, Any]: + """ + Convert the source node to a dictionary + """ + dict_ = super().to_dict(exclude=exclude) + dict_["catalog"] = self.catalog + dict_["schema_"] = self.schema_ + dict_["table"] = self.table + return dict_ + + def __post_init__(self): + """ + When `catalog` is a dictionary, parse out the catalog's + name, otherwise just return the string. + """ + if self.catalog and isinstance(self.catalog, dict): + self.catalog = self.catalog["name"] + + def _update(self) -> requests.Response: + """ + Update the node for fields that have changed + """ + update_node = models.UpdateNode( + display_name=self.display_name, + description=self.description, + mode=self.mode, + primary_key=self.primary_key, + columns=self.columns, + ) + return self.dj_client._update_node(self.name, update_node) + + def validate(self) -> str: + """ + This method is only for Source nodes. + + It will compare the source node metadata and create a new revision if necessary. + """ + response = self.dj_client._refresh_source_node(self.name) + self.refresh() + return response["status"] + + +@dataclass +class NodeWithQuery(Node): + """ + Nodes with query attribute + """ + + query: str = "" + + def to_dict(self, exclude: Optional[List[str]] = None) -> Dict[str, Any]: + dict_ = super().to_dict(exclude=exclude) + dict_["query"] = self.query + return dict_ + + def _update(self) -> requests.Response: + """ + Update the node for fields that have changed. + """ + update_node = models.UpdateNode( + display_name=self.display_name, + description=self.description, + mode=self.mode, + primary_key=self.primary_key, + query=self.query, + custom_metadata=self.custom_metadata, + ) + return self.dj_client._update_node(self.name, update_node) + + def validate(self) -> str: + """ + Check if the node is valid by calling the /validate endpoint. + + For source nodes, see the Source.validate() method. + """ + validation = self.dj_client._validate_node(self) + return validation["status"] + + def publish(self) -> bool: + """ + Change a node's mode to published + """ + self.dj_client._publish_node( + self.name, + models.UpdateNode(mode=models.NodeMode.PUBLISHED), + ) + return True + + def get_upstreams(self) -> List[str]: + """ + Lists the upstream nodes of this node + """ + return [node["name"] for node in self.dj_client._get_node_upstreams(self.name)] + + def get_downstreams(self) -> List[str]: + """ + Lists the downstream nodes of this node + """ + return [ + node["name"] for node in self.dj_client._get_node_downstreams(self.name) + ] + + def get_dimensions(self) -> List[str]: + """ + Lists dimensions available for the node + """ + return self.dj_client._get_node_dimensions(self.name) + + +@dataclass +class Transform(NodeWithQuery): + """ + DJ transform node + """ + + type: str = "transform" + + +@dataclass +class Metric(NodeWithQuery): + """ + DJ metric node + """ + + type: str = "metric" + required_dimensions: Optional[List[str]] = None + metric_metadata: Optional[models.MetricMetadata] = None + + def to_dict(self, exclude: Optional[List[str]] = None) -> Dict[str, Any]: + """ + Convert the source node to a dictionary + """ + dict_ = super().to_dict(exclude=exclude) + dict_["required_dimensions"] = self.required_dimensions + dict_["metric_metadata"] = ( + asdict(self.metric_metadata) if self.metric_metadata else None + ) + return dict_ + + def dimensions(self): + """ + Returns the available dimensions for this metric. + """ + metric = self.dj_client.get_metric(self.name) + return metric["dimensions"] + + def _update(self) -> requests.Response: + """ + Update the node for fields that have changed. + """ + update_node = models.UpdateNode( + display_name=self.display_name, + description=self.description, + mode=self.mode, + primary_key=self.primary_key, + query=self.query, + required_dimensions=self.required_dimensions, + metric_metadata=self.metric_metadata, + ) + return self.dj_client._update_node(self.name, update_node) + + +@dataclass +class Dimension(NodeWithQuery): + """ + DJ dimension node + """ + + type: str = "dimension" + + def linked_nodes(self): + """ + Find all nodes linked to this dimension + """ + return [ + node["name"] + for node in self.dj_client._find_nodes_with_dimension(self.name) + ] + + +@dataclass +class Cube(Node): # pylint: disable=abstract-method + """ + DJ cube node + """ + + type: str = "cube" + metrics: Optional[List[str]] = None + dimensions: Optional[List[str]] = None + filters: Optional[List[str]] = None + + def to_dict(self, exclude: Optional[List[str]] = None) -> Dict[str, Any]: + """ + Convert the source node to a dictionary + """ + dict_ = super().to_dict(exclude=exclude) + dict_["metrics"] = self.metrics + dict_["dimensions"] = self.dimensions + dict_["filters"] = self.filters + return dict_ + + def _update(self): # pragma: no cover + update_node = models.UpdateNode( + description=self.description, + mode=self.mode, + metrics=self.metrics, + dimensions=self.dimensions, + filters=self.filters, + ) + return self.dj_client._update_node(self.name, update_node) diff --git a/datajunction-clients/python/datajunction/tags.py b/datajunction-clients/python/datajunction/tags.py new file mode 100644 index 000000000..42b9a2295 --- /dev/null +++ b/datajunction-clients/python/datajunction/tags.py @@ -0,0 +1,82 @@ +""" +Models related to tags +""" +# pylint: disable=protected-access +from dataclasses import dataclass +from typing import Dict, Optional + +from datajunction._internal import ClientEntity +from datajunction.exceptions import DJClientException +from datajunction.models import UpdateTag + + +@dataclass +class Tag(ClientEntity): + """ + Node tag. + """ + + name: str + tag_type: str + description: Optional[str] = None + display_name: Optional[str] = None + tag_metadata: Optional[Dict] = None + + def to_dict(self) -> dict: + """ + Convert the tag to a dictionary. We need to make this method because + the default asdict() method from dataclasses does not handle nested dataclasses. + """ + return { + "name": self.name, + "tag_type": self.tag_type, + "description": self.description, + "display_name": self.display_name, + "tag_metadata": self.tag_metadata, + } + + def _update(self) -> dict: + """ + Update the tag for fields that have changed + """ + update_tag = UpdateTag( + description=self.description, + tag_metadata=self.tag_metadata, + ) + response = self.dj_client._update_tag(self.name, update_tag) + if not response.status_code < 400: # pragma: no cover + raise DJClientException( + f"Error updating tag `{self.name}`: {response.text}", + ) + return response.json() + + def save(self) -> dict: + """ + Saves the tag to DJ, whether it existed before or not. + """ + existing_tag = self.dj_client._get_tag(tag_name=self.name) + if "name" in existing_tag: + # update + response_json = self._update() + self.refresh() + else: + # create + response = self.dj_client._create_tag( + tag=self, + ) # pragma: no cover + if not response.status_code < 400: # pragma: no cover + raise DJClientException( + f"Error creating new tag `{self.name}`: {response.text}", + ) + response_json = response.json() + return response_json + + def refresh(self): + """ + Refreshes a tag with its latest version from the database. + """ + refreshed_tag = self.dj_client._get_tag(self.name) + for key, value in refreshed_tag.items(): # pragma: no cover + if hasattr(self, key): + setattr(self, key, value) + return self diff --git a/datajunction-clients/python/pdm.lock b/datajunction-clients/python/pdm.lock new file mode 100644 index 000000000..c22c63a05 --- /dev/null +++ b/datajunction-clients/python/pdm.lock @@ -0,0 +1,2957 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "pandas", "test"] +strategy = [] +lock_version = "4.5.0" +content_hash = "sha256:57e7532ecc53f23212e46e5f547e45414932325cd24a65d0f3504beb8c810c0d" + +[[metadata.targets]] +requires_python = "~=3.8" + +[[package]] +name = "about-time" +version = "4.2.1" +requires_python = ">=3.7, <4" +summary = "Easily measure timing and throughput of code blocks, with beautiful human friendly representations." +files = [ + {file = "about-time-4.2.1.tar.gz", hash = "sha256:6a538862d33ce67d997429d14998310e1dbfda6cb7d9bbfbf799c4709847fece"}, + {file = "about_time-4.2.1-py3-none-any.whl", hash = "sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341"}, +] + +[[package]] +name = "accept-types" +version = "0.4.1" +summary = "Determine the best content to send in an HTTP response" +files = [ + {file = "accept-types-0.4.1.tar.gz", hash = "sha256:fb27099716d8f0360408c8ca86d69dbfed44455834b70d1506250abe521b535a"}, + {file = "accept_types-0.4.1-py3-none-any.whl", hash = "sha256:c87feccdffb66b02f9343ff387d7fd5c451ccb2e1221fbd37ea0cedef5cf290f"}, +] + +[[package]] +name = "aiosqlite" +version = "0.20.0" +requires_python = ">=3.8" +summary = "asyncio bridge to the standard sqlite3 module" +dependencies = [ + "typing-extensions>=4.0", +] +files = [ + {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, + {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, +] + +[[package]] +name = "alembic" +version = "1.11.1" +requires_python = ">=3.7" +summary = "A database migration tool for SQLAlchemy." +dependencies = [ + "Mako", + "SQLAlchemy>=1.3.0", + "importlib-metadata; python_version < \"3.9\"", + "importlib-resources; python_version < \"3.9\"", + "typing-extensions>=4", +] +files = [ + {file = "alembic-1.11.1-py3-none-any.whl", hash = "sha256:dc871798a601fab38332e38d6ddb38d5e734f60034baeb8e2db5b642fccd8ab8"}, + {file = "alembic-1.11.1.tar.gz", hash = "sha256:6a810a6b012c88b33458fceb869aef09ac75d6ace5291915ba7fae44de372c01"}, +] + +[[package]] +name = "alive-progress" +version = "3.1.5" +requires_python = ">=3.7, <4" +summary = "A new kind of Progress Bar, with real-time throughput, ETA, and very cool animations!" +dependencies = [ + "about-time==4.2.1", + "grapheme==0.6.0", +] +files = [ + {file = "alive-progress-3.1.5.tar.gz", hash = "sha256:42e399a66c8150dc507602dff7b7953f105ef11faf97ddaa6d27b1cbf45c4c98"}, + {file = "alive_progress-3.1.5-py3-none-any.whl", hash = "sha256:347220c1858e3abe137fa0746895668c04df09c5261a13dc03f05795e8a29be5"}, +] + +[[package]] +name = "amqp" +version = "5.1.1" +requires_python = ">=3.6" +summary = "Low-level AMQP client for Python (fork of amqplib)." +dependencies = [ + "vine>=5.0.0", +] +files = [ + {file = "amqp-5.1.1-py3-none-any.whl", hash = "sha256:6f0956d2c23d8fa6e7691934d8c3930eadb44972cbbd1a7ae3a520f735d43359"}, + {file = "amqp-5.1.1.tar.gz", hash = "sha256:2c1b13fecc0893e946c65cbd5f36427861cffa4ea2201d8f6fca22e2a373b5e2"}, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.13.1" +summary = "ANTLR 4.13.1 runtime for Python 3" +dependencies = [ + "typing; python_version < \"3.5\"", +] +files = [ + {file = "antlr4-python3-runtime-4.13.1.tar.gz", hash = "sha256:3cd282f5ea7cfb841537fe01f143350fdb1c0b1ce7981443a2fa8513fddb6d1a"}, + {file = "antlr4_python3_runtime-4.13.1-py3-none-any.whl", hash = "sha256:78ec57aad12c97ac039ca27403ad61cb98aaec8a3f9bb8144f889aa0fa28b943"}, +] + +[[package]] +name = "anyio" +version = "3.7.0" +requires_python = ">=3.7" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +dependencies = [ + "exceptiongroup; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions; python_version < \"3.8\"", +] +files = [ + {file = "anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0"}, + {file = "anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce"}, +] + +[[package]] +name = "asciidag" +version = "0.2.0" +summary = "Draw DAGs (directed acyclic graphs) as ASCII art, à la git log --graph" +dependencies = [ + "enum34; python_version < \"3.4\"", +] +files = [ + {file = "asciidag-0.2.0-py2.py3-none-any.whl", hash = "sha256:f7ea1e6a867ab4c3a2537ff03bc0f25d8fccc2d5109f9f329220ba4fbb1b3e02"}, + {file = "asciidag-0.2.0.tar.gz", hash = "sha256:acf4df123fc222322467d9bdb2020e44b4e1af37d38129092a080c3cda54a788"}, +] + +[[package]] +name = "asgiref" +version = "3.7.2" +requires_python = ">=3.7" +summary = "ASGI specs, helper code, and adapters" +dependencies = [ + "typing-extensions>=4; python_version < \"3.11\"", +] +files = [ + {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, + {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, +] + +[[package]] +name = "astroid" +version = "3.0.2" +requires_python = ">=3.8.0" +summary = "An abstract syntax tree for Python with inference support." +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.11\"", +] +files = [ + {file = "astroid-3.0.2-py3-none-any.whl", hash = "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c"}, + {file = "astroid-3.0.2.tar.gz", hash = "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91"}, +] + +[[package]] +name = "astunparse" +version = "1.6.3" +summary = "An AST unparser for Python" +dependencies = [ + "six<2.0,>=1.6.1", + "wheel<1.0,>=0.23.0", +] +files = [ + {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, + {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +requires_python = ">=3.7" +summary = "Timeout context manager for asyncio programs" +dependencies = [ + "typing-extensions>=3.6.5; python_version < \"3.8\"", +] +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "attrs" +version = "23.1.0" +requires_python = ">=3.7" +summary = "Classes Without Boilerplate" +dependencies = [ + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +requires_python = ">=3.6" +summary = "Backport of the standard library zoneinfo module" +dependencies = [ + "importlib-resources; python_version < \"3.7\"", +] +files = [ + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +extras = ["tzdata"] +requires_python = ">=3.6" +summary = "Backport of the standard library zoneinfo module" +dependencies = [ + "backports-zoneinfo==0.2.1", + "tzdata", +] +files = [ + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[[package]] +name = "bcrypt" +version = "4.0.1" +requires_python = ">=3.6" +summary = "Modern password hashing for your software and your servers" +files = [ + {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, + {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, + {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, + {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, +] + +[[package]] +name = "billiard" +version = "4.1.0" +requires_python = ">=3.7" +summary = "Python multiprocessing fork with improvements and bugfixes" +files = [ + {file = "billiard-4.1.0-py3-none-any.whl", hash = "sha256:0f50d6be051c6b2b75bfbc8bfd85af195c5739c281d3f5b86a5640c65563614a"}, + {file = "billiard-4.1.0.tar.gz", hash = "sha256:1ad2eeae8e28053d729ba3373d34d9d6e210f6e4d8bf0a9c64f92bd053f1edf5"}, +] + +[[package]] +name = "cachelib" +version = "0.10.2" +requires_python = ">=3.7" +summary = "A collection of cache libraries in the same API interface." +files = [ + {file = "cachelib-0.10.2-py3-none-any.whl", hash = "sha256:42d49f2fad9310dd946d7be73d46776bcd4d5fde4f49ad210cfdd447fbdfc346"}, + {file = "cachelib-0.10.2.tar.gz", hash = "sha256:593faeee62a7c037d50fc835617a01b887503f972fb52b188ae7e50e9cb69740"}, +] + +[[package]] +name = "cachetools" +version = "5.3.1" +requires_python = ">=3.7" +summary = "Extensible memoizing collections and decorators" +files = [ + {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, + {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, +] + +[[package]] +name = "celery" +version = "5.3.1" +requires_python = ">=3.8" +summary = "Distributed Task Queue." +dependencies = [ + "backports-zoneinfo>=0.2.1; python_version < \"3.9\"", + "billiard<5.0,>=4.1.0", + "click-didyoumean>=0.3.0", + "click-plugins>=1.1.1", + "click-repl>=0.2.0", + "click<9.0,>=8.1.2", + "importlib-metadata>=3.6; python_version < \"3.8\"", + "kombu<6.0,>=5.3.1", + "python-dateutil>=2.8.2", + "tzdata>=2022.7", + "vine<6.0,>=5.0.0", +] +files = [ + {file = "celery-5.3.1-py3-none-any.whl", hash = "sha256:27f8f3f3b58de6e0ab4f174791383bbd7445aff0471a43e99cfd77727940753f"}, + {file = "celery-5.3.1.tar.gz", hash = "sha256:f84d1c21a1520c116c2b7d26593926581191435a03aa74b77c941b93ca1c6210"}, +] + +[[package]] +name = "certifi" +version = "2023.5.7" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +files = [ + {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, + {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, +] + +[[package]] +name = "cffi" +version = "1.15.1" +summary = "Foreign Function Interface for Python calling C code." +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[[package]] +name = "cfgv" +version = "3.3.1" +requires_python = ">=3.6.1" +summary = "Validate configuration and produce human readable error messages." +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.1.0" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +files = [ + {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, + {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, +] + +[[package]] +name = "click" +version = "8.1.3" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +dependencies = [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.0" +requires_python = ">=3.6.2,<4.0.0" +summary = "Enables git-like *did-you-mean* feature in click" +dependencies = [ + "click>=7", +] +files = [ + {file = "click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"}, + {file = "click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667"}, +] + +[[package]] +name = "click-plugins" +version = "1.1.1" +summary = "An extension module for click to enable registering CLI commands via setuptools entry-points." +dependencies = [ + "click>=4.0", +] +files = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +requires_python = ">=3.6" +summary = "REPL plugin for Click" +dependencies = [ + "click>=7.0", + "prompt-toolkit>=3.0.36", +] +files = [ + {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, + {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.2.7" +requires_python = ">=3.7" +summary = "Code coverage measurement for Python" +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[[package]] +name = "coverage" +version = "7.2.7" +extras = ["toml"] +requires_python = ">=3.7" +summary = "Code coverage measurement for Python" +dependencies = [ + "coverage==7.2.7", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[[package]] +name = "cryptography" +version = "41.0.3" +requires_python = ">=3.7" +summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +dependencies = [ + "cffi>=1.12", +] +files = [ + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, + {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, + {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, + {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, +] + +[[package]] +name = "datajunction-server" +version = "0.0.1a39" +requires_python = "<4.0,>=3.8" +path = "../../datajunction-server" +summary = "DataJunction server library for running to a DataJunction server" +dependencies = [ + "accept-types<1.0.0,>=0.4.1", + "aiosqlite>=0.20.0", + "alembic>=1.10.3", + "antlr4-python3-runtime==4.13.1", + "asciidag<1.0.0,>=0.2.0", + "bcrypt>=4.0.1", + "cachelib<1.0.0,>=0.10.2", + "cachetools>=5.3.1", + "celery<6.0.0,>=5.2.7", + "cryptography>=41.0.3", + "fastapi-cache2>=0.2.1", + "fastapi>=0.110.0", + "google-api-python-client>=2.95.0", + "google-auth-httplib2>=0.1.0", + "google-auth-oauthlib>=1.0.0", + "jinja2>=3.1.4", + "line-profiler>=4.0.3", + "msgpack<2.0.0,>=1.0.5", + "nbformat>=5.10.4", + "opentelemetry-instrumentation-fastapi==0.38b0", + "passlib>=1.7.4", + "psycopg>=3.1.16", + "pydantic<2", + "pytest-asyncio==0.21.2", + "python-dotenv<1.0.0,>=0.19.0", + "python-jose>=3.3.0", + "python-multipart>=0.0.6", + "redis<5.0.0,>=4.5.4", + "requests<=2.29.0,>=2.28.2", + "rich<14.0.0,>=13.3.3", + "sqlalchemy-utils<1.0.0,>=0.40.0", + "sqlalchemy>=2", + "sqlparse<1.0.0,>=0.4.3", + "sse-starlette>=1.6.0", + "strawberry-graphql>=0.204.0", + "types-cachetools>=5.3.0.6", + "yarl<2.0.0,>=1.8.2", +] + +[[package]] +name = "deprecated" +version = "1.2.14" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "Python @deprecated decorator to deprecate old python classes, functions or methods." +dependencies = [ + "wrapt<2,>=1.10", +] +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +summary = "A library to handle automated deprecations" +dependencies = [ + "packaging", +] +files = [ + {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, + {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, +] + +[[package]] +name = "dill" +version = "0.3.7" +requires_python = ">=3.7" +summary = "serialize all of Python" +files = [ + {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, + {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, +] + +[[package]] +name = "distlib" +version = "0.3.6" +summary = "Distribution utilities" +files = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] + +[[package]] +name = "docker" +version = "7.0.0" +requires_python = ">=3.8" +summary = "A Python library for the Docker Engine API." +dependencies = [ + "packaging>=14.0", + "pywin32>=304; sys_platform == \"win32\"", + "requests>=2.26.0", + "urllib3>=1.26.0", +] +files = [ + {file = "docker-7.0.0-py3-none-any.whl", hash = "sha256:12ba681f2777a0ad28ffbcc846a69c31b4dfd9752b47eb425a274ee269c5e14b"}, + {file = "docker-7.0.0.tar.gz", hash = "sha256:323736fb92cd9418fc5e7133bc953e11a9da04f4483f828b527db553f1e7e5a3"}, +] + +[[package]] +name = "ecdsa" +version = "0.18.0" +requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "ECDSA cryptographic signature library (pure python)" +dependencies = [ + "six>=1.9.0", +] +files = [ + {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, + {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.2" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +files = [ + {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, + {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, +] + +[[package]] +name = "execnet" +version = "2.0.2" +requires_python = ">=3.7" +summary = "execnet: rapid multi-Python deployment" +files = [ + {file = "execnet-2.0.2-py3-none-any.whl", hash = "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41"}, + {file = "execnet-2.0.2.tar.gz", hash = "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"}, +] + +[[package]] +name = "fastapi" +version = "0.110.1" +requires_python = ">=3.8" +summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +dependencies = [ + "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", + "starlette<0.38.0,>=0.37.2", + "typing-extensions>=4.8.0", +] +files = [ + {file = "fastapi-0.110.1-py3-none-any.whl", hash = "sha256:5df913203c482f820d31f48e635e022f8cbfe7350e4830ef05a3163925b1addc"}, + {file = "fastapi-0.110.1.tar.gz", hash = "sha256:6feac43ec359dfe4f45b2c18ec8c94edb8dc2dfc461d417d9e626590c071baad"}, +] + +[[package]] +name = "fastapi-cache2" +version = "0.2.2" +requires_python = "<4.0,>=3.8" +summary = "Cache for FastAPI" +dependencies = [ + "fastapi", + "importlib-metadata<7.0.0,>=6.6.0; python_version < \"3.8\"", + "pendulum<4.0.0,>=3.0.0", + "typing-extensions>=4.1.0", + "uvicorn", +] +files = [ + {file = "fastapi_cache2-0.2.2-py3-none-any.whl", hash = "sha256:e1fae86d8eaaa6c8501dfe08407f71d69e87cc6748042d59d51994000532846c"}, + {file = "fastapi_cache2-0.2.2.tar.gz", hash = "sha256:71bf4450117dc24224ec120be489dbe09e331143c9f74e75eb6f576b78926026"}, +] + +[[package]] +name = "fastjsonschema" +version = "2.20.0" +summary = "Fastest Python implementation of JSON schema" +files = [ + {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"}, + {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"}, +] + +[[package]] +name = "filelock" +version = "3.12.2" +requires_python = ">=3.7" +summary = "A platform independent file lock." +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] + +[[package]] +name = "google-api-core" +version = "2.14.0" +requires_python = ">=3.7" +summary = "Google API client core library" +dependencies = [ + "google-auth<3.0.dev0,>=2.14.1", + "googleapis-common-protos<2.0.dev0,>=1.56.2", + "protobuf!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0.dev0,>=3.19.5", + "requests<3.0.0.dev0,>=2.18.0", +] +files = [ + {file = "google-api-core-2.14.0.tar.gz", hash = "sha256:5368a4502b793d9bbf812a5912e13e4e69f9bd87f6efb508460c43f5bbd1ce41"}, + {file = "google_api_core-2.14.0-py3-none-any.whl", hash = "sha256:de2fb50ed34d47ddbb2bd2dcf680ee8fead46279f4ed6b16de362aca23a18952"}, +] + +[[package]] +name = "google-api-python-client" +version = "2.107.0" +requires_python = ">=3.7" +summary = "Google API Client Library for Python" +dependencies = [ + "google-api-core!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0.dev0,>=1.31.5", + "google-auth-httplib2>=0.1.0", + "google-auth<3.0.0.dev0,>=1.19.0", + "httplib2<1.dev0,>=0.15.0", + "uritemplate<5,>=3.0.1", +] +files = [ + {file = "google-api-python-client-2.107.0.tar.gz", hash = "sha256:ef6d4c1a17fe9ec0894fc6d4f61e751c4b859fb33f2ab5b881ceb0b80ba442ba"}, + {file = "google_api_python_client-2.107.0-py2.py3-none-any.whl", hash = "sha256:51d7bf676f41a77b00b7b9c72ace0c1db3dd5a4dd392a13ae897cf4f571a3539"}, +] + +[[package]] +name = "google-auth" +version = "2.23.4" +requires_python = ">=3.7" +summary = "Google Authentication Library" +dependencies = [ + "cachetools<6.0,>=2.0.0", + "pyasn1-modules>=0.2.1", + "rsa<5,>=3.1.4", +] +files = [ + {file = "google-auth-2.23.4.tar.gz", hash = "sha256:79905d6b1652187def79d491d6e23d0cbb3a21d3c7ba0dbaa9c8a01906b13ff3"}, + {file = "google_auth-2.23.4-py2.py3-none-any.whl", hash = "sha256:d4bbc92fe4b8bfd2f3e8d88e5ba7085935da208ee38a134fc280e7ce682a05f2"}, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.1.1" +summary = "Google Authentication Library: httplib2 transport" +dependencies = [ + "google-auth", + "httplib2>=0.19.0", +] +files = [ + {file = "google-auth-httplib2-0.1.1.tar.gz", hash = "sha256:c64bc555fdc6dd788ea62ecf7bccffcf497bf77244887a3f3d7a5a02f8e3fc29"}, + {file = "google_auth_httplib2-0.1.1-py2.py3-none-any.whl", hash = "sha256:42c50900b8e4dcdf8222364d1f0efe32b8421fb6ed72f2613f12f75cc933478c"}, +] + +[[package]] +name = "google-auth-oauthlib" +version = "1.1.0" +requires_python = ">=3.6" +summary = "Google Authentication Library" +dependencies = [ + "google-auth>=2.15.0", + "requests-oauthlib>=0.7.0", +] +files = [ + {file = "google-auth-oauthlib-1.1.0.tar.gz", hash = "sha256:83ea8c3b0881e453790baff4448e8a6112ac8778d1de9da0b68010b843937afb"}, + {file = "google_auth_oauthlib-1.1.0-py2.py3-none-any.whl", hash = "sha256:089c6e587d36f4803ac7e0720c045c6a8b1fd1790088b8424975b90d0ee61c12"}, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.61.0" +requires_python = ">=3.7" +summary = "Common protobufs used in Google APIs" +dependencies = [ + "protobuf!=3.20.0,!=3.20.1,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0.dev0,>=3.19.5", +] +files = [ + {file = "googleapis-common-protos-1.61.0.tar.gz", hash = "sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b"}, + {file = "googleapis_common_protos-1.61.0-py2.py3-none-any.whl", hash = "sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0"}, +] + +[[package]] +name = "grapheme" +version = "0.6.0" +summary = "Unicode grapheme helpers" +files = [ + {file = "grapheme-0.6.0.tar.gz", hash = "sha256:44c2b9f21bbe77cfb05835fec230bd435954275267fea1858013b102f8603cca"}, +] + +[[package]] +name = "graphql-core" +version = "3.2.3" +requires_python = ">=3.6,<4" +summary = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." +dependencies = [ + "typing-extensions<5,>=4.2; python_version < \"3.8\"", +] +files = [ + {file = "graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676"}, + {file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"}, +] + +[[package]] +name = "greenlet" +version = "3.0.3" +requires_python = ">=3.7" +summary = "Lightweight in-process concurrent programming" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +requires_python = ">=3.8" +summary = "A minimal low-level HTTP client." +dependencies = [ + "certifi", + "h11<0.15,>=0.13", +] +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[[package]] +name = "httplib2" +version = "0.22.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "A comprehensive HTTP client library." +dependencies = [ + "pyparsing!=3.0.0,!=3.0.1,!=3.0.2,!=3.0.3,<4,>=2.4.2; python_version > \"3.0\"", + "pyparsing<3,>=2.4.2; python_version < \"3.0\"", +] +files = [ + {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, + {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, +] + +[[package]] +name = "httpx" +version = "0.27.0" +requires_python = ">=3.8" +summary = "The next generation HTTP client." +dependencies = [ + "anyio", + "certifi", + "httpcore==1.*", + "idna", + "sniffio", +] +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[[package]] +name = "identify" +version = "2.5.24" +requires_python = ">=3.7" +summary = "File identification library for Python" +files = [ + {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, + {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, +] + +[[package]] +name = "idna" +version = "3.4" +requires_python = ">=3.5" +summary = "Internationalized Domain Names in Applications (IDNA)" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.0.1" +requires_python = ">=3.7" +summary = "Read metadata from Python packages" +dependencies = [ + "typing-extensions>=3.6.4; python_version < \"3.8\"", + "zipp>=0.5", +] +files = [ + {file = "importlib_metadata-6.0.1-py3-none-any.whl", hash = "sha256:1543daade821c89b1c4a55986c326f36e54f2e6ca3bad96be4563d0acb74dcd4"}, + {file = "importlib_metadata-6.0.1.tar.gz", hash = "sha256:950127d57e35a806d520817d3e92eec3f19fdae9f0cd99da77a407c5aabefba3"}, +] + +[[package]] +name = "importlib-resources" +version = "5.12.0" +requires_python = ">=3.7" +summary = "Read resources from Python packages" +dependencies = [ + "zipp>=3.1.0; python_version < \"3.10\"", +] +files = [ + {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, + {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.12.0" +requires_python = ">=3.8.0" +summary = "A Python utility / library to sort Python imports." +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[[package]] +name = "jsonschema" +version = "4.22.0" +requires_python = ">=3.8" +summary = "An implementation of JSON Schema validation for Python" +dependencies = [ + "attrs>=22.2.0", + "importlib-resources>=1.4.0; python_version < \"3.9\"", + "jsonschema-specifications>=2023.03.6", + "pkgutil-resolve-name>=1.3.10; python_version < \"3.9\"", + "referencing>=0.28.4", + "rpds-py>=0.7.1", +] +files = [ + {file = "jsonschema-4.22.0-py3-none-any.whl", hash = "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802"}, + {file = "jsonschema-4.22.0.tar.gz", hash = "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7"}, +] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +requires_python = ">=3.8" +summary = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +dependencies = [ + "importlib-resources>=1.4.0; python_version < \"3.9\"", + "referencing>=0.31.0", +] +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +requires_python = ">=3.8" +summary = "Jupyter core package. A base package on which Jupyter projects rely." +dependencies = [ + "platformdirs>=2.5", + "pywin32>=300; sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"", + "traitlets>=5.3", +] +files = [ + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, +] + +[[package]] +name = "kombu" +version = "5.3.1" +requires_python = ">=3.8" +summary = "Messaging library for Python." +dependencies = [ + "amqp<6.0.0,>=5.1.1", + "backports-zoneinfo[tzdata]>=0.2.1; python_version < \"3.9\"", + "typing-extensions; python_version < \"3.10\"", + "vine", +] +files = [ + {file = "kombu-5.3.1-py3-none-any.whl", hash = "sha256:48ee589e8833126fd01ceaa08f8a2041334e9f5894e5763c8486a550454551e9"}, + {file = "kombu-5.3.1.tar.gz", hash = "sha256:fbd7572d92c0bf71c112a6b45163153dea5a7b6a701ec16b568c27d0fd2370f2"}, +] + +[[package]] +name = "line-profiler" +version = "4.1.0" +requires_python = ">=3.6" +summary = "Line-by-line profiler" +files = [ + {file = "line_profiler-4.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5986f4ae3b84fe3986bb5079df8f0ff7b1212831508b3ea283a66a0bed29ee4"}, + {file = "line_profiler-4.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3fd75a74687c81a82ef70457050546ebdd64f8dabdf31c3497eeeaa13bedf26"}, + {file = "line_profiler-4.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60de42c5a6d02151d86f115aca6647e44ed47bb46cf2de4ea7718096472f62c9"}, + {file = "line_profiler-4.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:558dd5099188320fd5cfd03b446499155f7b08a4752e4bbc1c940754d981b4a4"}, + {file = "line_profiler-4.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ce9460d5201b28e42d61f2e4c53f50a962674a599a971f16ce00d8c9f158fa6"}, + {file = "line_profiler-4.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:f220b26e263bacb3c4c39ada33ad6fddeb810e37202b228b8590a993bb558873"}, + {file = "line_profiler-4.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9db0d09f982304f74a0dea273b1a55c9c2c6b429e6e5eb0bbf16f3d0c957a6df"}, + {file = "line_profiler-4.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:361823d5b7a00f4384193f499a6052856ffa5ac15a4885f7d96a193659ca7ea1"}, + {file = "line_profiler-4.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f79a032554a63181e218cd3eb364afbca4df3dd42abbd876ed4681c99077d2f"}, + {file = "line_profiler-4.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0001c957fa9e164a481cd65e91928f4fbe521e848b283c72b30f3ce22ac2bb84"}, + {file = "line_profiler-4.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a736cdccf1f97c210c0c4b932cb8b5895f16a73e974ba32dd200c562c888090b"}, + {file = "line_profiler-4.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:13f7b4cee81393ca6dacaeb3ebbfa104841202d6c6c69c800fae1bbde7244cae"}, + {file = "line_profiler-4.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:18b511dd9bfe457c76bebf7c2f486f2896433e83c97aa6fff7686115be6c1ca9"}, + {file = "line_profiler-4.1.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f553f225aac5443bf5cf32040041d08c74f17f408d0eb654dcb29fe8fb657a32"}, + {file = "line_profiler-4.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32866bdebbc322a708d29d4b9f90c606f498d56a55132fcd316fb8ed2aba577c"}, + {file = "line_profiler-4.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4e20e1c0bf3fd899a54ec74786463c754a852542f2aee9c357344279286a147b"}, + {file = "line_profiler-4.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7debb28f33611aea8c54c86fbb8464ed58cd185bc26c829dc8581cc68b87f276"}, + {file = "line_profiler-4.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:34be7a8f305c7d625f6c40ac465010cd77c92383372869e9ba2fa5d85e4a63ae"}, + {file = "line_profiler-4.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:950fd857238dd019274d1206b2440de749e631fd9f76f8c9ba6b1374cb0da2fc"}, + {file = "line_profiler-4.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e1aa55a233cd64eb46b769362aab0e3004feabdebb9bf94eb3acea1a204748"}, + {file = "line_profiler-4.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43aacb880d3b25bf47a3c1c1870bbee6ff4e533de5382636431f321a4d5dbe66"}, + {file = "line_profiler-4.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:57adfc47a87d109d304e6463b583fd53b1191c5b663a204b7512a27b460e4ac2"}, + {file = "line_profiler-4.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f6ae40c37744ef97731aae8b4c413ce4d6277733aa5614c3f1e2242716c41b2e"}, + {file = "line_profiler-4.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:a82649c7f50e50c48125386434ca80e718c401bc65080a03ca862ebc4fab0440"}, + {file = "line_profiler-4.1.0.tar.gz", hash = "sha256:dd93444373231e624aee354ceaf29ac35889660923cbf912124c9872cf410127"}, +] + +[[package]] +name = "mako" +version = "1.2.4" +requires_python = ">=3.7" +summary = "A super-fast templating language that borrows the best ideas from the existing templating languages." +dependencies = [ + "MarkupSafe>=0.9.2", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, + {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.3" +requires_python = ">=3.7" +summary = "Safely add untrusted strings to HTML/XML markup." +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +requires_python = ">=3.6" +summary = "McCabe checker, plugin for flake8" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "msgpack" +version = "1.0.5" +summary = "MessagePack serializer" +files = [ + {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, + {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, + {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, + {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, + {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, + {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, + {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, + {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, + {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, + {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, + {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, + {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, +] + +[[package]] +name = "multidict" +version = "6.0.4" +requires_python = ">=3.7" +summary = "multidict implementation" +files = [ + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] + +[[package]] +name = "namesgenerator" +version = "0.3" +summary = "" +files = [ + {file = "namesgenerator-0.3.tar.gz", hash = "sha256:50a03cc15e95edbf88a7ff86179f217f43eb2b2d6ee30fac3e9a20a54985b72e"}, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +requires_python = ">=3.8" +summary = "The Jupyter Notebook format" +dependencies = [ + "fastjsonschema>=2.15", + "jsonschema>=2.6", + "jupyter-core!=5.0.*,>=4.12", + "traitlets>=5.1", +] +files = [ + {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, + {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +summary = "Node.js virtual environment builder" +dependencies = [ + "setuptools", +] +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[[package]] +name = "numpy" +version = "1.24.4" +requires_python = ">=3.8" +summary = "Fundamental package for array computing in Python" +files = [ + {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, + {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, + {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, + {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, + {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, + {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, + {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, + {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, + {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, + {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +requires_python = ">=3.6" +summary = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[[package]] +name = "opentelemetry-api" +version = "1.18.0" +requires_python = ">=3.7" +summary = "OpenTelemetry Python API" +dependencies = [ + "deprecated>=1.2.6", + "importlib-metadata~=6.0.0", + "setuptools>=16.0", +] +files = [ + {file = "opentelemetry_api-1.18.0-py3-none-any.whl", hash = "sha256:d05bcc94ec239fd76fd90d784c5e3ad081a8a1ac2ffc8a2c83a49ace052d1492"}, + {file = "opentelemetry_api-1.18.0.tar.gz", hash = "sha256:2bbf29739fcef268c419e3bf1735566c2e7f81026c14bcc78b62a0b97f8ecf2f"}, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.38b0" +requires_python = ">=3.7" +summary = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +dependencies = [ + "opentelemetry-api~=1.4", + "setuptools>=16.0", + "wrapt<2.0.0,>=1.0.0", +] +files = [ + {file = "opentelemetry_instrumentation-0.38b0-py3-none-any.whl", hash = "sha256:48eed87e5db9d2cddd57a8ea359bd15318560c0ffdd80d90a5fc65816e15b7f4"}, + {file = "opentelemetry_instrumentation-0.38b0.tar.gz", hash = "sha256:3dbe93248eec7652d5725d3c6d2f9dd048bb8fda6b0505aadbc99e51638d833c"}, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.38b0" +requires_python = ">=3.7" +summary = "ASGI instrumentation for OpenTelemetry" +dependencies = [ + "asgiref~=3.0", + "opentelemetry-api~=1.12", + "opentelemetry-instrumentation==0.38b0", + "opentelemetry-semantic-conventions==0.38b0", + "opentelemetry-util-http==0.38b0", +] +files = [ + {file = "opentelemetry_instrumentation_asgi-0.38b0-py3-none-any.whl", hash = "sha256:c5bba11505008a3cd1b2c42b72f85f3f4f5af50ab931eddd0b01bde376dc5971"}, + {file = "opentelemetry_instrumentation_asgi-0.38b0.tar.gz", hash = "sha256:32d1034c253de6048d0d0166b304f9125267ca9329e374202ebe011a206eba53"}, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.38b0" +requires_python = ">=3.7" +summary = "OpenTelemetry FastAPI Instrumentation" +dependencies = [ + "opentelemetry-api~=1.12", + "opentelemetry-instrumentation-asgi==0.38b0", + "opentelemetry-instrumentation==0.38b0", + "opentelemetry-semantic-conventions==0.38b0", + "opentelemetry-util-http==0.38b0", +] +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.38b0-py3-none-any.whl", hash = "sha256:91139586732e437b1c3d5cf838dc5be910bce27b4b679612112be03fcc4fa2aa"}, + {file = "opentelemetry_instrumentation_fastapi-0.38b0.tar.gz", hash = "sha256:8946fd414084b305ad67556a1907e2d4a497924d023effc5ea3b4b1b0c55b256"}, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.38b0" +requires_python = ">=3.7" +summary = "OpenTelemetry Semantic Conventions" +files = [ + {file = "opentelemetry_semantic_conventions-0.38b0-py3-none-any.whl", hash = "sha256:b0ba36e8b70bfaab16ee5a553d809309cc11ff58aec3d2550d451e79d45243a7"}, + {file = "opentelemetry_semantic_conventions-0.38b0.tar.gz", hash = "sha256:37f09e47dd5fc316658bf9ee9f37f9389b21e708faffa4a65d6a3de484d22309"}, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.38b0" +requires_python = ">=3.7" +summary = "Web util for OpenTelemetry" +files = [ + {file = "opentelemetry_util_http-0.38b0-py3-none-any.whl", hash = "sha256:8e5f0451eeb5307b2c628dd799886adc5e113fb13a7207c29c672e8d168eabd8"}, + {file = "opentelemetry_util_http-0.38b0.tar.gz", hash = "sha256:85eb032b6129c4d7620583acf574e99fe2e73c33d60e256b54af436f76ceb5ae"}, +] + +[[package]] +name = "packaging" +version = "23.1" +requires_python = ">=3.7" +summary = "Core utilities for Python packages" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pandas" +version = "2.0.3" +requires_python = ">=3.8" +summary = "Powerful data structures for data analysis, time series, and statistics" +dependencies = [ + "numpy>=1.20.3; python_version < \"3.10\"", + "numpy>=1.21.0; python_version >= \"3.10\"", + "numpy>=1.23.2; python_version >= \"3.11\"", + "python-dateutil>=2.8.2", + "pytz>=2020.1", + "tzdata>=2022.1", +] +files = [ + {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"}, + {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"}, + {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"}, + {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"}, + {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"}, + {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"}, + {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"}, + {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"}, + {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"}, + {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"}, + {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"}, + {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"}, + {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"}, + {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"}, + {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"}, + {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"}, + {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"}, + {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"}, + {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"}, + {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"}, + {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"}, + {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"}, + {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"}, + {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"}, + {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, +] + +[[package]] +name = "passlib" +version = "1.7.4" +summary = "comprehensive password hashing framework supporting over 30 schemes" +files = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] + +[[package]] +name = "pendulum" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python datetimes made easy" +dependencies = [ + "backports-zoneinfo>=0.2.1; python_version < \"3.9\"", + "importlib-resources>=5.9.0; python_version < \"3.9\"", + "python-dateutil>=2.6", + "time-machine>=2.6.0; implementation_name != \"pypy\"", + "tzdata>=2020.1", +] +files = [ + {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, + {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, + {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, + {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, + {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, + {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, + {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, + {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, + {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, + {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, + {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, + {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, + {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, + {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, + {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, + {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, + {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, + {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, +] + +[[package]] +name = "pkgutil-resolve-name" +version = "1.3.10" +requires_python = ">=3.6" +summary = "Resolve a name to an object." +files = [ + {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, + {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, +] + +[[package]] +name = "platformdirs" +version = "3.8.0" +requires_python = ">=3.7" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +dependencies = [ + "typing-extensions>=4.6.3; python_version < \"3.8\"", +] +files = [ + {file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"}, + {file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"}, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +requires_python = ">=3.7" +summary = "plugin and hook calling mechanisms for python" +dependencies = [ + "importlib-metadata>=0.12; python_version < \"3.8\"", +] +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[[package]] +name = "pre-commit" +version = "3.5.0" +requires_python = ">=3.8" +summary = "A framework for managing and maintaining multi-language pre-commit hooks." +dependencies = [ + "cfgv>=2.0.0", + "identify>=1.0.0", + "nodeenv>=0.11.1", + "pyyaml>=5.1", + "virtualenv>=20.10.0", +] +files = [ + {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, + {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.39" +requires_python = ">=3.7.0" +summary = "Library for building powerful interactive command lines in Python" +dependencies = [ + "wcwidth", +] +files = [ + {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, + {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, +] + +[[package]] +name = "protobuf" +version = "4.25.0" +requires_python = ">=3.8" +summary = "" +files = [ + {file = "protobuf-4.25.0-cp310-abi3-win32.whl", hash = "sha256:5c1203ac9f50e4853b0a0bfffd32c67118ef552a33942982eeab543f5c634395"}, + {file = "protobuf-4.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:c40ff8f00aa737938c5378d461637d15c442a12275a81019cc2fef06d81c9419"}, + {file = "protobuf-4.25.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:cf21faba64cd2c9a3ed92b7a67f226296b10159dbb8fbc5e854fc90657d908e4"}, + {file = "protobuf-4.25.0-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:32ac2100b0e23412413d948c03060184d34a7c50b3e5d7524ee96ac2b10acf51"}, + {file = "protobuf-4.25.0-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:683dc44c61f2620b32ce4927de2108f3ebe8ccf2fd716e1e684e5a50da154054"}, + {file = "protobuf-4.25.0-cp38-cp38-win32.whl", hash = "sha256:1a3ba712877e6d37013cdc3476040ea1e313a6c2e1580836a94f76b3c176d575"}, + {file = "protobuf-4.25.0-cp38-cp38-win_amd64.whl", hash = "sha256:b2cf8b5d381f9378afe84618288b239e75665fe58d0f3fd5db400959274296e9"}, + {file = "protobuf-4.25.0-cp39-cp39-win32.whl", hash = "sha256:63714e79b761a37048c9701a37438aa29945cd2417a97076048232c1df07b701"}, + {file = "protobuf-4.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:d94a33db8b7ddbd0af7c467475fb9fde0c705fb315a8433c0e2020942b863a1f"}, + {file = "protobuf-4.25.0-py3-none-any.whl", hash = "sha256:1a53d6f64b00eecf53b65ff4a8c23dc95df1fa1e97bb06b8122e5a64f49fc90a"}, + {file = "protobuf-4.25.0.tar.gz", hash = "sha256:68f7caf0d4f012fd194a301420cf6aa258366144d814f358c5b32558228afa7c"}, +] + +[[package]] +name = "psycopg" +version = "3.1.17" +requires_python = ">=3.7" +summary = "PostgreSQL database adapter for Python" +dependencies = [ + "backports-zoneinfo>=0.2.0; python_version < \"3.9\"", + "typing-extensions>=4.1", + "tzdata; sys_platform == \"win32\"", +] +files = [ + {file = "psycopg-3.1.17-py3-none-any.whl", hash = "sha256:96b7b13af6d5a514118b759a66b2799a8a4aa78675fa6bb0d3f7d52d67eff002"}, + {file = "psycopg-3.1.17.tar.gz", hash = "sha256:437e7d7925459f21de570383e2e10542aceb3b9cb972ce957fdd3826ca47edc6"}, +] + +[[package]] +name = "pyasn1" +version = "0.5.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +files = [ + {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, + {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.3.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +summary = "A collection of ASN.1-based protocols modules" +dependencies = [ + "pyasn1<0.6.0,>=0.4.6", +] +files = [ + {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, + {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, +] + +[[package]] +name = "pycparser" +version = "2.21" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "C parser in Python" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pydantic" +version = "1.10.18" +requires_python = ">=3.7" +summary = "Data validation and settings management using python type hints" +dependencies = [ + "typing-extensions>=4.2.0", +] +files = [ + {file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"}, + {file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"}, + {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"}, + {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"}, + {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"}, + {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"}, + {file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"}, + {file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"}, + {file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"}, + {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"}, + {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"}, + {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"}, + {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"}, + {file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"}, + {file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"}, + {file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"}, + {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"}, + {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"}, + {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"}, + {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"}, + {file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"}, + {file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"}, + {file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"}, + {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"}, + {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"}, + {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"}, + {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"}, + {file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"}, + {file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"}, + {file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"}, + {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"}, + {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"}, + {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"}, + {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"}, + {file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"}, + {file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"}, + {file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"}, +] + +[[package]] +name = "pygments" +version = "2.15.1" +requires_python = ">=3.7" +summary = "Pygments is a syntax highlighting package written in Python." +files = [ + {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, + {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, +] + +[[package]] +name = "pylint" +version = "3.0.3" +requires_python = ">=3.8.0" +summary = "python code static checker" +dependencies = [ + "astroid<=3.1.0-dev0,>=3.0.1", + "colorama>=0.4.5; sys_platform == \"win32\"", + "dill>=0.2; python_version < \"3.11\"", + "dill>=0.3.6; python_version >= \"3.11\"", + "dill>=0.3.7; python_version >= \"3.12\"", + "isort!=5.13.0,<6,>=4.2.5", + "mccabe<0.8,>=0.6", + "platformdirs>=2.2.0", + "tomli>=1.1.0; python_version < \"3.11\"", + "tomlkit>=0.10.1", + "typing-extensions>=3.10.0; python_version < \"3.10\"", +] +files = [ + {file = "pylint-3.0.3-py3-none-any.whl", hash = "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810"}, + {file = "pylint-3.0.3.tar.gz", hash = "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b"}, +] + +[[package]] +name = "pyparsing" +version = "3.1.1" +requires_python = ">=3.6.8" +summary = "pyparsing module - Classes and methods to define and execute parsing grammars" +files = [ + {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, + {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, +] + +[[package]] +name = "pytest" +version = "7.4.0" +requires_python = ">=3.7" +summary = "pytest: simple powerful testing with Python" +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "importlib-metadata>=0.12; python_version < \"3.8\"", + "iniconfig", + "packaging", + "pluggy<2.0,>=0.12", + "tomli>=1.0.0; python_version < \"3.11\"", +] +files = [ + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, +] + +[[package]] +name = "pytest-asyncio" +version = "0.21.2" +requires_python = ">=3.7" +summary = "Pytest support for asyncio" +dependencies = [ + "pytest>=7.0.0", + "typing-extensions>=3.7.2; python_version < \"3.8\"", +] +files = [ + {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, + {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, +] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +requires_python = ">=3.7" +summary = "Pytest plugin for measuring coverage." +dependencies = [ + "coverage[toml]>=5.2.1", + "pytest>=4.6", +] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[[package]] +name = "pytest-integration" +version = "0.2.3" +requires_python = ">=3.6" +summary = "Organizing pytests by integration or not" +files = [ + {file = "pytest_integration-0.2.3-py3-none-any.whl", hash = "sha256:7f59ed1fa1cc8cb240f9495b68bc02c0421cce48589f78e49b7b842231604b12"}, + {file = "pytest_integration-0.2.3.tar.gz", hash = "sha256:b00988a5de8a6826af82d4c7a3485b43fbf32c11235e9f4a8b7225eef5fbcf65"}, +] + +[[package]] +name = "pytest-mock" +version = "3.12.0" +requires_python = ">=3.8" +summary = "Thin-wrapper around the mock package for easier use with pytest" +dependencies = [ + "pytest>=5.0", +] +files = [ + {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, + {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, +] + +[[package]] +name = "pytest-xdist" +version = "3.5.0" +requires_python = ">=3.7" +summary = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +dependencies = [ + "execnet>=1.1", + "pytest>=6.2.0", +] +files = [ + {file = "pytest-xdist-3.5.0.tar.gz", hash = "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a"}, + {file = "pytest_xdist-3.5.0-py3-none-any.whl", hash = "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"}, +] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[[package]] +name = "python-dotenv" +version = "0.21.1" +requires_python = ">=3.7" +summary = "Read key-value pairs from a .env file and set them as environment variables" +files = [ + {file = "python-dotenv-0.21.1.tar.gz", hash = "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49"}, + {file = "python_dotenv-0.21.1-py3-none-any.whl", hash = "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a"}, +] + +[[package]] +name = "python-jose" +version = "3.3.0" +summary = "JOSE implementation in Python" +dependencies = [ + "ecdsa!=0.15", + "pyasn1", + "rsa", +] +files = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] + +[[package]] +name = "python-multipart" +version = "0.0.6" +requires_python = ">=3.7" +summary = "A streaming multipart parser for Python" +files = [ + {file = "python_multipart-0.0.6-py3-none-any.whl", hash = "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18"}, + {file = "python_multipart-0.0.6.tar.gz", hash = "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132"}, +] + +[[package]] +name = "pytz" +version = "2023.3" +summary = "World timezone definitions, modern and historical" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + +[[package]] +name = "pywin32" +version = "306" +summary = "Python for Window Extensions" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +requires_python = ">=3.6" +summary = "YAML parser and emitter for Python" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "redis" +version = "4.6.0" +requires_python = ">=3.7" +summary = "Python client for Redis database and key-value store" +dependencies = [ + "async-timeout>=4.0.2; python_full_version <= \"3.11.2\"", + "importlib-metadata>=1.0; python_version < \"3.8\"", + "typing-extensions; python_version < \"3.8\"", +] +files = [ + {file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"}, + {file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"}, +] + +[[package]] +name = "referencing" +version = "0.35.1" +requires_python = ">=3.8" +summary = "JSON Referencing + Python" +dependencies = [ + "attrs>=22.2.0", + "rpds-py>=0.7.0", +] +files = [ + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, +] + +[[package]] +name = "requests" +version = "2.29.0" +requires_python = ">=3.7" +summary = "Python HTTP for Humans." +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<1.27,>=1.21.1", +] +files = [ + {file = "requests-2.29.0-py3-none-any.whl", hash = "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b"}, + {file = "requests-2.29.0.tar.gz", hash = "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059"}, +] + +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "OAuthlib authentication support for Requests." +dependencies = [ + "oauthlib>=3.0.0", + "requests>=2.0.0", +] +files = [ + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, +] + +[[package]] +name = "responses" +version = "0.23.1" +requires_python = ">=3.7" +summary = "A utility library for mocking out the `requests` Python library." +dependencies = [ + "pyyaml", + "requests<3.0,>=2.22.0", + "types-PyYAML", + "typing-extensions; python_version < \"3.8\"", + "urllib3>=1.25.10", +] +files = [ + {file = "responses-0.23.1-py3-none-any.whl", hash = "sha256:8a3a5915713483bf353b6f4079ba8b2a29029d1d1090a503c70b0dc5d9d0c7bd"}, + {file = "responses-0.23.1.tar.gz", hash = "sha256:c4d9aa9fc888188f0c673eff79a8dadbe2e75b7fe879dc80a221a06e0a68138f"}, +] + +[[package]] +name = "rich" +version = "13.7.0" +requires_python = ">=3.7.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", + "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", +] +files = [ + {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, + {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, +] + +[[package]] +name = "rpds-py" +version = "0.18.1" +requires_python = ">=3.8" +summary = "Python bindings to Rust's persistent data structures (rpds)" +files = [ + {file = "rpds_py-0.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53"}, + {file = "rpds_py-0.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d"}, + {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60"}, + {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da"}, + {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1"}, + {file = "rpds_py-0.18.1-cp310-none-win32.whl", hash = "sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333"}, + {file = "rpds_py-0.18.1-cp310-none-win_amd64.whl", hash = "sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a"}, + {file = "rpds_py-0.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6b5ff7e1d63a8281654b5e2896d7f08799378e594f09cf3674e832ecaf396ce8"}, + {file = "rpds_py-0.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8927638a4d4137a289e41d0fd631551e89fa346d6dbcfc31ad627557d03ceb6d"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:154bf5c93d79558b44e5b50cc354aa0459e518e83677791e6adb0b039b7aa6a7"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07f2139741e5deb2c5154a7b9629bc5aa48c766b643c1a6750d16f865a82c5fc"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c7672e9fba7425f79019db9945b16e308ed8bc89348c23d955c8c0540da0a07"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:489bdfe1abd0406eba6b3bb4fdc87c7fa40f1031de073d0cfb744634cc8fa261"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c20f05e8e3d4fc76875fc9cb8cf24b90a63f5a1b4c5b9273f0e8225e169b100"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:967342e045564cef76dfcf1edb700b1e20838d83b1aa02ab313e6a497cf923b8"}, + {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cc7c1a47f3a63282ab0f422d90ddac4aa3034e39fc66a559ab93041e6505da7"}, + {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f7afbfee1157e0f9376c00bb232e80a60e59ed716e3211a80cb8506550671e6e"}, + {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e6934d70dc50f9f8ea47081ceafdec09245fd9f6032669c3b45705dea096b88"}, + {file = "rpds_py-0.18.1-cp311-none-win32.whl", hash = "sha256:c69882964516dc143083d3795cb508e806b09fc3800fd0d4cddc1df6c36e76bb"}, + {file = "rpds_py-0.18.1-cp311-none-win_amd64.whl", hash = "sha256:70a838f7754483bcdc830444952fd89645569e7452e3226de4a613a4c1793fb2"}, + {file = "rpds_py-0.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3dd3cd86e1db5aadd334e011eba4e29d37a104b403e8ca24dcd6703c68ca55b3"}, + {file = "rpds_py-0.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35b2b771b13eee8729a5049c976197ff58a27a3829c018a04341bcf1ae409b2b"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee17cd26b97d537af8f33635ef38be873073d516fd425e80559f4585a7b90c43"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b646bf655b135ccf4522ed43d6902af37d3f5dbcf0da66c769a2b3938b9d8184"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19ba472b9606c36716062c023afa2484d1e4220548751bda14f725a7de17b4f6"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e30ac5e329098903262dc5bdd7e2086e0256aa762cc8b744f9e7bf2a427d3f8"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d58ad6317d188c43750cb76e9deacf6051d0f884d87dc6518e0280438648a9ac"}, + {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1735502458621921cee039c47318cb90b51d532c2766593be6207eec53e5c4c"}, + {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f5bab211605d91db0e2995a17b5c6ee5edec1270e46223e513eaa20da20076ac"}, + {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2fc24a329a717f9e2448f8cd1f960f9dac4e45b6224d60734edeb67499bab03a"}, + {file = "rpds_py-0.18.1-cp312-none-win32.whl", hash = "sha256:1805d5901779662d599d0e2e4159d8a82c0b05faa86ef9222bf974572286b2b6"}, + {file = "rpds_py-0.18.1-cp312-none-win_amd64.whl", hash = "sha256:720edcb916df872d80f80a1cc5ea9058300b97721efda8651efcd938a9c70a72"}, + {file = "rpds_py-0.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:c827576e2fa017a081346dce87d532a5310241648eb3700af9a571a6e9fc7e74"}, + {file = "rpds_py-0.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa3679e751408d75a0b4d8d26d6647b6d9326f5e35c00a7ccd82b78ef64f65f8"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0abeee75434e2ee2d142d650d1e54ac1f8b01e6e6abdde8ffd6eeac6e9c38e20"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed402d6153c5d519a0faf1bb69898e97fb31613b49da27a84a13935ea9164dfc"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:338dee44b0cef8b70fd2ef54b4e09bb1b97fc6c3a58fea5db6cc083fd9fc2724"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7750569d9526199c5b97e5a9f8d96a13300950d910cf04a861d96f4273d5b104"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:607345bd5912aacc0c5a63d45a1f73fef29e697884f7e861094e443187c02be5"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:207c82978115baa1fd8d706d720b4a4d2b0913df1c78c85ba73fe6c5804505f0"}, + {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6d1e42d2735d437e7e80bab4d78eb2e459af48c0a46e686ea35f690b93db792d"}, + {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5463c47c08630007dc0fe99fb480ea4f34a89712410592380425a9b4e1611d8e"}, + {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:06d218939e1bf2ca50e6b0ec700ffe755e5216a8230ab3e87c059ebb4ea06afc"}, + {file = "rpds_py-0.18.1-cp38-none-win32.whl", hash = "sha256:312fe69b4fe1ffbe76520a7676b1e5ac06ddf7826d764cc10265c3b53f96dbe9"}, + {file = "rpds_py-0.18.1-cp38-none-win_amd64.whl", hash = "sha256:9437ca26784120a279f3137ee080b0e717012c42921eb07861b412340f85bae2"}, + {file = "rpds_py-0.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93"}, + {file = "rpds_py-0.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26"}, + {file = "rpds_py-0.18.1-cp39-none-win32.whl", hash = "sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360"}, + {file = "rpds_py-0.18.1-cp39-none-win_amd64.whl", hash = "sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e"}, + {file = "rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f"}, +] + +[[package]] +name = "rsa" +version = "4.9" +requires_python = ">=3.6,<4" +summary = "Pure-Python RSA implementation" +dependencies = [ + "pyasn1>=0.1.3", +] +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[[package]] +name = "setuptools" +version = "68.0.0" +requires_python = ">=3.7" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.0" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.23" +requires_python = ">=3.7" +summary = "Database Abstraction Library" +dependencies = [ + "greenlet!=0.4.17; platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\"", + "importlib-metadata; python_version < \"3.8\"", + "typing-extensions>=4.2.0", +] +files = [ + {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-win32.whl", hash = "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"}, + {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, + {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, +] + +[[package]] +name = "sqlalchemy-utils" +version = "0.41.1" +requires_python = ">=3.6" +summary = "Various utility functions for SQLAlchemy." +dependencies = [ + "SQLAlchemy>=1.3", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "SQLAlchemy-Utils-0.41.1.tar.gz", hash = "sha256:a2181bff01eeb84479e38571d2c0718eb52042f9afd8c194d0d02877e84b7d74"}, + {file = "SQLAlchemy_Utils-0.41.1-py3-none-any.whl", hash = "sha256:6c96b0768ea3f15c0dc56b363d386138c562752b84f647fb8d31a2223aaab801"}, +] + +[[package]] +name = "sqlparse" +version = "0.4.4" +requires_python = ">=3.5" +summary = "A non-validating SQL parser." +files = [ + {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, + {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, +] + +[[package]] +name = "sse-starlette" +version = "1.6.5" +requires_python = ">=3.8" +summary = "\"SSE plugin for Starlette\"" +dependencies = [ + "starlette", +] +files = [ + {file = "sse-starlette-1.6.5.tar.gz", hash = "sha256:819f2c421fb37067380fe3dcaba246c476b02651b7bb7601099a378ad802a0ac"}, + {file = "sse_starlette-1.6.5-py3-none-any.whl", hash = "sha256:68b6b7eb49be0c72a2af80a055994c13afcaa4761b29226beb208f954c25a642"}, +] + +[[package]] +name = "starlette" +version = "0.37.2" +requires_python = ">=3.8" +summary = "The little ASGI library that shines." +dependencies = [ + "anyio<5,>=3.4.0", + "typing-extensions>=3.10.0; python_version < \"3.10\"", +] +files = [ + {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, + {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, +] + +[[package]] +name = "strawberry-graphql" +version = "0.204.0" +requires_python = ">=3.8,<4.0" +summary = "A library for creating GraphQL APIs" +dependencies = [ + "astunparse<2.0.0,>=1.6.3; python_version < \"3.9\"", + "graphql-core<3.3.0,>=3.2.0", + "python-dateutil<3.0.0,>=2.7.0", + "typing-extensions>=4.5.0", +] +files = [ + {file = "strawberry_graphql-0.204.0-py3-none-any.whl", hash = "sha256:e5f46abf49e7569335b51e992fb54f040355712274cf74a8bd540dd692457ccd"}, + {file = "strawberry_graphql-0.204.0.tar.gz", hash = "sha256:2c806036ecfe6d32cc0bae111346d9df24a7ca7156cc61047ce429efbb6cd630"}, +] + +[[package]] +name = "testcontainers" +version = "3.7.1" +requires_python = ">=3.7" +summary = "Library provides lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container" +dependencies = [ + "deprecation", + "docker>=4.0.0", + "wrapt", +] +files = [ + {file = "testcontainers-3.7.1-py2.py3-none-any.whl", hash = "sha256:7f48cef4bf0ccd78f1a4534d4b701a003a3bace851f24eae58a32f9e3f0aeba0"}, +] + +[[package]] +name = "time-machine" +version = "2.15.0" +requires_python = ">=3.8" +summary = "Travel through time in your tests." +dependencies = [ + "python-dateutil", +] +files = [ + {file = "time_machine-2.15.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:892d016789b59950989b2db188dcd46cf16d34e8daf2343e33b679b0c5fd1001"}, + {file = "time_machine-2.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4428bdae507996aa3fdeb4727bca09e26306fa64a502e7335207252684516cbf"}, + {file = "time_machine-2.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0302568338c8bd333ed0698231dbb781b70ead1a5579b4ac734b9bf88313229f"}, + {file = "time_machine-2.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18fc4740073e67071472c48355775ec6d1b93af5c675524b7de2474e0dcd8741"}, + {file = "time_machine-2.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:768d33b484a35da93731cc99bdc926b539240a78673216cdc6306833d9072350"}, + {file = "time_machine-2.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73a8c8160d2a170dadcad5b82fb5ee53236a19cec0996651cf4d21da0a2574d5"}, + {file = "time_machine-2.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:09fd839a321a92aa8183206c383b9725eaf4e0a28a70e4cb87db292b352eeefb"}, + {file = "time_machine-2.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:838a6d117739f1ae6ecc45ec630fa694f41a85c0d07b1f3b1db2a6cc52c1808b"}, + {file = "time_machine-2.15.0-cp310-cp310-win32.whl", hash = "sha256:d24d2ec74923b49bce7618e3e7762baa6be74e624d9829d5632321de102bf386"}, + {file = "time_machine-2.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:95c8e7036cf442480d0bf6f5fde371e1eb6dbbf5391d7bdb8db73bd8a732b538"}, + {file = "time_machine-2.15.0-cp310-cp310-win_arm64.whl", hash = "sha256:660810cd27a8a94cb5e845e8f28a95e70b01ff0c45466d394c4a0cba5a0ae279"}, + {file = "time_machine-2.15.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:674097dd54a0bbd555e7927092c74428c4c07268ad52bca38cfccc3214707e50"}, + {file = "time_machine-2.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e83fd6112808d1d14d1a57397c6fa3bd71bb2f3b8800036e12366e3680819b9"}, + {file = "time_machine-2.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b095a1de40ca1afaeae8df3f45e26b645094a1912e6e6871e725fcf06ecdb74a"}, + {file = "time_machine-2.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4601fe7a6b74c6fd9207e614d9db2a20dd4befd4d314677a0feac13a67189707"}, + {file = "time_machine-2.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245ef73f9927b7d4909d554a6a0284dbc5dee9730adea599e430b37c9e9fa203"}, + {file = "time_machine-2.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:704abc7f3403584cca9c01c5809812e0bd70632ea4251389fae4f45e11aad94f"}, + {file = "time_machine-2.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6425001e50a0c82108caed438233066cea04d42a8fc9c49bfcf081a5b96e5b4e"}, + {file = "time_machine-2.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d4073b754f90b19f28d036ec5143d3fca3a75e4d4241d78790a6178b00bb373"}, + {file = "time_machine-2.15.0-cp311-cp311-win32.whl", hash = "sha256:8817b0f7d7830215261b18db83c9c3ef1da6bb64da5c292d7c70b9a46e5a6745"}, + {file = "time_machine-2.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:ddad27a62df2ea47b7b483009fbfcf167a71d702cbd8e2eefd9ddc1c93146658"}, + {file = "time_machine-2.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f021aa2dbd8fbfe54d3fa2258518129108b7496922b3bcff2cf5991078eec67"}, + {file = "time_machine-2.15.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a22f47c34ee1fcf7d93a8c5c93135499aac879d9d5d8f820bd28571a30fdabcd"}, + {file = "time_machine-2.15.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b684f8ecdeacd6baabc17b15ac1b054ca62029193e6c5367ef00b3516671de80"}, + {file = "time_machine-2.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f7add997684bc6141e1c80f6ba0c38ffe316ba277a4074e61b1b7b4f5a172bf"}, + {file = "time_machine-2.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31af56399bf7c9ef76a3f7b6d9471dffa8f06ee373c194a374b69523f9061de9"}, + {file = "time_machine-2.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b94cba3edfc54bcb3ab5be616a2f50fa48be438e5af970824efdf882d1bc31"}, + {file = "time_machine-2.15.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3862dda89bdb05f9d521b08fdcb24b19a7dd9f559ae324f4301ba7a07b6eea64"}, + {file = "time_machine-2.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1790481a6b9ce38888f22ce30710244067898c3ac4805a0e061e381f3db3506"}, + {file = "time_machine-2.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a731c03bc00552ee6cc685a59616d36003124e7e04c6ddf65c2c47f1c3d85480"}, + {file = "time_machine-2.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e6776840aea3ff5ab6924b50117957da62db51b109b3b491c0d5817a804b1a8e"}, + {file = "time_machine-2.15.0-cp312-cp312-win32.whl", hash = "sha256:9479530e3fce65f6149058071fa4df8150025f15b43b103445f619842981a87c"}, + {file = "time_machine-2.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f3ab4185c1f72010846ca9fccb08349e23a2b52982a18d9870e848ce9f1c86"}, + {file = "time_machine-2.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:c0473dfa8f17c6a9a250b2bd6a5b62af3aa7d22518f701649115f1085d5e35ab"}, + {file = "time_machine-2.15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f50f10058b884d45cd8a50423bf561b1f9f9df7058abeb8b318700c8bcf4bb54"}, + {file = "time_machine-2.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:df6f618b98f0848fd8d07039541e10f23db679d8283f8719e870a98e1ef8e639"}, + {file = "time_machine-2.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52468a0784544eba708c0ae6bc5e8c5dcfd685495a60f7f74028662c984bd9cd"}, + {file = "time_machine-2.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c08800c28160f4d32ca510128b4e201a43c813e7a2dd53178fa79ebe050eba13"}, + {file = "time_machine-2.15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65d395211736d9844537a530287a7c64b9fda1d353e899a0e1723986a0859154"}, + {file = "time_machine-2.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b177d334a35bf2ce103bfe4e0e416e4ee824dd33386ea73fa7491c17cc61897"}, + {file = "time_machine-2.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9a6a9342fae113b12aab42c790880c549d9ba695b8deff27ee08096eedd67569"}, + {file = "time_machine-2.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bcbb25029ee8756f10c6473cea5ef21707a1d9a8752cdf29fad3a5f34aa4a313"}, + {file = "time_machine-2.15.0-cp313-cp313-win32.whl", hash = "sha256:29b988b1f09f2a083b12b6b054787b799ae91ee15bb0e9de3e48f880e4d68674"}, + {file = "time_machine-2.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:d828721dcbcb94b904a6b25df67c2513ecd24cd9e36694f38b9f0fa71c7c6103"}, + {file = "time_machine-2.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:008bd668d933b1a029c81805bcdc0132390c2545b103cf8e6709e3adbc37989d"}, + {file = "time_machine-2.15.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e99689f6c6b9ca6e2fc7a75d140e38c5a7985dab61fe1f4e506268f7e9844e05"}, + {file = "time_machine-2.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:671e88a6209a1cf415dc0f8c67d2b2d3b55b436cc63801a518f9800ebd752959"}, + {file = "time_machine-2.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b2d28daf4cabc698aafb12135525d87dc1f2f893cbd29a8a6fe0d8d36d1342c"}, + {file = "time_machine-2.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cd9f057457d12604be18b623bcd5ae7d0b917ad66cb510ee1135d5f123666e2"}, + {file = "time_machine-2.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dc6793e512a62ba9eab250134a2e67372c16ae9948e73d27c2ef355356e2e1"}, + {file = "time_machine-2.15.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0630a32e9ebcf2fac3704365b31e271fef6eabd6fedfa404cd8dbd244f7fc84d"}, + {file = "time_machine-2.15.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:617c9a92d8d8f60d5ef39e76596620503752a09f834a218e5b83be352fdd6c91"}, + {file = "time_machine-2.15.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3f7eadd820e792de33a9ec91f8178a2b9088e4e8b9a166953419ddc4ec5f7cfe"}, + {file = "time_machine-2.15.0-cp38-cp38-win32.whl", hash = "sha256:b7b647684eb2e1fd1e5e6b101249d5fe9d6117c117b5e336ad8dd75af48d2d1f"}, + {file = "time_machine-2.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b48abd7745caec1a78a16a048966cde14ff6ccb04d471a7201532648d3f77d14"}, + {file = "time_machine-2.15.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c2b1c91b437133c672e374857eccb1dd2c2d9f8477ae3b35138382d5ef19846"}, + {file = "time_machine-2.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:79bf1ef6850182e09d86e61fa31717da56014a3b2234afb025fca1f2a43ac07b"}, + {file = "time_machine-2.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:658ea8477fa020f08435fb7277635eb0b50cd5206b9d4cbe10e9a5466b01f855"}, + {file = "time_machine-2.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c947135750d20f35acac290c34f1acf5771fc166a3fbc0e3816a97c756aaa5f5"}, + {file = "time_machine-2.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dee3a0dd1866988c49a5d00564404db9bcdf49ca92f9c4e8b6c99609d64e698"}, + {file = "time_machine-2.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c596920d6017702a36e3a43fd8110a84e87d6229f30b84bd5640cbae9b5145da"}, + {file = "time_machine-2.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:014589d0edd4aa14f8d63985745565e8cbbe48461d6c004a96000b47f6b44e78"}, + {file = "time_machine-2.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5ff655716cd13a242eef8cf5d368074e8b396ff86508a5933e7cff4f2b3eb3c2"}, + {file = "time_machine-2.15.0-cp39-cp39-win32.whl", hash = "sha256:1168eebd7af7e6e3e2fd378c16ca917b97dd81c89a1f1f9e1daa985c81699d90"}, + {file = "time_machine-2.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:c344eb09fcfbf71e5b5847d4f188fec98e1c3a976125ef571eac5f1c39e7a5e5"}, + {file = "time_machine-2.15.0-cp39-cp39-win_arm64.whl", hash = "sha256:899f1a856b3bebb82b6cbc3c0014834b583b83f246b28e462a031ec1b766130b"}, + {file = "time_machine-2.15.0.tar.gz", hash = "sha256:ebd2e63baa117ded04b978813fcd1279d3fc6be2149c9cac75c716b6f1db774c"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tomlkit" +version = "0.11.8" +requires_python = ">=3.7" +summary = "Style preserving TOML library" +files = [ + {file = "tomlkit-0.11.8-py3-none-any.whl", hash = "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171"}, + {file = "tomlkit-0.11.8.tar.gz", hash = "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3"}, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +requires_python = ">=3.8" +summary = "Traitlets Python configuration system" +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + +[[package]] +name = "types-cachetools" +version = "5.3.0.6" +summary = "Typing stubs for cachetools" +files = [ + {file = "types-cachetools-5.3.0.6.tar.gz", hash = "sha256:595f0342d246c8ba534f5a762cf4c2f60ecb61e8002b8b2277fd5cf791d4e851"}, + {file = "types_cachetools-5.3.0.6-py3-none-any.whl", hash = "sha256:f7f8a25bfe306f2e6bc2ad0a2f949d9e72f2d91036d509c36d3810bf728bc6e1"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.10" +summary = "Typing stubs for PyYAML" +files = [ + {file = "types-PyYAML-6.0.12.10.tar.gz", hash = "sha256:ebab3d0700b946553724ae6ca636ea932c1b0868701d4af121630e78d695fc97"}, + {file = "types_PyYAML-6.0.12.10-py3-none-any.whl", hash = "sha256:662fa444963eff9b68120d70cda1af5a5f2aa57900003c2006d7626450eaae5f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.11.0" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +requires_python = ">=2" +summary = "Provider of IANA time zone data" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "uritemplate" +version = "4.1.1" +requires_python = ">=3.6" +summary = "Implementation of RFC 6570 URI Templates" +files = [ + {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, + {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, +] + +[[package]] +name = "urllib3" +version = "1.26.18" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +files = [ + {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, + {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, +] + +[[package]] +name = "uvicorn" +version = "0.24.0.post1" +requires_python = ">=3.8" +summary = "The lightning-fast ASGI server." +dependencies = [ + "click>=7.0", + "h11>=0.8", + "typing-extensions>=4.0; python_version < \"3.11\"", +] +files = [ + {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, + {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, +] + +[[package]] +name = "vine" +version = "5.0.0" +requires_python = ">=3.6" +summary = "Promises, promises, promises." +files = [ + {file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"}, + {file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"}, +] + +[[package]] +name = "virtualenv" +version = "20.23.1" +requires_python = ">=3.7" +summary = "Virtual Python Environment builder" +dependencies = [ + "distlib<1,>=0.3.6", + "filelock<4,>=3.12", + "importlib-metadata>=6.6; python_version < \"3.8\"", + "platformdirs<4,>=3.5.1", +] +files = [ + {file = "virtualenv-20.23.1-py3-none-any.whl", hash = "sha256:34da10f14fea9be20e0fd7f04aba9732f84e593dac291b757ce42e3368a39419"}, + {file = "virtualenv-20.23.1.tar.gz", hash = "sha256:8ff19a38c1021c742148edc4f81cb43d7f8c6816d2ede2ab72af5b84c749ade1"}, +] + +[[package]] +name = "wcwidth" +version = "0.2.6" +summary = "Measures the displayed width of unicode strings in a terminal" +dependencies = [ + "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", +] +files = [ + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, +] + +[[package]] +name = "wheel" +version = "0.41.1" +requires_python = ">=3.7" +summary = "A built-package format for Python" +files = [ + {file = "wheel-0.41.1-py3-none-any.whl", hash = "sha256:473219bd4cbedc62cea0cb309089b593e47c15c4a2531015f94e4e3b9a0f6981"}, + {file = "wheel-0.41.1.tar.gz", hash = "sha256:12b911f083e876e10c595779709f8a88a59f45aacc646492a67fe9ef796c1b47"}, +] + +[[package]] +name = "wrapt" +version = "1.15.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +summary = "Module for decorators, wrappers and monkey patching." +files = [ + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +] + +[[package]] +name = "yarl" +version = "1.9.2" +requires_python = ">=3.7" +summary = "Yet another URL library" +dependencies = [ + "idna>=2.0", + "multidict>=4.0", + "typing-extensions>=3.7.4; python_version < \"3.8\"", +] +files = [ + {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, + {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, + {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, + {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, + {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, + {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, + {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, + {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, + {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, + {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, +] + +[[package]] +name = "zipp" +version = "3.15.0" +requires_python = ">=3.7" +summary = "Backport of pathlib-compatible object wrapper for zip files" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] diff --git a/datajunction-clients/python/pyproject.toml b/datajunction-clients/python/pyproject.toml new file mode 100644 index 000000000..8bf47d813 --- /dev/null +++ b/datajunction-clients/python/pyproject.toml @@ -0,0 +1,77 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "datajunction" +dynamic = ["version"] +description = "DataJunction client library for connecting to a DataJunction server" +authors = [ + {name = "DataJunction Authors", email = "yian.shang@gmail.com"}, +] +dependencies = [ + "requests<3.0.0,>=2.28.2", + "alive-progress>=3.1.2", + "pyyaml>=6.0.1", + "rich>=13.7.0", + "pytest-xdist>=3.5.0", + "httpx>=0.27.0", +] +requires-python = ">=3.8,<4.0" +readme = "README.md" +license = {text = "MIT"} +classifiers = [ + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.optional-dependencies] +pandas = ["pandas>=2.0.2"] + +[tool.hatch.version] +path = "datajunction/__about__.py" + +[project.scripts] +dj = "datajunction.cli:main" + +[tool.pdm] +[tool.pdm.build] +includes = ["datajunction"] + +[tool.pdm.dev-dependencies] +test = [ + "pre-commit>=3.2.2", + "pylint>=2.17.3", + "pytest-asyncio>=0.21.0", + "pytest-cov>=4.0.0", + "pytest-integration>=0.2.3", + "pytest-mock>=3.10.0", + "pytest>=7.3.1", + "responses>=0.23.1", + "fastapi>=0.79.0", + "urllib3<2", + "datajunction-server @ {root:uri}/../../datajunction-server", + "namesgenerator==0.3", + "testcontainers>=3.7.1", + "greenlet>=3.0.3", + "jinja2>=3.1.4", + "nbformat>=5.10.4", +] + +[project.urls] +repository = "https://github.com/DataJunction/dj" + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.coverage.run] +source = ['datajunction/'] + +[tool.isort] +src_paths = ["datajunction/", "tests/"] +profile = 'black' diff --git a/datajunction-clients/python/setup.cfg b/datajunction-clients/python/setup.cfg new file mode 100644 index 000000000..814ec3fa1 --- /dev/null +++ b/datajunction-clients/python/setup.cfg @@ -0,0 +1,19 @@ +# This file is used to configure your project. +# Read more about the various options under: +# https://setuptools.pypa.io/en/latest/userguide/declarative_config.html +# https://setuptools.pypa.io/en/latest/references/keywords.html + +[tool:pytest] +# Specify command line options as you would do when invoking pytest directly. +# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml +# in order to write a coverage file that can be read by Jenkins. +# CAUTION: --cov flags may prohibit setting breakpoints while debugging. +# Comment those flags to avoid this pytest issue. +addopts = + --cov datajunction --cov-report term-missing + --verbose +norecursedirs = + dist + build + .tox +testpaths = tests diff --git a/tests/__init__.py b/datajunction-clients/python/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to datajunction-clients/python/tests/__init__.py diff --git a/datajunction-clients/python/tests/conftest.py b/datajunction-clients/python/tests/conftest.py new file mode 100644 index 000000000..920870699 --- /dev/null +++ b/datajunction-clients/python/tests/conftest.py @@ -0,0 +1,380 @@ +""" +Fixtures for testing DJ client. +""" +# pylint: disable=redefined-outer-name, invalid-name, W0611 +import asyncio +import os +from http.client import HTTPException +from pathlib import Path +from typing import AsyncGenerator, Dict, Iterator, List, Optional +from unittest.mock import MagicMock + +import pytest +import pytest_asyncio +from cachelib import SimpleCache +from datajunction_server.api.main import app +from datajunction_server.config import Settings +from datajunction_server.database.base import Base +from datajunction_server.database.column import Column +from datajunction_server.database.engine import Engine +from datajunction_server.models.materialization import MaterializationInfo +from datajunction_server.models.query import QueryCreate, QueryWithResults +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.typing import QueryState +from datajunction_server.utils import ( + get_query_service_client, + get_session, + get_settings, +) +from fastapi import Request +from httpx import AsyncClient +from pytest_mock import MockerFixture +from sqlalchemy import create_engine +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.orm import Session +from sqlalchemy.pool import StaticPool +from starlette.testclient import TestClient +from testcontainers.core.waiting_utils import wait_for_logs +from testcontainers.postgres import PostgresContainer + +from datajunction import DJBuilder +from tests.examples import COLUMN_MAPPINGS, EXAMPLES, QUERY_DATA_MAPPINGS + + +def post_and_raise_if_error(server: TestClient, endpoint: str, json: dict): + """ + Post the payload to the client and raise if there's an error + """ + response = server.post(endpoint, json=json) + if not response.status_code < 400: + raise HTTPException(response.text) + + +@pytest.fixture +def change_to_project_dir(request): + """ + Returns a function that changes to a specified project directory + only for a single test. At the end of the test, this will change back + to the tests directory to prevent any side-effects. + """ + + def _change_to_project_dir(project: str): + """ + Changes to the directory for a specific example project + """ + os.chdir(os.path.join(request.fspath.dirname, "examples", project)) + + try: + yield _change_to_project_dir + finally: + os.chdir(request.config.invocation_params.dir) + + +@pytest.fixture +def change_to_package_root_dir(request): + """ + Changes to the datajunction package root dir only for a single test + At the end of the test, this will change back + to the tests directory to prevent any side-effects. + """ + try: + os.chdir(Path(request.fspath.dirname).parent) + finally: + os.chdir(request.config.invocation_params.dir) + + +def pytest_addoption(parser): + """ + Add flags + """ + parser.addoption( + "--integration", + action="store_true", + dest="integration", + default=False, + help="Run integration tests", + ) + + +# +# Module scope fixtures +# +@pytest.fixture(scope="module") +def module__settings(module_mocker: MockerFixture) -> Iterator[Settings]: + """ + Custom settings for unit tests. + """ + settings = Settings( + index="sqlite://", + repository="/path/to/repository", + results_backend=SimpleCache(default_timeout=0), + celery_broker=None, + redis_cache=None, + query_service=None, + secret="a-fake-secretkey", + ) + + module_mocker.patch( + "datajunction_server.utils.get_settings", + return_value=settings, + ) + + yield settings + + +@pytest.fixture(scope="module") +def event_loop(): + """ + This fixture is OK because we are pinning the pytest_asyncio to 0.21.x. + When they fix https://github.com/pytest-dev/pytest-asyncio/issues/718 + we can remove the pytest_asyncio pin and remove this fixture. + """ + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + yield loop + loop.close() + + +@pytest_asyncio.fixture(scope="module") +async def module__session( + module__postgres_container: PostgresContainer, +) -> AsyncGenerator[AsyncSession, None]: + """ + Create a Postgres session to test models. + """ + engine = create_async_engine( + url=module__postgres_container.get_connection_url(), + poolclass=StaticPool, + ) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + async_session_factory = async_sessionmaker( + bind=engine, + autocommit=False, + expire_on_commit=False, + ) + async with async_session_factory() as session: + yield session + + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) + + # for AsyncEngine created in function scope, close and + # clean-up pooled connections + await engine.dispose() + + +@pytest.fixture(scope="module") +def module__query_service_client( + module_mocker: MockerFixture, +) -> Iterator[QueryServiceClient]: + """ + Custom settings for unit tests. + """ + qs_client = QueryServiceClient(uri="query_service:8001") + qs_client.query_state = QueryState.RUNNING # type: ignore + + def mock_get_columns_for_table( + catalog: str, + schema: str, + table: str, + engine: Optional[Engine] = None, # pylint: disable=unused-argument + request_headers: Optional[ # pylint: disable=unused-argument + Dict[str, str] + ] = None, + ) -> List[Column]: + return COLUMN_MAPPINGS[f"{catalog}.{schema}.{table}"] + + module_mocker.patch.object( + qs_client, + "get_columns_for_table", + mock_get_columns_for_table, + ) + + def mock_submit_query( + query_create: QueryCreate, + request_headers: Optional[ # pylint: disable=unused-argument + Dict[str, str] + ] = None, + ) -> QueryWithResults: + results = QUERY_DATA_MAPPINGS[ + query_create.submitted_query.strip() + .replace('"', "") + .replace("\n", "") + .replace(" ", "") + ] + if isinstance(results, Exception): + raise results + + if results.state not in (QueryState.FAILED,): + results.state = qs_client.query_state # type: ignore + qs_client.query_state = QueryState.FINISHED # type: ignore + return results + + module_mocker.patch.object( + qs_client, + "submit_query", + mock_submit_query, + ) + + def mock_create_view( + view_name: str, + query_create: QueryCreate, # pylint: disable=unused-argument + request_headers: Optional[ # pylint: disable=unused-argument + Dict[str, str] + ] = None, + ) -> str: + return f"View {view_name} created successfully." + + module_mocker.patch.object( + qs_client, + "create_view", + mock_create_view, + ) + + mock_materialize = MagicMock() + mock_materialize.return_value = MaterializationInfo( + urls=["http://fake.url/job"], + output_tables=["common.a", "common.b"], + ) + module_mocker.patch.object( + qs_client, + "materialize", + mock_materialize, + ) + + mock_deactivate_materialization = MagicMock() + mock_deactivate_materialization.return_value = MaterializationInfo( + urls=["http://fake.url/job"], + output_tables=[], + ) + module_mocker.patch.object( + qs_client, + "deactivate_materialization", + mock_deactivate_materialization, + ) + + mock_get_materialization_info = MagicMock() + mock_get_materialization_info.return_value = MaterializationInfo( + urls=["http://fake.url/job"], + output_tables=["common.a", "common.b"], + ) + module_mocker.patch.object( + qs_client, + "get_materialization_info", + mock_get_materialization_info, + ) + yield qs_client + + +@pytest.fixture(scope="module") +def module__server( # pylint: disable=too-many-statements + module__session: AsyncSession, + module__settings: Settings, + module__query_service_client: QueryServiceClient, + module_mocker, +) -> Iterator[TestClient]: + """ + Create a mock server for testing APIs that contains a mock query service. + """ + + def get_query_service_client_override( + request: Request = None, # pylint: disable=unused-argument + ) -> QueryServiceClient: + return module__query_service_client + + async def get_session_override() -> AsyncSession: + return module__session + + def get_settings_override() -> Settings: + return module__settings + + module_mocker.patch( + "datajunction_server.api.materializations.get_query_service_client", + get_query_service_client_override, + ) + + app.dependency_overrides[get_session] = get_session_override + app.dependency_overrides[get_settings] = get_settings_override + app.dependency_overrides[ + get_query_service_client + ] = get_query_service_client_override + + with TestClient(app) as test_client: + + test_client.post( + "/basic/user/", + data={ + "email": "dj@datajunction.io", + "username": "datajunction", + "password": "datajunction", + }, + ) + test_client.post( + "/basic/login/", + data={ + "username": "datajunction", + "password": "datajunction", + }, + ) + yield test_client + + app.dependency_overrides.clear() + + +@pytest.fixture(scope="module") +def module__session_with_examples(module__server: TestClient) -> TestClient: + """ + load examples + """ + for endpoint, json in EXAMPLES: + post_and_raise_if_error(server=module__server, endpoint=endpoint, json=json) # type: ignore + return module__server + + +@pytest.fixture(scope="module") +def module__postgres_container(request) -> PostgresContainer: + """ + Setup postgres container + """ + postgres = PostgresContainer( + image="postgres:latest", + user="dj", + password="dj", + dbname=request.module.__name__, + port=5432, + driver="psycopg", + ) + with postgres: + wait_for_logs( + postgres, + r"UTC \[1\] LOG: database system is ready to accept connections", + 10, + ) + yield postgres + + +@pytest.fixture(scope="module") +def builder_client(module__session_with_examples: TestClient): + """ + Returns a DJ client instance + """ + client = DJBuilder(requests_session=module__session_with_examples) # type: ignore + client.create_user( + email="dj@datajunction.io", + username="datajunction", + password="datajunction", + ) + client.basic_login( + username="datajunction", + password="datajunction", + ) + client.create_tag( + name="system-tag", + description="some system tag", + tag_type="system", + tag_metadata={}, + ) + return client diff --git a/datajunction-clients/python/tests/examples.py b/datajunction-clients/python/tests/examples.py new file mode 100644 index 000000000..60d965e5d --- /dev/null +++ b/datajunction-clients/python/tests/examples.py @@ -0,0 +1,1381 @@ +""" +Roads database examples loaded into DJ test session +""" +from typing import Dict, Union + +from datajunction_server.database.column import Column +from datajunction_server.errors import DJException, DJQueryServiceClientException +from datajunction_server.models.query import QueryWithResults +from datajunction_server.sql.parsing.types import IntegerType, StringType, TimestampType +from datajunction_server.typing import QueryState + +# pylint: disable=too-many-lines + +EXAMPLES = ( # type: ignore + ( + "/catalogs/", + {"name": "draft"}, + ), + ( + "/catalogs/", + {"name": "default"}, + ), + ( + "/engines/", + {"name": "spark", "version": "3.1.1", "dialect": "spark"}, + ), + ( + "/catalogs/default/engines/", + [{"name": "spark", "version": "3.1.1", "dialect": "spark"}], + ), + ( + "/catalogs/", + {"name": "public"}, + ), + ( + "/engines/", + {"name": "postgres", "version": "15.2"}, + ), + ( + "/catalogs/public/engines/", + [{"name": "postgres", "version": "15.2"}], + ), + ( # DJ must be primed with a "default" namespace + "/namespaces/default/", + {}, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "municipality_id", "type": "string"}, + {"name": "hard_hat_id", "type": "int"}, + {"name": "order_date", "type": "timestamp"}, + {"name": "required_date", "type": "timestamp"}, + {"name": "dispatched_date", "type": "timestamp"}, + {"name": "dispatcher_id", "type": "int"}, + ], + "description": "All repair orders", + "mode": "published", + "name": "default.repair_orders", + "catalog": "default", + "schema_": "roads", + "table": "repair_orders", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "municipality_id", "type": "string"}, + {"name": "hard_hat_id", "type": "int"}, + {"name": "order_date", "type": "timestamp"}, + {"name": "required_date", "type": "timestamp"}, + {"name": "dispatched_date", "type": "timestamp"}, + {"name": "dispatcher_id", "type": "int"}, + ], + "description": "All repair orders", + "mode": "published", + "name": "default.repair_orders_foo", + "catalog": "default", + "schema_": "roads", + "table": "repair_orders", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "repair_type_id", "type": "int"}, + {"name": "price", "type": "float"}, + {"name": "quantity", "type": "int"}, + {"name": "discount", "type": "float"}, + ], + "description": "Details on repair orders", + "mode": "published", + "name": "default.repair_order_details", + "catalog": "default", + "schema_": "roads", + "table": "repair_order_details", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_type_id", "type": "int"}, + {"name": "repair_type_name", "type": "string"}, + {"name": "contractor_id", "type": "int"}, + ], + "description": "Information on types of repairs", + "mode": "published", + "name": "default.repair_type", + "catalog": "default", + "schema_": "roads", + "table": "repair_type", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "contractor_id", "type": "int"}, + {"name": "company_name", "type": "string"}, + {"name": "contact_name", "type": "string"}, + {"name": "contact_title", "type": "string"}, + {"name": "address", "type": "string"}, + {"name": "city", "type": "string"}, + {"name": "state", "type": "string"}, + {"name": "postal_code", "type": "string"}, + {"name": "country", "type": "string"}, + {"name": "phone", "type": "string"}, + ], + "description": "Information on contractors", + "mode": "published", + "name": "default.contractors", + "catalog": "default", + "schema_": "roads", + "table": "contractors", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "municipality_id", "type": "string"}, + {"name": "municipality_type_id", "type": "string"}, + ], + "description": "Lookup table for municipality and municipality types", + "mode": "published", + "name": "default.municipality_municipality_type", + "catalog": "default", + "schema_": "roads", + "table": "municipality_municipality_type", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "municipality_type_id", "type": "string"}, + {"name": "municipality_type_desc", "type": "string"}, + ], + "description": "Information on municipality types", + "mode": "published", + "name": "default.municipality_type", + "catalog": "default", + "schema_": "roads", + "table": "municipality_type", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "municipality_id", "type": "string"}, + {"name": "contact_name", "type": "string"}, + {"name": "contact_title", "type": "string"}, + {"name": "local_region", "type": "string"}, + {"name": "phone", "type": "string"}, + {"name": "state_id", "type": "int"}, + ], + "description": "Information on municipalities", + "mode": "published", + "name": "default.municipality", + "catalog": "default", + "schema_": "roads", + "table": "municipality", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "dispatcher_id", "type": "int"}, + {"name": "company_name", "type": "string"}, + {"name": "phone", "type": "string"}, + ], + "description": "Information on dispatchers", + "mode": "published", + "name": "default.dispatchers", + "catalog": "default", + "schema_": "roads", + "table": "dispatchers", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "hard_hat_id", "type": "int"}, + {"name": "last_name", "type": "string"}, + {"name": "first_name", "type": "string"}, + {"name": "title", "type": "string"}, + {"name": "birth_date", "type": "timestamp"}, + {"name": "hire_date", "type": "timestamp"}, + {"name": "address", "type": "string"}, + {"name": "city", "type": "string"}, + {"name": "state", "type": "string"}, + {"name": "postal_code", "type": "string"}, + {"name": "country", "type": "string"}, + {"name": "manager", "type": "int"}, + {"name": "contractor_id", "type": "int"}, + ], + "description": "Information on employees", + "mode": "published", + "name": "default.hard_hats", + "catalog": "default", + "schema_": "roads", + "table": "hard_hats", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "hard_hat_id", "type": "int"}, + {"name": "state_id", "type": "string"}, + ], + "description": "Lookup table for employee's current state", + "mode": "published", + "name": "default.hard_hat_state", + "catalog": "default", + "schema_": "roads", + "table": "hard_hat_state", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "state_id", "type": "int"}, + {"name": "state_name", "type": "string"}, + {"name": "state_abbr", "type": "string"}, + {"name": "state_region", "type": "int"}, + ], + "description": "Information on different types of repairs", + "mode": "published", + "name": "default.us_states", + "catalog": "default", + "schema_": "roads", + "table": "us_states", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "us_region_id", "type": "int"}, + {"name": "us_region_description", "type": "string"}, + ], + "description": "Information on US regions", + "mode": "published", + "name": "default.us_region", + "catalog": "default", + "schema_": "roads", + "table": "us_region", + }, + ), + ( + "/nodes/dimension/", + { + "description": "Repair order dimension", + "query": """ + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM default.repair_orders + """, + "mode": "published", + "name": "default.repair_order", + "primary_key": ["repair_order_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Contractor dimension", + "query": """ + SELECT + contractor_id, + company_name, + contact_name, + contact_title, + address, + city, + state, + postal_code, + country, + phone + FROM default.contractors + """, + "mode": "published", + "name": "default.contractor", + "primary_key": ["contractor_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Hard hat dimension", + "query": """ + SELECT + hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id + FROM default.hard_hats + """, + "mode": "published", + "name": "default.hard_hat", + "primary_key": ["hard_hat_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Hard hat dimension", + "query": """ + SELECT + hh.hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id, + hhs.state_id AS state_id + FROM default.hard_hats hh + LEFT JOIN default.hard_hat_state hhs + ON hh.hard_hat_id = hhs.hard_hat_id + WHERE hh.state_id = 'NY' + """, + "mode": "published", + "name": "default.local_hard_hats", + "primary_key": ["hard_hat_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "US state dimension", + "query": """ + SELECT + state_id, + state_name, + state_abbr AS state_short, + state_region, + r.us_region_description AS state_region_description + FROM default.us_states s + LEFT JOIN default.us_region r + ON s.state_region = r.us_region_id + """, + "mode": "published", + "name": "default.us_state", + "primary_key": ["state_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Dispatcher dimension", + "query": """ + SELECT + dispatcher_id, + company_name, + phone + FROM default.dispatchers + """, + "mode": "published", + "name": "default.dispatcher", + "primary_key": ["dispatcher_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Municipality dimension", + "query": """ + SELECT + m.municipality_id AS municipality_id, + contact_name, + contact_title, + local_region, + state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM default.municipality AS m + LEFT JOIN default.municipality_municipality_type AS mmt + ON m.municipality_id = mmt.municipality_id + LEFT JOIN default.municipality_type AS mt + ON mmt.municipality_type_id = mt.municipality_type_desc + """, + "mode": "published", + "name": "default.municipality_dim", + "primary_key": ["municipality_id"], + }, + ), + ( + "/nodes/metric/", + { + "description": "Number of repair orders", + "query": ("SELECT count(repair_order_id) " "FROM default.repair_orders"), + "mode": "published", + "name": "default.num_repair_orders", + }, + ), + ( + "/nodes/metric/", + { + "description": "Average repair price", + "query": ( + "SELECT avg(price) as default_DOT_avg_repair_price " + "FROM default.repair_order_details" + ), + "mode": "published", + "name": "default.avg_repair_price", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total repair cost", + "query": ( + "SELECT sum(price) as default_DOT_total_repair_cost " + "FROM default.repair_order_details" + ), + "mode": "published", + "name": "default.total_repair_cost", + }, + ), + ( + "/nodes/metric/", + { + "description": "Average length of employment", + "query": ( + "SELECT avg(NOW() - hire_date) as default_DOT_avg_length_of_employment " + "FROM default.hard_hats" + ), + "mode": "published", + "name": "default.avg_length_of_employment", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total repair order discounts", + "query": ( + "SELECT sum(price * discount) " "FROM default.repair_order_details" + ), + "mode": "published", + "name": "default.total_repair_order_discounts", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total repair order discounts", + "query": ( + "SELECT avg(price * discount) " "FROM default.repair_order_details" + ), + "mode": "published", + "name": "default.avg_repair_order_discounts", + }, + ), + ( + "/nodes/metric/", + { + "description": "Average time to dispatch a repair order", + "query": ( + "SELECT avg(dispatched_date - order_date) " "FROM default.repair_orders" + ), + "mode": "published", + "name": "default.avg_time_to_dispatch", + }, + ), + ( + ( + "/nodes/default.repair_order_details/columns/repair_order_id/" + "?dimension=default.repair_order&dimension_column=repair_order_id" + ), + {}, + ), + ( + ( + "/nodes/default.repair_orders/columns/municipality_id/" + "?dimension=default.municipality_dim&dimension_column=municipality_id" + ), + {}, + ), + ( + ( + "/nodes/default.repair_type/columns/contractor_id/" + "?dimension=default.contractor&dimension_column=contractor_id" + ), + {}, + ), + ( + ( + "/nodes/default.repair_orders/columns/hard_hat_id/" + "?dimension=default.hard_hat&dimension_column=hard_hat_id" + ), + {}, + ), + ( + ( + "/nodes/default.repair_orders/columns/dispatcher_id/" + "?dimension=default.dispatcher&dimension_column=dispatcher_id" + ), + {}, + ), + ( + ( + "/nodes/default.hard_hat/columns/state/" + "?dimension=default.us_state&dimension_column=state_short" + ), + {}, + ), + ( + ( + "/nodes/default.repair_order_details/columns/repair_order_id/" + "?dimension=default.repair_order&dimension_column=repair_order_id" + ), + {}, + ), + ( + ( + "/nodes/default.repair_order/columns/dispatcher_id/" + "?dimension=default.dispatcher&dimension_column=dispatcher_id" + ), + {}, + ), + ( + ( + "/nodes/default.repair_order/columns/hard_hat_id/" + "?dimension=default.hard_hat&dimension_column=hard_hat_id" + ), + {}, + ), + ( + ( + "/nodes/default.repair_order/columns/municipality_id/" + "?dimension=default.municipality_dim&dimension_column=municipality_id" + ), + {}, + ), + ( # foo.bar Namespaced copy of roads database example + "/namespaces/foo.bar/", + {}, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "municipality_id", "type": "string"}, + {"name": "hard_hat_id", "type": "int"}, + {"name": "order_date", "type": "timestamp"}, + {"name": "required_date", "type": "timestamp"}, + {"name": "dispatched_date", "type": "timestamp"}, + {"name": "dispatcher_id", "type": "int"}, + ], + "description": "All repair orders", + "mode": "published", + "name": "foo.bar.repair_orders", + "catalog": "default", + "schema_": "roads", + "table": "repair_orders", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "repair_type_id", "type": "int"}, + {"name": "price", "type": "float"}, + {"name": "quantity", "type": "int"}, + {"name": "discount", "type": "float"}, + ], + "description": "Details on repair orders", + "mode": "published", + "name": "foo.bar.repair_order_details", + "catalog": "default", + "schema_": "roads", + "table": "repair_order_details", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_type_id", "type": "int"}, + {"name": "repair_type_name", "type": "string"}, + {"name": "contractor_id", "type": "int"}, + ], + "description": "Information on types of repairs", + "mode": "published", + "name": "foo.bar.repair_type", + "catalog": "default", + "schema_": "roads", + "table": "repair_type", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "contractor_id", "type": "int"}, + {"name": "company_name", "type": "string"}, + {"name": "contact_name", "type": "string"}, + {"name": "contact_title", "type": "string"}, + {"name": "address", "type": "string"}, + {"name": "city", "type": "string"}, + {"name": "state", "type": "string"}, + {"name": "postal_code", "type": "string"}, + {"name": "country", "type": "string"}, + {"name": "phone", "type": "string"}, + ], + "description": "Information on contractors", + "mode": "published", + "name": "foo.bar.contractors", + "catalog": "default", + "schema_": "roads", + "table": "contractors", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "municipality_id", "type": "string"}, + {"name": "municipality_type_id", "type": "string"}, + ], + "description": "Lookup table for municipality and municipality types", + "mode": "published", + "name": "foo.bar.municipality_municipality_type", + "catalog": "default", + "schema_": "roads", + "table": "municipality_municipality_type", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "municipality_type_id", "type": "string"}, + {"name": "municipality_type_desc", "type": "string"}, + ], + "description": "Information on municipality types", + "mode": "published", + "name": "foo.bar.municipality_type", + "catalog": "default", + "schema_": "roads", + "table": "municipality_type", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "municipality_id", "type": "string"}, + {"name": "contact_name", "type": "string"}, + {"name": "contact_title", "type": "string"}, + {"name": "local_region", "type": "string"}, + {"name": "phone", "type": "string"}, + {"name": "state_id", "type": "int"}, + ], + "description": "Information on municipalities", + "mode": "published", + "name": "foo.bar.municipality", + "catalog": "default", + "schema_": "roads", + "table": "municipality", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "dispatcher_id", "type": "int"}, + {"name": "company_name", "type": "string"}, + {"name": "phone", "type": "string"}, + ], + "description": "Information on dispatchers", + "mode": "published", + "name": "foo.bar.dispatchers", + "catalog": "default", + "schema_": "roads", + "table": "dispatchers", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "hard_hat_id", "type": "int"}, + {"name": "last_name", "type": "string"}, + {"name": "first_name", "type": "string"}, + {"name": "title", "type": "string"}, + {"name": "birth_date", "type": "timestamp"}, + {"name": "hire_date", "type": "timestamp"}, + {"name": "address", "type": "string"}, + {"name": "city", "type": "string"}, + {"name": "state", "type": "string"}, + {"name": "postal_code", "type": "string"}, + {"name": "country", "type": "string"}, + {"name": "manager", "type": "int"}, + {"name": "contractor_id", "type": "int"}, + ], + "description": "Information on employees", + "mode": "published", + "name": "foo.bar.hard_hats", + "catalog": "default", + "schema_": "roads", + "table": "hard_hats", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "hard_hat_id", "type": "int"}, + {"name": "state_id", "type": "string"}, + ], + "description": "Lookup table for employee's current state", + "mode": "published", + "name": "foo.bar.hard_hat_state", + "catalog": "default", + "schema_": "roads", + "table": "hard_hat_state", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "state_id", "type": "int"}, + {"name": "state_name", "type": "string"}, + {"name": "state_abbr", "type": "string"}, + {"name": "state_region", "type": "int"}, + ], + "description": "Information on different types of repairs", + "mode": "published", + "name": "foo.bar.us_states", + "catalog": "default", + "schema_": "roads", + "table": "us_states", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "us_region_id", "type": "int"}, + {"name": "us_region_description", "type": "string"}, + ], + "description": "Information on US regions", + "mode": "published", + "name": "foo.bar.us_region", + "catalog": "default", + "schema_": "roads", + "table": "us_region", + }, + ), + ( + "/nodes/dimension/", + { + "description": "Repair order dimension", + "query": """ + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM foo.bar.repair_orders + """, + "mode": "published", + "name": "foo.bar.repair_order", + "primary_key": ["repair_order_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Contractor dimension", + "query": """ + SELECT + contractor_id, + company_name, + contact_name, + contact_title, + address, + city, + state, + postal_code, + country, + phone + FROM foo.bar.contractors + """, + "mode": "published", + "name": "foo.bar.contractor", + "primary_key": ["contractor_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Hard hat dimension", + "query": """ + SELECT + hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id + FROM foo.bar.hard_hats + """, + "mode": "published", + "name": "foo.bar.hard_hat", + "primary_key": ["hard_hat_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Hard hat dimension", + "query": """ + SELECT + hh.hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id, + hhs.state_id AS state_id + FROM foo.bar.hard_hats hh + LEFT JOIN foo.bar.hard_hat_state hhs + ON hh.hard_hat_id = hhs.hard_hat_id + WHERE hh.state_id = 'NY' + """, + "mode": "published", + "name": "foo.bar.local_hard_hats", + "primary_key": ["hard_hat_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "US state dimension", + "query": """ + SELECT + state_id, + state_name, + state_abbr, + state_region, + r.us_region_description AS state_region_description + FROM foo.bar.us_states s + LEFT JOIN foo.bar.us_region r + ON s.state_region = r.us_region_id + """, + "mode": "published", + "name": "foo.bar.us_state", + "primary_key": ["state_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Dispatcher dimension", + "query": """ + SELECT + dispatcher_id, + company_name, + phone + FROM foo.bar.dispatchers + """, + "mode": "published", + "name": "foo.bar.dispatcher", + "primary_key": ["dispatcher_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Municipality dimension", + "query": """ + SELECT + m.municipality_id AS municipality_id, + contact_name, + contact_title, + local_region, + state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM foo.bar.municipality AS m + LEFT JOIN foo.bar.municipality_municipality_type AS mmt + ON m.municipality_id = mmt.municipality_id + LEFT JOIN foo.bar.municipality_type AS mt + ON mmt.municipality_type_id = mt.municipality_type_desc + """, + "mode": "published", + "name": "foo.bar.municipality_dim", + "primary_key": ["municipality_id"], + }, + ), + ( + "/nodes/metric/", + { + "description": "Number of repair orders", + "query": ("SELECT count(repair_order_id) " "FROM foo.bar.repair_orders"), + "mode": "published", + "name": "foo.bar.num_repair_orders", + }, + ), + ( + "/nodes/metric/", + { + "description": "Average repair price", + "query": "SELECT avg(price) FROM foo.bar.repair_order_details", + "mode": "published", + "name": "foo.bar.avg_repair_price", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total repair cost", + "query": "SELECT sum(price) FROM foo.bar.repair_order_details", + "mode": "published", + "name": "foo.bar.total_repair_cost", + }, + ), + ( + "/nodes/metric/", + { + "description": "Average length of employment", + "query": ("SELECT avg(NOW() - hire_date) " "FROM foo.bar.hard_hats"), + "mode": "published", + "name": "foo.bar.avg_length_of_employment", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total repair order discounts", + "query": ( + "SELECT sum(price * discount) " "FROM foo.bar.repair_order_details" + ), + "mode": "published", + "name": "foo.bar.total_repair_order_discounts", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total repair order discounts", + "query": ( + "SELECT avg(price * discount) " "FROM foo.bar.repair_order_details" + ), + "mode": "published", + "name": "foo.bar.avg_repair_order_discounts", + }, + ), + ( + "/nodes/metric/", + { + "description": "Average time to dispatch a repair order", + "query": ( + "SELECT avg(dispatched_date - order_date) " "FROM foo.bar.repair_orders" + ), + "mode": "published", + "name": "foo.bar.avg_time_to_dispatch", + }, + ), + ( + ( + "/nodes/foo.bar.repair_order_details/columns/repair_order_id/" + "?dimension=foo.bar.repair_order&dimension_column=repair_order_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_orders/columns/municipality_id/" + "?dimension=foo.bar.municipality_dim&dimension_column=municipality_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_type/columns/contractor_id/" + "?dimension=foo.bar.contractor&dimension_column=contractor_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_orders/columns/hard_hat_id/" + "?dimension=foo.bar.hard_hat&dimension_column=hard_hat_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_orders/columns/dispatcher_id/" + "?dimension=foo.bar.dispatcher&dimension_column=dispatcher_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_order_details/columns/repair_order_id/" + "?dimension=foo.bar.repair_order&dimension_column=repair_order_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_order/columns/dispatcher_id/" + "?dimension=foo.bar.dispatcher&dimension_column=dispatcher_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_order/columns/hard_hat_id/" + "?dimension=foo.bar.hard_hat&dimension_column=hard_hat_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_order/columns/municipality_id/" + "?dimension=foo.bar.municipality_dim&dimension_column=municipality_id" + ), + {}, + ), + ( + "/nodes/cube/", + { + "description": "Cube #1 for metrics and dimensions.", + "mode": "published", + "name": "foo.bar.cube_one", + "metrics": ["foo.bar.num_repair_orders"], + "dimensions": ["foo.bar.municipality_dim.local_region"], + }, + ), + ( + "/nodes/cube/", + { + "description": "Cube #2 for metrics and dimensions.", + "mode": "published", + "name": "default.cube_two", + "metrics": ["default.num_repair_orders"], + "dimensions": ["default.municipality_dim.local_region"], + }, + ), + ( + "/nodes/transform/", + { + "description": "3 columns from default.repair_orders", + "query": ( + "SELECT repair_order_id, municipality_id, hard_hat_id " + "FROM default.repair_orders" + ), + "mode": "published", + "name": "default.repair_orders_thin", + }, + ), + ( + "/nodes/transform/", + { + "description": "3 columns from foo.bar.repair_orders", + "query": ( + "SELECT repair_order_id, municipality_id, hard_hat_id " + "FROM foo.bar.repair_orders" + ), + "mode": "published", + "name": "foo.bar.repair_orders_thin", + }, + ), + ( + "/nodes/transform/", + { + "description": "node with custom metadata", + "query": ( + "SELECT repair_order_id, municipality_id, hard_hat_id " + "FROM foo.bar.repair_orders" + ), + "mode": "published", + "name": "foo.bar.with_custom_metadata", + "custom_metadata": {"foo": "bar"}, + }, + ), +) + +COLUMN_MAPPINGS = { + "default.roads.repair_orders": [ + Column(name="id", type=IntegerType()), + Column(name="user_id", type=IntegerType()), + Column(name="timestamp", type=TimestampType()), + Column(name="text", type=StringType()), + ], + "default.store.comments": [ + Column(name="id", type=IntegerType()), + Column(name="user_id", type=IntegerType()), + Column(name="timestamp", type=TimestampType()), + Column(name="text", type=StringType()), + ], + "default.store.comments_view": [ + Column(name="id", type=IntegerType()), + Column(name="user_id", type=IntegerType()), + Column(name="timestamp", type=TimestampType()), + Column(name="text", type=StringType()), + ], +} + +QUERY_DATA_MAPPINGS: Dict[str, Union[DJException, QueryWithResults]] = { + "WITHdefault_DOT_repair_order_detailsAS(SELECTdefault_DOT_repair_order_details." + "repair_order_id,\tdefault_DOT_repair_order_details.repair_type_id,\tdefault_DOT" + "_repair_order_details.price,\tdefault_DOT_repair_order_details.quantity,\tdefault" + "_DOT_repair_order_details.discountFROMroads.repair_order_detailsASdefault_DOT_" + "repair_order_details),default_DOT_repair_orderAS(SELECTdefault_DOT_repair_orders" + ".repair_order_id,\tdefault_DOT_repair_orders.municipality_id,\tdefault_DOT_repair" + "_orders.hard_hat_id,\tdefault_DOT_repair_orders.order_date,\tdefault_DOT_repair_" + "orders.required_date,\tdefault_DOT_repair_orders.dispatched_date,\tdefault_DOT_" + "repair_orders.dispatcher_idFROMroads.repair_ordersASdefault_DOT_repair_orders)," + "default_DOT_hard_hatAS(SELECTdefault_DOT_hard_hats.hard_hat_id,\tdefault_DOT_hard" + "_hats.last_name,\tdefault_DOT_hard_hats.first_name,\tdefault_DOT_hard_hats.title," + "\tdefault_DOT_hard_hats.birth_date,\tdefault_DOT_hard_hats.hire_date,\tdefault_" + "DOT_hard_hats.address,\tdefault_DOT_hard_hats.city,\tdefault_DOT_hard_hats.state," + "\tdefault_DOT_hard_hats.postal_code,\tdefault_DOT_hard_hats.country,\tdefault_DOT" + "_hard_hats.manager,\tdefault_DOT_hard_hats.contractor_idFROMroads.hard_hatsASdefault" + "_DOT_hard_hats),default_DOT_repair_order_details_metricsAS(SELECTdefault_DOT_hard_" + "hat.citydefault_DOT_hard_hat_DOT_city,\tavg(default_DOT_repair_order_details.price)" + "ASdefault_DOT_avg_repair_priceFROMdefault_DOT_repair_order_detailsLEFTJOINdefault_" + "DOT_repair_orderONdefault_DOT_repair_order_details.repair_order_id=default_DOT_" + "repair_order.repair_order_idLEFTJOINdefault_DOT_hard_hatONdefault_DOT_repair_order" + ".hard_hat_id=default_DOT_hard_hat.hard_hat_idGROUPBYdefault_DOT_hard_hat.city)" + "SELECTdefault_DOT_repair_order_details_metrics.default_DOT_hard_hat_DOT_city,\t" + "default_DOT_repair_order_details_metrics.default_DOT_avg_repair_priceFROMdefault" + "_DOT_repair_order_details_metrics" + "": QueryWithResults( + **{ + "id": "bd98d6be-e2d2-413e-94c7-96d9411ddee2", + "submitted_query": "...", + "state": QueryState.FINISHED, + "results": [ + { + "columns": [ + {"name": "default_DOT_hard_hat_DOT_city", "type": "str"}, + {"name": "default_DOT_avg_repair_price", "type": "float"}, + ], + "rows": [ + ("Foo", 1.0), + ("Bar", 2.0), + ], + "sql": "", + }, + ], + "errors": [], + } + ), + "WITHdefault_DOT_repair_order_detailsAS(SELECTdefault_DOT_repair_order_details." + "repair_order_id,\tdefault_DOT_repair_order_details.repair_type_id,\tdefault_DOT_repair_order" + "_details.price,\tdefault_DOT_repair_order_details.quantity,\tdefault_DOT_repair_order_details" + ".discountFROMroads.repair_order_detailsASdefault_DOT_repair_order_details),default_DOT_repair" + "_orderAS(SELECTdefault_DOT_repair_orders.repair_order_id,\tdefault_DOT_repair_orders.municipa" + "lity_id,\tdefault_DOT_repair_orders.hard_hat_id,\tdefault_DOT_repair_orders.order_date,\tdef" + "ault_DOT_repair_orders.required_date,\tdefault_DOT_repair_orders.dispatched_date,\tdefault_" + "DOT_repair_orders.dispatcher_idFROMroads.repair_ordersASdefault_DOT_repair_orders),default_" + "DOT_hard_hatAS(SELECTdefault_DOT_hard_hats.hard_hat_id,\tdefault_DOT_hard_hats.last_name,\t" + "default_DOT_hard_hats.first_name,\tdefault_DOT_hard_hats.title,\tdefault_DOT_hard_hats.birth" + "_date,\tdefault_DOT_hard_hats.hire_date,\tdefault_DOT_hard_hats.address,\tdefault_DOT_hard_" + "hats.city,\tdefault_DOT_hard_hats.state,\tdefault_DOT_hard_hats.postal_code,\tdefault_DOT_" + "hard_hats.country,\tdefault_DOT_hard_hats.manager,\tdefault_DOT_hard_hats.contractor_idFROM" + "roads.hard_hatsASdefault_DOT_hard_hats),default_DOT_repair_order_details_metricsAS(SELECT" + "default_DOT_hard_hat.statedefault_DOT_hard_hat_DOT_state,\tavg(default_DOT_repair_order_" + "details.price)ASdefault_DOT_avg_repair_priceFROMdefault_DOT_repair_order_detailsLEFTJOIN" + "default_DOT_repair_orderONdefault_DOT_repair_order_details.repair_order_id=default_DOT_" + "repair_order.repair_order_idLEFTJOINdefault_DOT_hard_hatONdefault_DOT_repair_order.hard" + "_hat_id=default_DOT_hard_hat.hard_hat_idGROUPBYdefault_DOT_hard_hat.state)SELECTdefault_" + "DOT_repair_order_details_metrics.default_DOT_hard_hat_DOT_state,\tdefault_DOT_repair_order" + "_details_metrics.default_DOT_avg_repair_priceFROMdefault_DOT_repair_order_details_metrics": ( + QueryWithResults( + **{ + "id": "bd98d6be-e2d2-413e-94c7-96d9411ddee2", + "submitted_query": "...", + "state": QueryState.FINISHED, + "results": [], + "errors": [], + } + ) + ), + "WITHdefault_DOT_repair_order_detailsAS(SELECTdefault_DOT_hard_hat.postal_codedefault_" + "DOT_hard_hat_DOT_postal_code,\tavg(default_DOT_repair_order_details.price)ASdefault_" + "DOT_avg_repair_priceFROMroads.repair_order_detailsASdefault_DOT_repair_order_details" + "LEFTJOIN(SELECTdefault_DOT_repair_orders.repair_order_id,\tdefault_DOT_repair_" + "orders.municipality_id,\tdefault_DOT_repair_orders.hard_hat_id,\tdefault_DOT_repair_" + "orders.dispatcher_idFROMroads.repair_ordersASdefault_DOT_repair_orders)ASdefault_DOT_" + "repair_orderONdefault_DOT_repair_order_details.repair_order_id=default_DOT_repair_order" + ".repair_order_idLEFTJOIN(SELECTdefault_DOT_hard_hats.hard_hat_id,\tdefault_DOT_hard" + "_hats.state,\tdefault_DOT_hard_hats.postal_codeFROMroads.hard_hatsASdefault_DOT_hard_hats" + ")ASdefault_DOT_hard_hatONdefault_DOT_repair_order.hard_hat_id=default_DOT_hard_hat.hard_" + "hat_idGROUPBYdefault_DOT_hard_hat.postal_code)SELECTdefault_DOT_repair_order_details." + "default_DOT_avg_repair_price,\tdefault_DOT_repair_order_details.default_DOT_hard_hat_DOT" + "_postal_codeFROMdefault_DOT_repair_order_details": ( + DJQueryServiceClientException("Error response from query service") + ), + "WITHdefault_DOT_repair_order_detailsAS(SELECTdefault_DOT_repair_order_details.repair_" + "order_id,\tdefault_DOT_repair_order_details.repair_type_id,\tdefault_DOT_repair_order_" + "details.price,\tdefault_DOT_repair_order_details.quantity,\tdefault_DOT_repair_order_" + "details.discountFROMroads.repair_order_detailsASdefault_DOT_repair_order_details)," + "default_DOT_repair_orderAS(SELECTdefault_DOT_repair_orders.repair_order_id,\tdefault" + "_DOT_repair_orders.municipality_id,\tdefault_DOT_repair_orders.hard_hat_id,\tdefault" + "_DOT_repair_orders.order_date,\tdefault_DOT_repair_orders.required_date,\tdefault_DOT" + "_repair_orders.dispatched_date,\tdefault_DOT_repair_orders.dispatcher_idFROMroads.repair" + "_ordersASdefault_DOT_repair_orders),default_DOT_hard_hatAS(SELECTdefault_DOT_hard_hats." + "hard_hat_id,\tdefault_DOT_hard_hats.last_name,\tdefault_DOT_hard_hats.first_name,\t" + "default_DOT_hard_hats.title,\tdefault_DOT_hard_hats.birth_date,\tdefault_DOT_hard_hats" + ".hire_date,\tdefault_DOT_hard_hats.address,\tdefault_DOT_hard_hats.city,\tdefault_DOT_" + "hard_hats.state,\tdefault_DOT_hard_hats.postal_code,\tdefault_DOT_hard_hats.country,\t" + "default_DOT_hard_hats.manager,\tdefault_DOT_hard_hats.contractor_idFROMroads.hard_hats" + "ASdefault_DOT_hard_hats)SELECTdefault_DOT_hard_hat.citydefault_DOT_hard_hat_DOT_city," + "\tavg(default_DOT_repair_order_details.price)ASdefault_DOT_avg_repair_priceFROMdefault" + "_DOT_repair_order_detailsLEFTJOINdefault_DOT_repair_orderONdefault_DOT_repair_order_" + "details.repair_order_id=default_DOT_repair_order.repair_order_idLEFTJOINdefault_DOT_" + "hard_hatONdefault_DOT_repair_order.hard_hat_id=default_DOT_hard_hat.hard_hat_idGROUP" + "BYdefault_DOT_hard_hat.city'": QueryWithResults( + **{ + "id": "bd98d6be-e2d2-413e-94c7-96d9411ddee2", + "submitted_query": "...", + "state": QueryState.FINISHED, + "results": [ + { + "columns": [ + {"name": "default_DOT_avg_repair_price", "type": "float"}, + {"name": "default_DOT_hard_hat_DOT_city", "type": "str"}, + ], + "rows": [ + (1.0, "Foo"), + (2.0, "Bar"), + ], + "sql": "", + }, + ], + "errors": [], + } + ), + "WITHdefault_DOT_repair_order_detailsAS(SELECTdefault_DOT_repair_order_details." + "repair_order_id,\tdefault_DOT_repair_order_details.repair_type_id,\tdefault_DOT" + "_repair_order_details.price,\tdefault_DOT_repair_order_details.quantity,\tdefault" + "_DOT_repair_order_details.discountFROMroads.repair_order_detailsASdefault_DOT_repair" + "_order_details),default_DOT_repair_orderAS(SELECTdefault_DOT_repair_orders.repair_" + "order_id,\tdefault_DOT_repair_orders.municipality_id,\tdefault_DOT_repair_orders.hard" + "_hat_id,\tdefault_DOT_repair_orders.order_date,\tdefault_DOT_repair_orders.required" + "_date,\tdefault_DOT_repair_orders.dispatched_date,\tdefault_DOT_repair_orders.dispatcher" + "_idFROMroads.repair_ordersASdefault_DOT_repair_orders),default_DOT_hard_hatAS(SELECT" + "default_DOT_hard_hats.hard_hat_id,\tdefault_DOT_hard_hats.last_name,\tdefault_DOT_" + "hard_hats.first_name,\tdefault_DOT_hard_hats.title,\tdefault_DOT_hard_hats.birth_date" + ",\tdefault_DOT_hard_hats.hire_date,\tdefault_DOT_hard_hats.address,\tdefault_DOT_hard" + "_hats.city,\tdefault_DOT_hard_hats.state,\tdefault_DOT_hard_hats.postal_code,\tdefault" + "_DOT_hard_hats.country,\tdefault_DOT_hard_hats.manager,\tdefault_DOT_hard_hats." + "contractor_idFROMroads.hard_hatsASdefault_DOT_hard_hats)SELECTdefault_DOT_hard_hat" + ".citydefault_DOT_hard_hat_DOT_city,\tavg(default_DOT_repair_order_details.price)AS" + "default_DOT_avg_repair_priceFROMdefault_DOT_repair_order_detailsLEFTJOINdefault_DOT" + "_repair_orderONdefault_DOT_repair_order_details.repair_order_id=default_DOT_repair_" + "order.repair_order_idLEFTJOINdefault_DOT_hard_hatONdefault_DOT_repair_order.hard_hat" + "_id=default_DOT_hard_hat.hard_hat_idGROUPBYdefault_DOT_hard_hat.city": QueryWithResults( + **{ + "id": "bd98d6be-e2d2-413e-94c7-96d9411ddee2", + "submitted_query": "...", + "state": QueryState.FINISHED, + "results": [ + { + "columns": [ + {"name": "default_DOT_hard_hat_DOT_city", "type": "str"}, + {"name": "default_DOT_avg_repair_price", "type": "float"}, + ], + "rows": [ + ("Foo", 1.0), + ("Bar", 2.0), + ], + "sql": "", + }, + ], + "errors": [], + } + ), + "SELECTavg(default_DOT_repair_order_details.price)ASdefault_DOT_avg_repair_price,\tdefault" + "_DOT_hard_hat.citydefault_DOT_hard_hat_DOT_cityFROMroads.repair_order_detailsASdefault_" + "DOT_repair_order_detailsLEFTJOIN(SELECTdefault_DOT_repair_orders.repair_order_id,\tdefault_" + "DOT_repair_orders.municipality_id,\tdefault_DOT_repair_orders.hard_hat_id,\tdefault_DOT_" + "repair_orders.dispatcher_idFROMroads.repair_ordersASdefault_DOT_repair_orders)ASdefault_" + "DOT_repair_orderONdefault_DOT_repair_order_details.repair_order_id=default_DOT_repair_order" + ".repair_order_idLEFTJOIN(SELECTdefault_DOT_hard_hats.hard_hat_id,\tdefault_DOT_hard_hats." + "city,\tdefault_DOT_hard_hats.stateFROMroads.hard_hatsASdefault_DOT_hard_hats)ASdefault_DOT_" + "hard_hatONdefault_DOT_repair_order.hard_hat_id=default_DOT_hard_hat.hard_hat_idGROUPBY" + "default_DOT_hard_hat.city": QueryWithResults( + **{ + "id": "bd98d6be-e2d2-413e-94c7-96d9411ddee2", + "submitted_query": "...", + "state": QueryState.FINISHED, + "results": [ + { + "columns": [ + {"name": "default_DOT_avg_repair_price", "type": "float"}, + {"name": "default_DOT_hard_hat_DOT_city", "type": "str"}, + ], + "rows": [ + (1.0, "Foo"), + (2.0, "Bar"), + ], + "sql": "", + }, + ], + "errors": [], + } + ), +} diff --git a/datajunction-clients/python/tests/examples/project1/dj.yaml b/datajunction-clients/python/tests/examples/project1/dj.yaml new file mode 100644 index 000000000..5536f1146 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/dj.yaml @@ -0,0 +1,27 @@ +name: My DJ Project 1 +description: The description of my DJ project 1 +prefix: projects.project1 +mode: published +tags: + - name: deprecated + description: This node is deprecated + tag_type: Maintenance + - name: tag_in_project + description: tag in project + tag_type: Maintenance +build: + priority: + - roads.date + - roads.date_dim + - roads.repair_orders + - roads.repair_order_transform + - roads.repair_order_details + - roads.contractors + - roads.hard_hats + - roads.hard_hat_state + - roads.us_states + - roads.us_region + - roads.dispatchers + - roads.municipality + - roads.municipality_municipality_type + - roads.municipality_type diff --git a/datajunction-clients/python/tests/examples/project1/roads/avg_length_of_employment.metric.yaml b/datajunction-clients/python/tests/examples/project1/roads/avg_length_of_employment.metric.yaml new file mode 100644 index 000000000..eb1aa572d --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/avg_length_of_employment.metric.yaml @@ -0,0 +1,10 @@ +description: Average length of employment +query: SELECT avg(NOW() - hire_date) FROM ${prefix}roads.hard_hats +display_name: Avg Length of Employment +tags: + - tag_in_project + - system-tag +direction: higher_is_better +unit: second +required_dimensions: + - hard_hat_id diff --git a/datajunction-clients/python/tests/examples/project1/roads/avg_repair_price.metric.yaml b/datajunction-clients/python/tests/examples/project1/roads/avg_repair_price.metric.yaml new file mode 100644 index 000000000..0bc04663f --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/avg_repair_price.metric.yaml @@ -0,0 +1,5 @@ +description: Average repair price +query: SELECT avg(price) FROM ${prefix}roads.repair_order_details +display_name: Avg Repair Price +direction: higher_is_better +unit: dollar diff --git a/datajunction-clients/python/tests/examples/project1/roads/avg_time_to_dispatch.metric.yaml b/datajunction-clients/python/tests/examples/project1/roads/avg_time_to_dispatch.metric.yaml new file mode 100644 index 000000000..0ca91ad90 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/avg_time_to_dispatch.metric.yaml @@ -0,0 +1,3 @@ +description: Average time to dispatch a repair order +query: SELECT avg(dispatched_date - order_date) FROM ${prefix}roads.repair_orders +display_name: Avg Time to Dispatch Repair Order diff --git a/datajunction-clients/python/tests/examples/project1/roads/contractor.dimension.yaml b/datajunction-clients/python/tests/examples/project1/roads/contractor.dimension.yaml new file mode 100644 index 000000000..18a1c572e --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/contractor.dimension.yaml @@ -0,0 +1,19 @@ +description: Contractor dimension +query: | + SELECT + contractor_id, + company_name, + contact_name, + contact_title, + address, + city, + state, + postal_code, + country + FROM ${prefix}roads.contractors +primary_key: + - contractor_id +display_name: Contractor +columns: +- name: company_name + display_name: Contractor Company Name diff --git a/datajunction-clients/python/tests/examples/project1/roads/contractors.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/contractors.source.yaml new file mode 100644 index 000000000..7819b57b4 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/contractors.source.yaml @@ -0,0 +1,22 @@ +description: Information on companies contracted for repairs +table: default.roads.contractors +columns: + - name: contractor_id + type: int + - name: company_name + type: string + - name: contact_name + type: string + - name: contact_title + type: string + - name: address + type: string + - name: city + type: string + - name: state + type: string + - name: postal_code + type: string + - name: country + type: string +display_name: default.roads.contractors diff --git a/datajunction-clients/python/tests/examples/project1/roads/date.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/date.source.yaml new file mode 100644 index 000000000..43e14c93e --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/date.source.yaml @@ -0,0 +1,12 @@ +description: Date dimension +table: default.roads.date +columns: + - name: dateint + type: timestamp + - name: year + type: int + - name: month + type: int + - name: day + type: int +display_name: default.roads.date diff --git a/datajunction-clients/python/tests/examples/project1/roads/date_dim.dimension.yaml b/datajunction-clients/python/tests/examples/project1/roads/date_dim.dimension.yaml new file mode 100644 index 000000000..f3981a3fe --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/date_dim.dimension.yaml @@ -0,0 +1,11 @@ +description: This is a source for information on outstanding and fulfilled repair orders +query: | + SELECT + dateint, + month, + year, + day + FROM ${prefix}roads.date +primary_key: + - dateint +display_name: Date diff --git a/datajunction-clients/python/tests/examples/project1/roads/dispatcher.dimension.yaml b/datajunction-clients/python/tests/examples/project1/roads/dispatcher.dimension.yaml new file mode 100644 index 000000000..c2ef25d08 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/dispatcher.dimension.yaml @@ -0,0 +1,10 @@ +description: Dispatcher dimension +query: | + SELECT + dispatcher_id, + company_name, + phone + FROM ${prefix}roads.dispatchers +primary_key: + - dispatcher_id +display_name: Dispatcher diff --git a/datajunction-clients/python/tests/examples/project1/roads/dispatchers.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/dispatchers.source.yaml new file mode 100644 index 000000000..1ae5eea10 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/dispatchers.source.yaml @@ -0,0 +1,10 @@ +description: Dispatcher information +table: default.roads.dispatchers +columns: + - name: dispatcher_id + type: int + - name: company_name + type: string + - name: phone + type: string +display_name: default.roads.dispatchers diff --git a/datajunction-clients/python/tests/examples/project1/roads/hard_hat.dimension.yaml b/datajunction-clients/python/tests/examples/project1/roads/hard_hat.dimension.yaml new file mode 100644 index 000000000..0b3517cf8 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/hard_hat.dimension.yaml @@ -0,0 +1,35 @@ +description: Hard hat dimension +query: | + SELECT + hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id + FROM ${prefix}roads.hard_hats +primary_key: + - hard_hat_id +dimension_links: + - type: join + node_column: state + dimension_node: ${prefix}roads.us_state + join_on: ${prefix}roads.us_state.state_abbr = ${prefix}roads.hard_hat.state + - type: join + node_column: birth_date + dimension_node: ${prefix}roads.date_dim + join_on: ${prefix}roads.date_dim.dateint = ${prefix}roads.hard_hat.birth_date + role: birth_date + - type: join + node_column: hire_date + dimension_node: ${prefix}roads.date_dim + join_on: ${prefix}roads.date_dim.dateint = ${prefix}roads.hard_hat.hire_date + role: hire_date +display_name: Hard Hat diff --git a/datajunction-clients/python/tests/examples/project1/roads/hard_hat_state.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/hard_hat_state.source.yaml new file mode 100644 index 000000000..c5da3fd69 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/hard_hat_state.source.yaml @@ -0,0 +1,8 @@ +description: State locale lookup table for employees +table: default.roads.hard_hat_state +columns: + - name: hard_hat_id + type: int + - name: state_id + type: string +display_name: default.roads.hard_hat_state diff --git a/datajunction-clients/python/tests/examples/project1/roads/hard_hats.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/hard_hats.source.yaml new file mode 100644 index 000000000..a0e0a61de --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/hard_hats.source.yaml @@ -0,0 +1,30 @@ +description: Internal employees table +table: default.roads.hard_hats +columns: + - name: hard_hat_id + type: int + - name: last_name + type: string + - name: first_name + type: string + - name: title + type: string + - name: birth_date + type: date + - name: hire_date + type: date + - name: address + type: string + - name: city + type: string + - name: state + type: string + - name: postal_code + type: string + - name: country + type: string + - name: manager + type: int + - name: contractor_id + type: int +display_name: default.roads.hard_hats diff --git a/datajunction-clients/python/tests/examples/project1/roads/local_hard_hats.dimension.yaml b/datajunction-clients/python/tests/examples/project1/roads/local_hard_hats.dimension.yaml new file mode 100644 index 000000000..8ec8239cb --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/local_hard_hats.dimension.yaml @@ -0,0 +1,36 @@ +description: Hard hat dimension +query: | + SELECT + hh.hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id, + hhs.state_id AS state_id + FROM ${prefix}roads.hard_hats hh + LEFT JOIN ${prefix}roads.hard_hat_state hhs + ON hh.hard_hat_id = hhs.hard_hat_id + WHERE hh.state_id = 'NY' +primary_key: + - hard_hat_id +dimension_links: + - type: join + node_column: state_id + dimension_node: ${prefix}roads.us_state + - type: reference + node_column: birth_date + dimension: ${prefix}roads.date_dim.dateint + role: birth_date + - type: reference + node_column: hire_date + dimension: ${prefix}roads.date_dim.dateint + role: hire_date +display_name: Local Hard Hats diff --git a/datajunction-clients/python/tests/examples/project1/roads/municipality.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/municipality.source.yaml new file mode 100644 index 000000000..264539b0d --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/municipality.source.yaml @@ -0,0 +1,16 @@ +description: Municipality data +table: default.roads.municipality +columns: + - name: municipality_id + type: string + - name: contact_name + type: string + - name: contact_title + type: string + - name: local_region + type: string + - name: phone + type: string + - name: state_id + type: int +display_name: default.roads.municipality diff --git a/datajunction-clients/python/tests/examples/project1/roads/municipality_dim.dimension.yaml b/datajunction-clients/python/tests/examples/project1/roads/municipality_dim.dimension.yaml new file mode 100644 index 000000000..0af94ec55 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/municipality_dim.dimension.yaml @@ -0,0 +1,18 @@ +description: Municipality dimension +query: | + SELECT + m.municipality_id AS municipality_id, + contact_name, + contact_title, + local_region, + state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM ${prefix}roads.municipality AS m + LEFT JOIN ${prefix}roads.municipality_municipality_type AS mmt + ON m.municipality_id = mmt.municipality_id + LEFT JOIN ${prefix}roads.municipality_type AS mt + ON mmt.municipality_type_id = mt.municipality_type_desc +primary_key: + - municipality_id +display_name: Municipality diff --git a/datajunction-clients/python/tests/examples/project1/roads/municipality_municipality_type.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/municipality_municipality_type.source.yaml new file mode 100644 index 000000000..9b6e7902a --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/municipality_municipality_type.source.yaml @@ -0,0 +1,8 @@ +description: Municipality information +table: default.roads.municipality_municipality_type +columns: + - name: municipality_id + type: string + - name: municipality_type_id + type: string +display_name: default.roads.municipality_municipality_type diff --git a/datajunction-clients/python/tests/examples/project1/roads/municipality_type.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/municipality_type.source.yaml new file mode 100644 index 000000000..e0df1a418 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/municipality_type.source.yaml @@ -0,0 +1,8 @@ +description: Descriptions for different types of municipalities +table: default.roads.municipality_type +columns: + - name: municipality_type_id + type: string + - name: municipality_type_desc + type: string +display_name: default.roads.municipality_type diff --git a/datajunction-clients/python/tests/examples/project1/roads/national_level_agg.transform.yaml b/datajunction-clients/python/tests/examples/project1/roads/national_level_agg.transform.yaml new file mode 100644 index 000000000..b774840d8 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/national_level_agg.transform.yaml @@ -0,0 +1,6 @@ +description: National level aggregates +query: SELECT SUM(rd.price * rd.quantity) AS total_amount_nationwide FROM ${prefix}roads.repair_order_details rd +display_name: National Level Aggs +custom_metadata: + level: national + sublevel: state diff --git a/datajunction-clients/python/tests/examples/project1/roads/num_repair_orders.metric.yaml b/datajunction-clients/python/tests/examples/project1/roads/num_repair_orders.metric.yaml new file mode 100644 index 000000000..acf343b0d --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/num_repair_orders.metric.yaml @@ -0,0 +1,3 @@ +description: Number of repair orders +query: SELECT count(repair_order_id) FROM ${prefix}roads.repair_orders +display_name: Num Repair Orders diff --git a/datajunction-clients/python/tests/examples/project1/roads/regional_level_agg.transform.yaml b/datajunction-clients/python/tests/examples/project1/roads/regional_level_agg.transform.yaml new file mode 100644 index 000000000..8447145ee --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/regional_level_agg.transform.yaml @@ -0,0 +1,57 @@ +description: Regional-level aggregates +display_name: Regional-level Aggs +query: | + SELECT + usr.us_region_id, + us.state_name, + CONCAT(us.state_name, '-', usr.us_region_description) AS location_hierarchy, + EXTRACT(YEAR FROM ro.order_date) AS order_year, + EXTRACT(MONTH FROM ro.order_date) AS order_month, + EXTRACT(DAY FROM ro.order_date) AS order_day, + COUNT(DISTINCT CASE WHEN ro.dispatched_date IS NOT NULL THEN ro.repair_order_id ELSE NULL END) AS completed_repairs, + COUNT(DISTINCT ro.repair_order_id) AS total_repairs_dispatched, + SUM(rd.price * rd.quantity) AS total_amount_in_region, + AVG(rd.price * rd.quantity) AS avg_repair_amount_in_region, + -- ELEMENT_AT(ARRAY_SORT(COLLECT_LIST(STRUCT(COUNT(*) AS cnt, rt.repair_type_name AS repair_type_name)), (left, right) -> case when left.cnt < right.cnt then 1 when left.cnt > right.cnt then -1 else 0 end), 0).repair_type_name AS most_common_repair_type, + AVG(DATEDIFF(ro.dispatched_date, ro.order_date)) AS avg_dispatch_delay, + COUNT(DISTINCT c.contractor_id) AS unique_contractors + FROM + (SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM ${prefix}roads.repair_orders) ro + JOIN + ${prefix}roads.municipality m ON ro.municipality_id = m.municipality_id + JOIN + ${prefix}roads.us_states us ON m.state_id = us.state_id + AND AVG(rd.price * rd.quantity) > + (SELECT AVG(price * quantity) FROM ${prefix}roads.repair_order_details WHERE repair_order_id = ro.repair_order_id) + JOIN + ${prefix}roads.us_states us ON m.state_id = us.state_id + JOIN + ${prefix}roads.us_region usr ON us.state_region = usr.us_region_id + JOIN + ${prefix}roads.repair_order_details rd ON ro.repair_order_id = rd.repair_order_id + JOIN + ${prefix}roads.repair_type rt ON rd.repair_type_id = rt.repair_type_id + JOIN + ${prefix}roads.contractors c ON rt.contractor_id = c.contractor_id + GROUP BY + usr.us_region_id, + EXTRACT(YEAR FROM ro.order_date), + EXTRACT(MONTH FROM ro.order_date), + EXTRACT(DAY FROM ro.order_date) +dimension_links: + - type: join + node_column: state_name + dimension_node: ${prefix}roads.us_state +columns: +- name: location_hierarchy + display_name: Location (Hierarchy) + attributes: + - dimension diff --git a/datajunction-clients/python/tests/examples/project1/roads/regional_repair_efficiency.metric.yaml b/datajunction-clients/python/tests/examples/project1/roads/regional_repair_efficiency.metric.yaml new file mode 100644 index 000000000..14291c356 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/regional_repair_efficiency.metric.yaml @@ -0,0 +1,17 @@ +description: | + For each US region (as defined in the us_region table), we want to calculate: + Regional Repair Efficiency = (Number of Completed Repairs / Total Repairs Dispatched) × + (Total Repair Amount in Region / Total Repair Amount Nationwide) × 100 + Here: + A "Completed Repair" is one where the dispatched_date is not null. + "Total Repair Amount in Region" is the total amount spent on repairs in a given region. + "Total Repair Amount Nationwide" is the total amount spent on all repairs nationwide. +query: | + SELECT + (SUM(rm.completed_repairs) * 1.0 / SUM(rm.total_repairs_dispatched)) * + (SUM(rm.total_amount_in_region) * 1.0 / SUM(na.total_amount_nationwide)) * 100 + FROM + ${prefix}roads.regional_level_agg rm + CROSS JOIN + ${prefix}roads.national_level_agg na +display_name: Regional Repair Efficiency diff --git a/datajunction-clients/python/tests/examples/project1/roads/repair_order.dimension.yaml b/datajunction-clients/python/tests/examples/project1/roads/repair_order.dimension.yaml new file mode 100644 index 000000000..0708acd4b --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/repair_order.dimension.yaml @@ -0,0 +1,21 @@ +description: Repair order dimension +query: | + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + dispatcher_id + FROM ${prefix}roads.repair_order_transform +primary_key: + - repair_order_id +dimension_links: + - type: join + node_column: dispatcher_id + dimension_node: ${prefix}roads.dispatcher + - type: join + node_column: hard_hat_id + dimension_node: ${prefix}roads.hard_hat + - type: join + node_column: municipality_id + dimension_node: ${prefix}roads.municipality_dim +display_name: Repair Order Dim diff --git a/datajunction-clients/python/tests/examples/project1/roads/repair_order_details.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/repair_order_details.source.yaml new file mode 100644 index 000000000..9c20bbb91 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/repair_order_details.source.yaml @@ -0,0 +1,18 @@ +description: This is a source for details on individual repair orders +table: default.roads.repair_order_details +columns: + - name: repair_order_id + type: int + - name: repair_type_id + type: int + - name: price + type: float + - name: quantity + type: int + - name: discount + type: float +dimension_links: + - type: join + node_column: repair_order_id + dimension_node: ${prefix}roads.repair_order +display_name: default.roads.repair_order_details diff --git a/datajunction-clients/python/tests/examples/project1/roads/repair_order_transform.transform.yaml b/datajunction-clients/python/tests/examples/project1/roads/repair_order_transform.transform.yaml new file mode 100644 index 000000000..eb6abfc80 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/repair_order_transform.transform.yaml @@ -0,0 +1,10 @@ +description: Some column pruning for the repair order dimension +query: | + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + dispatcher_id + FROM ${prefix}roads.repair_orders + WHERE dispatcher_id IS NOT NULL +display_name: Repair Order Transform diff --git a/datajunction-clients/python/tests/examples/project1/roads/repair_orders.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/repair_orders.source.yaml new file mode 100644 index 000000000..812c4ab4e --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/repair_orders.source.yaml @@ -0,0 +1,22 @@ +description: This is a source for information on outstanding and fulfilled repair orders +table: default.roads.repair_orders +columns: + - name: repair_order_id + type: int + - name: municipality_id + type: string + - name: hard_hat_id + type: int + - name: order_date + type: date + - name: required_date + type: date + - name: dispatched_date + type: date + - name: dispatcher_id + type: int +dimension_links: + - type: join + node_column: repair_order_id + dimension_node: ${prefix}roads.repair_order +display_name: default.roads.repair_orders diff --git a/datajunction-clients/python/tests/examples/project1/roads/repair_orders_cube.cube.yaml b/datajunction-clients/python/tests/examples/project1/roads/repair_orders_cube.cube.yaml new file mode 100644 index 000000000..626f355f6 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/repair_orders_cube.cube.yaml @@ -0,0 +1,9 @@ +description: Repair Orders Cube +metrics: + - ${prefix}roads.num_repair_orders + - ${prefix}roads.avg_repair_price + - ${prefix}roads.total_repair_cost +dimensions: + - ${prefix}roads.hard_hat.state + - ${prefix}roads.dispatcher.company_name +filters: [] diff --git a/datajunction-clients/python/tests/examples/project1/roads/repair_type.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/repair_type.source.yaml new file mode 100644 index 000000000..8aa90ffcd --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/repair_type.source.yaml @@ -0,0 +1,14 @@ +description: Information on different types of repairs +table: default.roads.repair_type +columns: + - name: repair_type_id + type: int + - name: repair_type_name + type: string + - name: contractor_id + type: int +dimension_links: + - type: join + node_column: contractor_id + dimension_node: ${prefix}roads.contractor +display_name: default.roads.repair_type diff --git a/datajunction-clients/python/tests/examples/project1/roads/total_repair_cost.metric.yaml b/datajunction-clients/python/tests/examples/project1/roads/total_repair_cost.metric.yaml new file mode 100644 index 000000000..fbc743134 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/total_repair_cost.metric.yaml @@ -0,0 +1,3 @@ +description: Total repair cost +query: SELECT sum(price) FROM ${prefix}roads.repair_order_details +display_name: Total Repair Cost diff --git a/datajunction-clients/python/tests/examples/project1/roads/total_repair_order_discounts.metric.yaml b/datajunction-clients/python/tests/examples/project1/roads/total_repair_order_discounts.metric.yaml new file mode 100644 index 000000000..1ce6ec0aa --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/total_repair_order_discounts.metric.yaml @@ -0,0 +1,3 @@ +description: Total repair order discounts +query: SELECT sum(price * discount) FROM ${prefix}roads.repair_order_details +display_name: Total Repair Order Discounts diff --git a/datajunction-clients/python/tests/examples/project1/roads/us_region.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/us_region.source.yaml new file mode 100644 index 000000000..f8d41424f --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/us_region.source.yaml @@ -0,0 +1,8 @@ +description: US region info +table: default.roads.us_region +columns: + - name: us_region_id + type: int + - name: us_region_description + type: string +display_name: default.roads.us_region diff --git a/datajunction-clients/python/tests/examples/project1/roads/us_state.dimension.yaml b/datajunction-clients/python/tests/examples/project1/roads/us_state.dimension.yaml new file mode 100644 index 000000000..474437aaa --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/us_state.dimension.yaml @@ -0,0 +1,14 @@ +description: US state dimension +query: | + SELECT + state_id, + state_name, + state_abbr, + state_region, + r.us_region_description AS state_region_description + FROM ${prefix}roads.us_states s + LEFT JOIN ${prefix}roads.us_region r + ON s.state_region = r.us_region_description +primary_key: + - state_id +display_name: US State diff --git a/datajunction-clients/python/tests/examples/project1/roads/us_states.source.yaml b/datajunction-clients/python/tests/examples/project1/roads/us_states.source.yaml new file mode 100644 index 000000000..908cfb24d --- /dev/null +++ b/datajunction-clients/python/tests/examples/project1/roads/us_states.source.yaml @@ -0,0 +1,12 @@ +description: US state data +table: default.roads.us_states +columns: + - name: state_id + type: int + - name: state_name + type: string + - name: state_abbr + type: string + - name: state_region + type: int +display_name: default.roads.us_states diff --git a/datajunction-clients/python/tests/examples/project10/dj.yaml b/datajunction-clients/python/tests/examples/project10/dj.yaml new file mode 100644 index 000000000..ab65c99d2 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project10/dj.yaml @@ -0,0 +1,11 @@ +name: My DJ Project 10 +description: Tests that duplicate tags are handled +prefix: projects.project10 +mode: published +tags: + - name: deprecated + description: This node is deprecated + tag_type: Maintenance + - name: deprecated + description: This node is deprecated + tag_type: Maintenance diff --git a/datajunction-clients/python/tests/examples/project11/avg_length_of_employment.metric.yaml b/datajunction-clients/python/tests/examples/project11/avg_length_of_employment.metric.yaml new file mode 100644 index 000000000..a27a4ff70 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/avg_length_of_employment.metric.yaml @@ -0,0 +1,2 @@ +description: Average length of employment +query: SELECT avg(NOW() - hire_date) FROM ${prefix}roads.hard_hats diff --git a/datajunction-clients/python/tests/examples/project11/avg_repair_price.metric.yaml b/datajunction-clients/python/tests/examples/project11/avg_repair_price.metric.yaml new file mode 100644 index 000000000..25e0c85e5 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/avg_repair_price.metric.yaml @@ -0,0 +1,2 @@ +description: Average repair price +query: SELECT avg(price) FROM ${prefix}roads.repair_order_details diff --git a/datajunction-clients/python/tests/examples/project11/avg_time_to_dispatch.metric.yaml b/datajunction-clients/python/tests/examples/project11/avg_time_to_dispatch.metric.yaml new file mode 100644 index 000000000..db5b9c71f --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/avg_time_to_dispatch.metric.yaml @@ -0,0 +1,2 @@ +description: Average time to dispatch a repair order +query: SELECT avg(dispatched_date - order_date) FROM ${prefix}roads.repair_orders diff --git a/datajunction-clients/python/tests/examples/project11/contractor.dimension.yaml b/datajunction-clients/python/tests/examples/project11/contractor.dimension.yaml new file mode 100644 index 000000000..89310a894 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/contractor.dimension.yaml @@ -0,0 +1,15 @@ +description: Contractor dimension +query: | + SELECT + contractor_id, + company_name, + contact_name, + contact_title, + address, + city, + state, + postal_code, + country + FROM ${prefix}roads.contractors +primary_key: + - contractor_id diff --git a/datajunction-clients/python/tests/examples/project11/contractors.source.yaml b/datajunction-clients/python/tests/examples/project11/contractors.source.yaml new file mode 100644 index 000000000..e0d3946d7 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/contractors.source.yaml @@ -0,0 +1,21 @@ +description: Information on companies contracted for repairs +table: default.roads.contractors +columns: + - name: contractor_id + type: int + - name: company_name + type: string + - name: contact_name + type: string + - name: contact_title + type: string + - name: address + type: string + - name: city + type: string + - name: state + type: string + - name: postal_code + type: string + - name: country + type: string diff --git a/datajunction-clients/python/tests/examples/project11/date.source.yaml b/datajunction-clients/python/tests/examples/project11/date.source.yaml new file mode 100644 index 000000000..7b91b747f --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/date.source.yaml @@ -0,0 +1,11 @@ +description: Date dimension +table: default.roads.date +columns: + - name: dateint + type: timestamp + - name: year + type: int + - name: month + type: int + - name: day + type: int diff --git a/datajunction-clients/python/tests/examples/project11/date_dim.dimension.yaml b/datajunction-clients/python/tests/examples/project11/date_dim.dimension.yaml new file mode 100644 index 000000000..ea53fcca5 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/date_dim.dimension.yaml @@ -0,0 +1,10 @@ +description: This is a source for information on outstanding and fulfilled repair orders +query: | + SELECT + dateint, + month, + year, + day + FROM ${prefix}roads.date +primary_key: + - dateint diff --git a/datajunction-clients/python/tests/examples/project11/dispatcher.dimension.yaml b/datajunction-clients/python/tests/examples/project11/dispatcher.dimension.yaml new file mode 100644 index 000000000..4260c870c --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/dispatcher.dimension.yaml @@ -0,0 +1,9 @@ +description: Dispatcher dimension +query: | + SELECT + dispatcher_id, + company_name, + phone + FROM ${prefix}roads.dispatchers +primary_key: + - dispatcher_id diff --git a/datajunction-clients/python/tests/examples/project11/dispatchers.source.yaml b/datajunction-clients/python/tests/examples/project11/dispatchers.source.yaml new file mode 100644 index 000000000..650e5ab52 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/dispatchers.source.yaml @@ -0,0 +1,9 @@ +description: Dispatcher information +table: default.roads.dispatchers +columns: + - name: dispatcher_id + type: int + - name: company_name + type: string + - name: phone + type: string diff --git a/datajunction-clients/python/tests/examples/project11/dj.yaml b/datajunction-clients/python/tests/examples/project11/dj.yaml new file mode 100644 index 000000000..0c95a4f04 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/dj.yaml @@ -0,0 +1,23 @@ +name: My DJ Project 11 +prefix: projects.project11 +mode: published +tags: + - name: deprecated + description: This node is deprecated + tag_type: Maintenance +build: + priority: + - roads.date + - roads.date_dim + - roads.repair_orders + - roads.repair_order_transform + - roads.repair_order_details + - roads.contractors + - roads.hard_hats + - roads.hard_hat_state + - roads.us_states + - roads.us_region + - roads.dispatchers + - roads.municipality + - roads.municipality_municipality_type + - roads.municipality_type diff --git a/datajunction-clients/python/tests/examples/project11/hard_hat.dimension.yaml b/datajunction-clients/python/tests/examples/project11/hard_hat.dimension.yaml new file mode 100644 index 000000000..af49a27f2 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/hard_hat.dimension.yaml @@ -0,0 +1,32 @@ +description: Hard hat dimension +query: | + SELECT + hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id + FROM ${prefix}roads.hard_hats +primary_key: + - hard_hat_id +dimension_links: + - type: join + node_column: state + dimension_node: ${prefix}roads.us_state + join_on: ${prefix}roads.us_state.state_abbr = ${prefix}roads.hard_hat.state + - type: join + node_column: birth_date + dimension_node: ${prefix}roads.date_dim + join_on: ${prefix}roads.date_dim.dateint = ${prefix}roads.hard_hat.birth_date + - type: join + node_column: hire_date + dimension_node: ${prefix}roads.date_dim + join_on: ${prefix}roads.date_dim.dateint = ${prefix}roads.hard_hat.hire_date diff --git a/datajunction-clients/python/tests/examples/project11/hard_hat_state.source.yaml b/datajunction-clients/python/tests/examples/project11/hard_hat_state.source.yaml new file mode 100644 index 000000000..438f4e9a8 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/hard_hat_state.source.yaml @@ -0,0 +1,7 @@ +description: State locale lookup table for employees +table: default.roads.hard_hat_state +columns: + - name: hard_hat_id + type: int + - name: state_id + type: string diff --git a/datajunction-clients/python/tests/examples/project11/hard_hats.source.yaml b/datajunction-clients/python/tests/examples/project11/hard_hats.source.yaml new file mode 100644 index 000000000..04dfe4b21 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/hard_hats.source.yaml @@ -0,0 +1,29 @@ +description: Internal employees table +table: default.roads.hard_hats +columns: + - name: hard_hat_id + type: int + - name: last_name + type: string + - name: first_name + type: string + - name: title + type: string + - name: birth_date + type: date + - name: hire_date + type: date + - name: address + type: string + - name: city + type: string + - name: state + type: string + - name: postal_code + type: string + - name: country + type: string + - name: manager + type: int + - name: contractor_id + type: int diff --git a/datajunction-clients/python/tests/examples/project11/local_hard_hats.dimension.yaml b/datajunction-clients/python/tests/examples/project11/local_hard_hats.dimension.yaml new file mode 100644 index 000000000..217443c32 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/local_hard_hats.dimension.yaml @@ -0,0 +1,28 @@ +description: Hard hat dimension +query: | + SELECT + hh.hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id, + hhs.state_id AS state_id + FROM ${prefix}roads.hard_hats hh + LEFT JOIN ${prefix}roads.hard_hat_state hhs + ON hh.hard_hat_id = hhs.hard_hat_id + WHERE hh.state_id = 'NY' +primary_key: + - hard_hat_id +dimension_links: + - type: join + node_column: state_id + dimension_node: ${prefix}roads.us_state + join_on: ${prefix}roads.us_state.state_abbr = ${prefix}roads.hard_hat.state_id diff --git a/datajunction-clients/python/tests/examples/project11/municipality.source.yaml b/datajunction-clients/python/tests/examples/project11/municipality.source.yaml new file mode 100644 index 000000000..a5d2b6f34 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/municipality.source.yaml @@ -0,0 +1,15 @@ +description: Municipality data +table: default.roads.municipality +columns: + - name: municipality_id + type: string + - name: contact_name + type: string + - name: contact_title + type: string + - name: local_region + type: string + - name: phone + type: string + - name: state_id + type: int diff --git a/datajunction-clients/python/tests/examples/project11/municipality_dim.dimension.yaml b/datajunction-clients/python/tests/examples/project11/municipality_dim.dimension.yaml new file mode 100644 index 000000000..757bc0d73 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/municipality_dim.dimension.yaml @@ -0,0 +1,17 @@ +description: Municipality dimension +query: | + SELECT + m.municipality_id AS municipality_id, + contact_name, + contact_title, + local_region, + state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM ${prefix}roads.municipality AS m + LEFT JOIN ${prefix}roads.municipality_municipality_type AS mmt + ON m.municipality_id = mmt.municipality_id + LEFT JOIN ${prefix}roads.municipality_type AS mt + ON mmt.municipality_type_id = mt.municipality_type_desc +primary_key: + - municipality_id diff --git a/datajunction-clients/python/tests/examples/project11/municipality_municipality_type.source.yaml b/datajunction-clients/python/tests/examples/project11/municipality_municipality_type.source.yaml new file mode 100644 index 000000000..eef1762bb --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/municipality_municipality_type.source.yaml @@ -0,0 +1,7 @@ +description: Municipality information +table: default.roads.municipality_municipality_type +columns: + - name: municipality_id + type: string + - name: municipality_type_id + type: string diff --git a/datajunction-clients/python/tests/examples/project11/municipality_type.source.yaml b/datajunction-clients/python/tests/examples/project11/municipality_type.source.yaml new file mode 100644 index 000000000..7a9baa684 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/municipality_type.source.yaml @@ -0,0 +1,7 @@ +description: Descriptions for different types of municipalities +table: default.roads.municipality_type +columns: + - name: municipality_type_id + type: string + - name: municipality_type_desc + type: string diff --git a/datajunction-clients/python/tests/examples/project11/national_level_agg.transform.yaml b/datajunction-clients/python/tests/examples/project11/national_level_agg.transform.yaml new file mode 100644 index 000000000..8cb04002a --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/national_level_agg.transform.yaml @@ -0,0 +1,2 @@ +description: National level aggregates +query: SELECT SUM(rd.price * rd.quantity) AS total_amount_nationwide FROM ${prefix}roads.repair_order_details rd diff --git a/datajunction-clients/python/tests/examples/project11/num_repair_orders.metric.yaml b/datajunction-clients/python/tests/examples/project11/num_repair_orders.metric.yaml new file mode 100644 index 000000000..42ac4c46f --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/num_repair_orders.metric.yaml @@ -0,0 +1,2 @@ +description: Number of repair orders +query: SELECT count(repair_order_id) FROM ${prefix}roads.repair_orders diff --git a/datajunction-clients/python/tests/examples/project11/regional_level_agg.transform.yaml b/datajunction-clients/python/tests/examples/project11/regional_level_agg.transform.yaml new file mode 100644 index 000000000..ba3a11f55 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/regional_level_agg.transform.yaml @@ -0,0 +1,51 @@ +description: Regional-level aggregates +query: | + SELECT + usr.us_region_id, + us.state_name, + CONCAT(us.state_name, '-', usr.us_region_description) AS location_hierarchy, + EXTRACT(YEAR FROM ro.order_date) AS order_year, + EXTRACT(MONTH FROM ro.order_date) AS order_month, + EXTRACT(DAY FROM ro.order_date) AS order_day, + COUNT(DISTINCT CASE WHEN ro.dispatched_date IS NOT NULL THEN ro.repair_order_id ELSE NULL END) AS completed_repairs, + COUNT(DISTINCT ro.repair_order_id) AS total_repairs_dispatched, + SUM(rd.price * rd.quantity) AS total_amount_in_region, + AVG(rd.price * rd.quantity) AS avg_repair_amount_in_region, + -- ELEMENT_AT(ARRAY_SORT(COLLECT_LIST(STRUCT(COUNT(*) AS cnt, rt.repair_type_name AS repair_type_name)), (left, right) -> case when left.cnt < right.cnt then 1 when left.cnt > right.cnt then -1 else 0 end), 0).repair_type_name AS most_common_repair_type, + AVG(DATEDIFF(ro.dispatched_date, ro.order_date)) AS avg_dispatch_delay, + COUNT(DISTINCT c.contractor_id) AS unique_contractors + FROM + (SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM ${prefix}roads.repair_orders) ro + JOIN + ${prefix}roads.municipality m ON ro.municipality_id = m.municipality_id + JOIN + ${prefix}roads.us_states us ON m.state_id = us.state_id + AND AVG(rd.price * rd.quantity) > + (SELECT AVG(price * quantity) FROM ${prefix}roads.repair_order_details WHERE repair_order_id = ro.repair_order_id) + JOIN + ${prefix}roads.us_states us ON m.state_id = us.state_id + JOIN + ${prefix}roads.us_region usr ON us.state_region = usr.us_region_id + JOIN + ${prefix}roads.repair_order_details rd ON ro.repair_order_id = rd.repair_order_id + JOIN + ${prefix}roads.repair_type rt ON rd.repair_type_id = rt.repair_type_id + JOIN + ${prefix}roads.contractors c ON rt.contractor_id = c.contractor_id + GROUP BY + usr.us_region_id, + EXTRACT(YEAR FROM ro.order_date), + EXTRACT(MONTH FROM ro.order_date), + EXTRACT(DAY FROM ro.order_date) +dimension_links: + state_name: + dimension: ${prefix}roads.us_state + column: state_abbr diff --git a/datajunction-clients/python/tests/examples/project11/regional_repair_efficiency.metric.yaml b/datajunction-clients/python/tests/examples/project11/regional_repair_efficiency.metric.yaml new file mode 100644 index 000000000..3349b5cd0 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/regional_repair_efficiency.metric.yaml @@ -0,0 +1,16 @@ +description: | + For each US region (as defined in the us_region table), we want to calculate: + Regional Repair Efficiency = (Number of Completed Repairs / Total Repairs Dispatched) × + (Total Repair Amount in Region / Total Repair Amount Nationwide) × 100 + Here: + A "Completed Repair" is one where the dispatched_date is not null. + "Total Repair Amount in Region" is the total amount spent on repairs in a given region. + "Total Repair Amount Nationwide" is the total amount spent on all repairs nationwide. +query: | + SELECT + (SUM(rm.completed_repairs) * 1.0 / SUM(rm.total_repairs_dispatched)) * + (SUM(rm.total_amount_in_region) * 1.0 / SUM(na.total_amount_nationwide)) * 100 + FROM + ${prefix}roads.regional_level_agg rm + CROSS JOIN + ${prefix}roads.national_level_agg na diff --git a/datajunction-clients/python/tests/examples/project11/repair_order.dimension.yaml b/datajunction-clients/python/tests/examples/project11/repair_order.dimension.yaml new file mode 100644 index 000000000..cf9e39cbb --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/repair_order.dimension.yaml @@ -0,0 +1,20 @@ +description: Repair order dimension +query: | + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + dispatcher_id + FROM ${prefix}roads.repair_order_transform +primary_key: + - repair_order_id +dimension_links: + - type: join + node_column: dispatcher_id + dimension_node: ${prefix}roads.dispatcher + - type: join + node_column: hard_hat_id + dimension_node: ${prefix}roads.hard_hat + - type: join + node_column: municipality_id + dimension_node: ${prefix}roads.municipality_dim diff --git a/datajunction-clients/python/tests/examples/project11/repair_order_details.source.yaml b/datajunction-clients/python/tests/examples/project11/repair_order_details.source.yaml new file mode 100644 index 000000000..f2947f6ad --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/repair_order_details.source.yaml @@ -0,0 +1,17 @@ +description: This is a source for details on individual repair orders +table: default.roads.repair_order_details +columns: + - name: repair_order_id + type: int + - name: repair_type_id + type: int + - name: price + type: float + - name: quantity + type: int + - name: discount + type: float +dimension_links: + - type: join + node_column: repair_order_id + dimension_node: ${prefix}roads.repair_order diff --git a/datajunction-clients/python/tests/examples/project11/repair_order_transform.transform.yaml b/datajunction-clients/python/tests/examples/project11/repair_order_transform.transform.yaml new file mode 100644 index 000000000..861594a45 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/repair_order_transform.transform.yaml @@ -0,0 +1,9 @@ +description: Some column pruning for the repair order dimension +query: | + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + dispatcher_id + FROM ${prefix}roads.repair_orders + WHERE dispatcher_id IS NOT NULL diff --git a/datajunction-clients/python/tests/examples/project11/repair_orders.source.yaml b/datajunction-clients/python/tests/examples/project11/repair_orders.source.yaml new file mode 100644 index 000000000..1bacd3e1b --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/repair_orders.source.yaml @@ -0,0 +1,21 @@ +description: This is a source for information on outstanding and fulfilled repair orders +table: default.roads.repair_orders +columns: + - name: repair_order_id + type: int + - name: municipality_id + type: string + - name: hard_hat_id + type: int + - name: order_date + type: date + - name: required_date + type: date + - name: dispatched_date + type: date + - name: dispatcher_id + type: int +dimension_links: + - type: join + node_column: repair_order_id + dimension_node: ${prefix}roads.repair_order diff --git a/datajunction-clients/python/tests/examples/project11/repair_orders_cube.cube.yaml b/datajunction-clients/python/tests/examples/project11/repair_orders_cube.cube.yaml new file mode 100644 index 000000000..f92d8fc37 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/repair_orders_cube.cube.yaml @@ -0,0 +1,9 @@ +description: Repair Orders Cube +metrics: + - ${prefix}roads.num_repair_orders + - ${prefix}roads.avg_repair_price + - ${prefix}roads.total_repair_cost +dimensions: + - ${prefix}roads.hard_hat.state + - ${prefix}roads.dispatcher.company_name +filter: [] diff --git a/datajunction-clients/python/tests/examples/project11/repair_type.source.yaml b/datajunction-clients/python/tests/examples/project11/repair_type.source.yaml new file mode 100644 index 000000000..b8ba7d255 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/repair_type.source.yaml @@ -0,0 +1,13 @@ +description: Information on different types of repairs +table: default.roads.repair_type +columns: + - name: repair_type_id + type: int + - name: repair_type_name + type: string + - name: contractor_id + type: int +dimension_links: + - type: join + node_column: contractor_id + dimension_node: ${prefix}roads.contractor diff --git a/datajunction-clients/python/tests/examples/project11/total_repair_cost.metric.yaml b/datajunction-clients/python/tests/examples/project11/total_repair_cost.metric.yaml new file mode 100644 index 000000000..0451e60dd --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/total_repair_cost.metric.yaml @@ -0,0 +1,2 @@ +description: Total repair cost +query: SELECT sum(price) FROM ${prefix}roads.repair_order_details diff --git a/datajunction-clients/python/tests/examples/project11/total_repair_order_discounts.metric.yaml b/datajunction-clients/python/tests/examples/project11/total_repair_order_discounts.metric.yaml new file mode 100644 index 000000000..8d0f7d164 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/total_repair_order_discounts.metric.yaml @@ -0,0 +1,2 @@ +description: Total repair order discounts +query: SELECT sum(price * discount) FROM ${prefix}roads.repair_order_details diff --git a/datajunction-clients/python/tests/examples/project11/us_region.source.yaml b/datajunction-clients/python/tests/examples/project11/us_region.source.yaml new file mode 100644 index 000000000..2d63d5a70 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/us_region.source.yaml @@ -0,0 +1,7 @@ +description: US region info +table: default.roads.us_region +columns: + - name: us_region_id + type: int + - name: us_region_description + type: string diff --git a/datajunction-clients/python/tests/examples/project11/us_state.dimension.yaml b/datajunction-clients/python/tests/examples/project11/us_state.dimension.yaml new file mode 100644 index 000000000..6bebf25f0 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/us_state.dimension.yaml @@ -0,0 +1,13 @@ +description: US state dimension +query: | + SELECT + state_id, + state_name, + state_abbr, + state_region, + r.us_region_description AS state_region_description + FROM ${prefix}roads.us_states s + LEFT JOIN ${prefix}roads.us_region r + ON s.state_region = r.us_region_description +primary_key: + - state_id diff --git a/datajunction-clients/python/tests/examples/project11/us_states.source.yaml b/datajunction-clients/python/tests/examples/project11/us_states.source.yaml new file mode 100644 index 000000000..c3f5a40d0 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project11/us_states.source.yaml @@ -0,0 +1,11 @@ +description: US state data +table: default.roads.us_states +columns: + - name: state_id + type: int + - name: state_name + type: string + - name: state_abbr + type: string + - name: state_region + type: int diff --git a/datajunction-clients/python/tests/examples/project12/dj.yaml b/datajunction-clients/python/tests/examples/project12/dj.yaml new file mode 100644 index 000000000..40de609d3 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project12/dj.yaml @@ -0,0 +1,11 @@ +name: My DJ Project 7 +prefix: projects.project7 +mode: published +build: + priority: + - roads.companies + - roads.contractors + - roads.us_states + - roads.companies_dim + - roads.us_state + - roads.contractor diff --git a/datajunction-clients/python/tests/examples/project12/roads/companies.source.yaml b/datajunction-clients/python/tests/examples/project12/roads/companies.source.yaml new file mode 100644 index 000000000..c2a1bb177 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project12/roads/companies.source.yaml @@ -0,0 +1,5 @@ +description: Companies dimension +table: default.roads.companies +columns: + - name: name + type: str diff --git a/datajunction-clients/python/tests/examples/project12/roads/companies_dim.dimension.yaml b/datajunction-clients/python/tests/examples/project12/roads/companies_dim.dimension.yaml new file mode 100644 index 000000000..03d573ed8 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project12/roads/companies_dim.dimension.yaml @@ -0,0 +1,7 @@ +description: This is a source for information on companies +query: | + SELECT + name + FROM ${prefix}roads.companies +primary_key: + - name diff --git a/datajunction-clients/python/tests/examples/project12/roads/contractor.dimension.yaml b/datajunction-clients/python/tests/examples/project12/roads/contractor.dimension.yaml new file mode 100644 index 000000000..89310a894 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project12/roads/contractor.dimension.yaml @@ -0,0 +1,15 @@ +description: Contractor dimension +query: | + SELECT + contractor_id, + company_name, + contact_name, + contact_title, + address, + city, + state, + postal_code, + country + FROM ${prefix}roads.contractors +primary_key: + - contractor_id diff --git a/datajunction-clients/python/tests/examples/project12/roads/contractors.source.yaml b/datajunction-clients/python/tests/examples/project12/roads/contractors.source.yaml new file mode 100644 index 000000000..e0d3946d7 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project12/roads/contractors.source.yaml @@ -0,0 +1,21 @@ +description: Information on companies contracted for repairs +table: default.roads.contractors +columns: + - name: contractor_id + type: int + - name: company_name + type: string + - name: contact_name + type: string + - name: contact_title + type: string + - name: address + type: string + - name: city + type: string + - name: state + type: string + - name: postal_code + type: string + - name: country + type: string diff --git a/datajunction-clients/python/tests/examples/project12/roads/us_state.dimension.yaml b/datajunction-clients/python/tests/examples/project12/roads/us_state.dimension.yaml new file mode 100644 index 000000000..c6915eaf1 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project12/roads/us_state.dimension.yaml @@ -0,0 +1,10 @@ +description: US state dimension +query: | + SELECT + state_id, + state_name, + state_abbr, + state_region + FROM ${prefix}roads.us_states s +primary_key: + - state_id diff --git a/datajunction-clients/python/tests/examples/project12/roads/us_states.source.yaml b/datajunction-clients/python/tests/examples/project12/roads/us_states.source.yaml new file mode 100644 index 000000000..c3f5a40d0 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project12/roads/us_states.source.yaml @@ -0,0 +1,11 @@ +description: US state data +table: default.roads.us_states +columns: + - name: state_id + type: int + - name: state_name + type: string + - name: state_abbr + type: string + - name: state_region + type: int diff --git a/datajunction-clients/python/tests/examples/project2/dj.yaml b/datajunction-clients/python/tests/examples/project2/dj.yaml new file mode 100644 index 000000000..e63893f1e --- /dev/null +++ b/datajunction-clients/python/tests/examples/project2/dj.yaml @@ -0,0 +1,3 @@ +name: My DJ Project 2 +prefix: projects.project2 +mode: published diff --git a/datajunction-clients/python/tests/examples/project2/some_node.source.yaml b/datajunction-clients/python/tests/examples/project2/some_node.source.yaml new file mode 100644 index 000000000..c5fe7489f --- /dev/null +++ b/datajunction-clients/python/tests/examples/project2/some_node.source.yaml @@ -0,0 +1,7 @@ +description: A node that will raise due to missing a catalog in the table name +table: roads.us_states +columns: + - name: foo + type: int + - name: bar + type: string diff --git a/datajunction-clients/python/tests/examples/project3/dj.yaml b/datajunction-clients/python/tests/examples/project3/dj.yaml new file mode 100644 index 000000000..0893fdb36 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project3/dj.yaml @@ -0,0 +1,3 @@ +name: My DJ Project 3 +prefix: projects.project3 +mode: published diff --git a/datajunction-clients/python/tests/examples/project3/some_node.yaml b/datajunction-clients/python/tests/examples/project3/some_node.yaml new file mode 100644 index 000000000..2a86c25b2 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project3/some_node.yaml @@ -0,0 +1,7 @@ +description: A source that raises an error because it's missing the .source.yaml extension +table: default.myschema.mytable +columns: + - name: foo + type: int + - name: bar + type: string diff --git a/datajunction-clients/python/tests/examples/project4/dj.yaml b/datajunction-clients/python/tests/examples/project4/dj.yaml new file mode 100644 index 000000000..fb4495a4c --- /dev/null +++ b/datajunction-clients/python/tests/examples/project4/dj.yaml @@ -0,0 +1,3 @@ +name: My DJ Project 4 +prefix: projects.project4 +mode: published diff --git a/datajunction-clients/python/tests/examples/project4/very/very/deeply/nested/namespace/some_node.source.yaml b/datajunction-clients/python/tests/examples/project4/very/very/deeply/nested/namespace/some_node.source.yaml new file mode 100644 index 000000000..3b3a5f71e --- /dev/null +++ b/datajunction-clients/python/tests/examples/project4/very/very/deeply/nested/namespace/some_node.source.yaml @@ -0,0 +1,7 @@ +description: A source node definition +table: default.myschema.mytable +columns: + - name: foo + type: int + - name: bar + type: string diff --git a/datajunction-clients/python/tests/examples/project5/dj.yaml b/datajunction-clients/python/tests/examples/project5/dj.yaml new file mode 100644 index 000000000..311980138 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project5/dj.yaml @@ -0,0 +1,3 @@ +name: My DJ Project 5 +prefix: projects.project5 +mode: published diff --git a/datajunction-clients/python/tests/examples/project5/some_node.a.b.c.source.yaml b/datajunction-clients/python/tests/examples/project5/some_node.a.b.c.source.yaml new file mode 100644 index 000000000..2a86c25b2 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project5/some_node.a.b.c.source.yaml @@ -0,0 +1,7 @@ +description: A source that raises an error because it's missing the .source.yaml extension +table: default.myschema.mytable +columns: + - name: foo + type: int + - name: bar + type: string diff --git a/datajunction-clients/python/tests/examples/project6/dj.yaml b/datajunction-clients/python/tests/examples/project6/dj.yaml new file mode 100644 index 000000000..5163abfdf --- /dev/null +++ b/datajunction-clients/python/tests/examples/project6/dj.yaml @@ -0,0 +1,3 @@ +name: My DJ Project 6 +prefix: projects.project6 +mode: published diff --git a/datajunction-clients/python/tests/examples/project6/roads/contractor.dimension.yaml b/datajunction-clients/python/tests/examples/project6/roads/contractor.dimension.yaml new file mode 100644 index 000000000..d812169ba --- /dev/null +++ b/datajunction-clients/python/tests/examples/project6/roads/contractor.dimension.yaml @@ -0,0 +1,15 @@ +description: A dimension node that will fail because it's selecting from a node that doesn't exist +query: | + SELECT + contractor_id, + company_name, + contact_name, + contact_title, + address, + city, + state, + postal_code, + country + FROM some_non_existent_node +primary_key: + - contractor_id diff --git a/datajunction-clients/python/tests/examples/project6/roads/contractors.source.yaml b/datajunction-clients/python/tests/examples/project6/roads/contractors.source.yaml new file mode 100644 index 000000000..e0d3946d7 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project6/roads/contractors.source.yaml @@ -0,0 +1,21 @@ +description: Information on companies contracted for repairs +table: default.roads.contractors +columns: + - name: contractor_id + type: int + - name: company_name + type: string + - name: contact_name + type: string + - name: contact_title + type: string + - name: address + type: string + - name: city + type: string + - name: state + type: string + - name: postal_code + type: string + - name: country + type: string diff --git a/datajunction-clients/python/tests/examples/project7/dj.yaml b/datajunction-clients/python/tests/examples/project7/dj.yaml new file mode 100644 index 000000000..97176a069 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project7/dj.yaml @@ -0,0 +1,3 @@ +name: My DJ Project 7 +prefix: projects.project7 +mode: published diff --git a/datajunction-clients/python/tests/examples/project7/roads/contractor.dimension.yaml b/datajunction-clients/python/tests/examples/project7/roads/contractor.dimension.yaml new file mode 100644 index 000000000..f84bcb49c --- /dev/null +++ b/datajunction-clients/python/tests/examples/project7/roads/contractor.dimension.yaml @@ -0,0 +1,19 @@ +description: A dimension node that will fail because it's selecting from a node that doesn't exist +query: | + SELECT + contractor_id, + company_name, + contact_name, + contact_title, + address, + city, + state, + postal_code, + country + FROM some_non_existent_node +primary_key: + - contractor_id +dimension_links: + - type: join + node_column: repair_order_id + dimension_node: ${prefix}does.not.exist diff --git a/datajunction-clients/python/tests/examples/project7/roads/contractors.source.yaml b/datajunction-clients/python/tests/examples/project7/roads/contractors.source.yaml new file mode 100644 index 000000000..e0d3946d7 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project7/roads/contractors.source.yaml @@ -0,0 +1,21 @@ +description: Information on companies contracted for repairs +table: default.roads.contractors +columns: + - name: contractor_id + type: int + - name: company_name + type: string + - name: contact_name + type: string + - name: contact_title + type: string + - name: address + type: string + - name: city + type: string + - name: state + type: string + - name: postal_code + type: string + - name: country + type: string diff --git a/datajunction-clients/python/tests/examples/project8/dj.yaml b/datajunction-clients/python/tests/examples/project8/dj.yaml new file mode 100644 index 000000000..17bea96bc --- /dev/null +++ b/datajunction-clients/python/tests/examples/project8/dj.yaml @@ -0,0 +1,6 @@ +name: My DJ Project 8 +prefix: projects.project8 +mode: published +build: + priority: + - node.that.does.not.exist diff --git a/datajunction-clients/python/tests/examples/project9/dj.yaml b/datajunction-clients/python/tests/examples/project9/dj.yaml new file mode 100644 index 000000000..40de609d3 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project9/dj.yaml @@ -0,0 +1,11 @@ +name: My DJ Project 7 +prefix: projects.project7 +mode: published +build: + priority: + - roads.companies + - roads.contractors + - roads.us_states + - roads.companies_dim + - roads.us_state + - roads.contractor diff --git a/datajunction-clients/python/tests/examples/project9/roads/companies.source.yaml b/datajunction-clients/python/tests/examples/project9/roads/companies.source.yaml new file mode 100644 index 000000000..c2a1bb177 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project9/roads/companies.source.yaml @@ -0,0 +1,5 @@ +description: Companies dimension +table: default.roads.companies +columns: + - name: name + type: str diff --git a/datajunction-clients/python/tests/examples/project9/roads/companies_dim.dimension.yaml b/datajunction-clients/python/tests/examples/project9/roads/companies_dim.dimension.yaml new file mode 100644 index 000000000..03d573ed8 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project9/roads/companies_dim.dimension.yaml @@ -0,0 +1,7 @@ +description: This is a source for information on companies +query: | + SELECT + name + FROM ${prefix}roads.companies +primary_key: + - name diff --git a/datajunction-clients/python/tests/examples/project9/roads/contractor.dimension.yaml b/datajunction-clients/python/tests/examples/project9/roads/contractor.dimension.yaml new file mode 100644 index 000000000..bd585c364 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project9/roads/contractor.dimension.yaml @@ -0,0 +1,22 @@ +description: Contractor dimension +query: | + SELECT + contractor_id, + company_name, + contact_name, + contact_title, + address, + city, + state, + postal_code, + country + FROM ${prefix}roads.contractors +primary_key: + - contractor_id +dimension_links: + - type: join + node_column: state + dimension_node: ${prefix}roads.us_state + - type: reference + node_column: company_name + dimension: ${prefix}roads.companies_dim.name diff --git a/datajunction-clients/python/tests/examples/project9/roads/contractors.source.yaml b/datajunction-clients/python/tests/examples/project9/roads/contractors.source.yaml new file mode 100644 index 000000000..e0d3946d7 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project9/roads/contractors.source.yaml @@ -0,0 +1,21 @@ +description: Information on companies contracted for repairs +table: default.roads.contractors +columns: + - name: contractor_id + type: int + - name: company_name + type: string + - name: contact_name + type: string + - name: contact_title + type: string + - name: address + type: string + - name: city + type: string + - name: state + type: string + - name: postal_code + type: string + - name: country + type: string diff --git a/datajunction-clients/python/tests/examples/project9/roads/us_state.dimension.yaml b/datajunction-clients/python/tests/examples/project9/roads/us_state.dimension.yaml new file mode 100644 index 000000000..c6915eaf1 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project9/roads/us_state.dimension.yaml @@ -0,0 +1,10 @@ +description: US state dimension +query: | + SELECT + state_id, + state_name, + state_abbr, + state_region + FROM ${prefix}roads.us_states s +primary_key: + - state_id diff --git a/datajunction-clients/python/tests/examples/project9/roads/us_states.source.yaml b/datajunction-clients/python/tests/examples/project9/roads/us_states.source.yaml new file mode 100644 index 000000000..c3f5a40d0 --- /dev/null +++ b/datajunction-clients/python/tests/examples/project9/roads/us_states.source.yaml @@ -0,0 +1,11 @@ +description: US state data +table: default.roads.us_states +columns: + - name: state_id + type: int + - name: state_name + type: string + - name: state_abbr + type: string + - name: state_region + type: int diff --git a/datajunction-clients/python/tests/test__internal.py b/datajunction-clients/python/tests/test__internal.py new file mode 100644 index 000000000..87e3fa191 --- /dev/null +++ b/datajunction-clients/python/tests/test__internal.py @@ -0,0 +1,91 @@ +""" +Tests DJ client (internal) functionality. +""" + +from unittest.mock import MagicMock, call, patch + +import pytest + +from datajunction._internal import DJClient +from datajunction.exceptions import DJClientException, DJTagDoesNotExist + + +class TestDJClient: # pylint: disable=too-many-public-methods, protected-access + """ + Tests for DJClient internal functionality. + """ + + @pytest.fixture + def client(self, module__server): + """ + Returns a DJ client instance + """ + return DJClient(requests_session=module__server) + + def test_create_user(self, client): + """ + Check that `client.create_user()` works as expected. + """ + client._session.post = MagicMock( + return_value=MagicMock( + json=MagicMock(return_value={"text": "User already exists."}), + ), + ) + response = client.create_user(email="foo", username="bar", password="baz") + assert response == {"text": "User already exists."} + assert client._session.post.call_args == call( + "/basic/user/", + data={"email": "foo", "username": "bar", "password": "baz"}, + ) + + def test_basic_login(self, client): + """ + Check that `client.basic_login()` works as expected. + """ + client._session.post = MagicMock() + client.basic_login(username="bar", password="baz") + assert client._session.post.call_args == call( + "/basic/login/", + data={"username": "bar", "password": "baz"}, + ) + + def test__verify_node_exists(self, client): + """ + Check that `client._verify_node_exists()` works as expected. + """ + with patch("starlette.testclient.TestClient.get") as get_mock: + get_mock.return_value = MagicMock( + json=MagicMock(return_value={"name": "_", "type": "foo"}), + ) + with pytest.raises(DJClientException): + client._verify_node_exists(node_name="_", type_="bar") + + def test__list_nodes_with_tag(self, client): + """ + Check that `client._list_nodes_with_tag()` works as expected. + """ + # error: tag does not exist + with pytest.raises(DJTagDoesNotExist): + client._list_nodes_with_tag( + tag_name="foo", + ) + + # error: invalid node_type + with pytest.raises(AttributeError): + client._list_nodes_with_tag( + tag_name="foo", + node_type="not_a_node", + ) + with pytest.raises(TypeError): + client._list_nodes_with_tag( + tag_name="foo", + node_type=MagicMock(), + ) + + # error: exception during request + client._session.get = MagicMock(side_effect=Exception("Boom!")) + with pytest.raises(Exception) as exc_info: + client._list_nodes_with_tag( + tag_name="foo", + ) + assert "Boom!" in str(exc_info.value) diff --git a/datajunction-clients/python/tests/test_admin.py b/datajunction-clients/python/tests/test_admin.py new file mode 100644 index 000000000..8cfab46ec --- /dev/null +++ b/datajunction-clients/python/tests/test_admin.py @@ -0,0 +1,129 @@ +"""Tests DJ client""" +import pytest + +from datajunction import DJAdmin +from datajunction.exceptions import DJClientException + + +class TestDJAdmin: # pylint: disable=too-many-public-methods + """ + Tests for DJ client/builder functionality. + """ + + @pytest.fixture + def client(self, module__session_with_examples): + """ + Returns a DJ client instance + """ + return DJAdmin(requests_session=module__session_with_examples) # type: ignore + + # + # Data Catalogs + # + def test_get_catalog(self, client): + """ + Check that `client.get_catalog()` works as expected. + """ + result = client.get_catalog(name="default") + assert result == { + "name": "default", + "engines": [ + {"name": "spark", "version": "3.1.1", "uri": None, "dialect": "spark"}, + ], + } + + def test_add_catalog(self, client): + """ + Check that `client.add_catalog()` works as expected. + """ + # check not exists + result = client.get_catalog(name="foo-bar-baz") + assert "Catalog with name `foo-bar-baz` does not exist." in result["message"] + # add catalog + result = client.add_catalog(name="foo-bar-baz") + assert result is None + # check does exist + result = client.get_catalog(name="foo-bar-baz") + assert result == { + "name": "foo-bar-baz", + "engines": [], + } + + # + # Database Engines + # + def test_get_engine(self, client): + """ + Check that `client.get_engine()` works as expected. + """ + result = client.get_engine(name="spark", version="3.1.1") + assert result == { + "dialect": "spark", + "name": "spark", + "uri": None, + "version": "3.1.1", + } + + def test_add_engine(self, client): + """ + Check that `client.get_engine()` works as expected. + """ + # check not exist + result = client.get_engine(name="8-cylinder", version="7.11") + assert "Engine not found: `8-cylinder` version `7.11`" in result["detail"] + # add engine + client.add_engine( + name="8-cylinder", + version="7.11", + uri="go/to-the-corner", + dialect="trino", + ) + # check not exist + result = client.get_engine(name="8-cylinder", version="7.11") + assert result == { + "dialect": "trino", + "name": "8-cylinder", + "uri": "go/to-the-corner", + "version": "7.11", + } + + def test_link_engine_to_catalog(self, client): + """ + Check that `client.get_engine()` works as expected. + """ + # try to link engine to catalog + with pytest.raises(DJClientException) as exc: + result = client.link_engine_to_catalog( + engine="12-cylinder", + version="7.11", + catalog="public", + ) + assert "Engine not found: `12-cylinder` version `7.11`" in str(exc) + # add engine + client.add_engine( + name="12-cylinder", + version="7.11", + uri="go/to-the-corner", + dialect="trino", + ) + # try to link engine to catalog (again) + result = client.link_engine_to_catalog( + engine="12-cylinder", + version="7.11", + catalog="public", + ) + assert result is None + # check the catalog + result = client.get_catalog(name="public") + assert result == { + "engines": [ + {"dialect": None, "name": "postgres", "uri": None, "version": "15.2"}, + { + "dialect": "trino", + "name": "12-cylinder", + "uri": "go/to-the-corner", + "version": "7.11", + }, + ], + "name": "public", + } diff --git a/datajunction-clients/python/tests/test_base.py b/datajunction-clients/python/tests/test_base.py new file mode 100644 index 000000000..29b369c85 --- /dev/null +++ b/datajunction-clients/python/tests/test_base.py @@ -0,0 +1,170 @@ +""" +Test serializable mixin for dict to dataclass conversion +""" + +from dataclasses import dataclass +from datetime import datetime +from typing import List, Optional + +from datajunction._base import SerializableMixin +from datajunction._internal import DJClient + + +@dataclass +class DataClassSimple(SerializableMixin): + """Simple dataclass""" + + name: str + version: str + other: Optional[int] + created: datetime + + +@dataclass +class DataClassNested(SerializableMixin): + """Nested dataclass""" + + name: str + dj_client: Optional[DJClient] + dc_list: List[DataClassSimple] + dc_list_optional: Optional[List[DataClassSimple]] + dc_optional: Optional[DataClassSimple] + dc_str_list: List[str] + + +class OtherClass(SerializableMixin): # pylint: disable=too-few-public-methods + """Non-dataclass test case""" + + def __init__(self, name: str, version: Optional[str]): + self.name = name + self.version = version + + +class TestSerializableMixin: + """ + Tests for the SerializableMixin base class + """ + + def test_non_dataclass(self): + """ + Test the mixin with non-dataclasses + """ + other_class = OtherClass.from_dict( + dj_client=None, + data={"name": "Name", "version": "v1"}, + ) + assert other_class.name == "Name" + assert other_class.version == "v1" + + def test_serialize_simple(self): + """ + Serialize simple dataclass + """ + now = datetime.now() + dc_simple = DataClassSimple.from_dict( + dj_client=None, + data={ + "name": "Name", + "version": "v1", + "other": 123, + "created": now, + }, + ) + assert dc_simple.name == "Name" + assert dc_simple.version == "v1" + assert dc_simple.other == 123 + assert dc_simple.created == now + + def test_serialize_nested(self): + """ + Serialize nested dataclass + """ + now = datetime.now() + dj_client = DJClient() + dc_nested = DataClassNested.from_dict( + dj_client=dj_client, + data={ + "name": "Name", + "dc_list": [ + {"name": "N1", "version": "v1", "other": 123, "created": now}, + {"name": "N2", "version": "v2", "other": None, "created": now}, + ], + "dc_list_optional": [ + {"name": "N1", "version": "v1", "other": 123, "created": now}, + ], + "dc_optional": { + "name": "N1", + "version": "v1", + "other": 123, + "created": now, + }, + "dc_str_list": ["a", "b", "c"], + }, + ) + assert dc_nested == DataClassNested( + name="Name", + dj_client=dj_client, + dc_list=[ + DataClassSimple( + name="N1", + version="v1", + other=123, + created=now, + ), + DataClassSimple( + name="N2", + version="v2", + other=None, + created=now, + ), + ], + dc_list_optional=[ + DataClassSimple( + name="N1", + version="v1", + other=123, + created=now, + ), + ], + dc_optional=DataClassSimple( + name="N1", + version="v1", + other=123, + created=now, + ), + dc_str_list=["a", "b", "c"], + ) + dc_nested = DataClassNested.from_dict( + dj_client=None, + data={ + "name": "Name", + "dc_list": [ + {"name": "N1", "version": "v1", "other": 123, "created": now}, + {"name": "N2", "version": "v2", "other": None, "created": now}, + ], + "dc_list_optional": None, + "dc_optional": None, + "dc_str_list": ["a", "b", "c"], + }, + ) + assert dc_nested == DataClassNested( + name="Name", + dj_client=None, + dc_list=[ + DataClassSimple( + name="N1", + version="v1", + other=123, + created=now, + ), + DataClassSimple( + name="N2", + version="v2", + other=None, + created=now, + ), + ], + dc_list_optional=None, + dc_optional=None, + dc_str_list=["a", "b", "c"], + ) diff --git a/datajunction-clients/python/tests/test_builder.py b/datajunction-clients/python/tests/test_builder.py new file mode 100644 index 000000000..8a4e3446a --- /dev/null +++ b/datajunction-clients/python/tests/test_builder.py @@ -0,0 +1,1283 @@ +# pylint: disable=too-many-lines,too-many-statements +"""Tests DJ client""" +from unittest.mock import MagicMock, patch + +import pytest +from requests.exceptions import HTTPError + +from datajunction import DJBuilder +from datajunction.exceptions import ( + DJClientException, + DJNamespaceAlreadyExists, + DJTableAlreadyRegistered, + DJTagAlreadyExists, + DJViewAlreadyRegistered, +) +from datajunction.models import ( + AvailabilityState, + Column, + ColumnAttribute, + Materialization, + MaterializationJobType, + MaterializationStrategy, + MetricDirection, + MetricUnit, + NodeMode, +) + + +class TestDJBuilder: # pylint: disable=too-many-public-methods, protected-access + """ + Tests for DJ client/builder functionality. + """ + + @pytest.fixture + def client(self, module__session_with_examples): + """ + Returns a DJ client instance + """ + return DJBuilder(requests_session=module__session_with_examples) # type: ignore + + def test_nodes_in_namespace(self, client): + """ + Check that `client._get_nodes_in_namespace()` works as expected. + """ + assert set(client.namespace("foo.bar").nodes()) == { + "foo.bar.repair_orders", + "foo.bar.repair_order_details", + "foo.bar.repair_type", + "foo.bar.contractors", + "foo.bar.municipality_municipality_type", + "foo.bar.municipality_type", + "foo.bar.municipality", + "foo.bar.dispatchers", + "foo.bar.hard_hats", + "foo.bar.hard_hat_state", + "foo.bar.us_states", + "foo.bar.us_region", + "foo.bar.repair_order", + "foo.bar.contractor", + "foo.bar.hard_hat", + "foo.bar.local_hard_hats", + "foo.bar.us_state", + "foo.bar.dispatcher", + "foo.bar.municipality_dim", + "foo.bar.num_repair_orders", + "foo.bar.avg_repair_price", + "foo.bar.total_repair_cost", + "foo.bar.avg_length_of_employment", + "foo.bar.total_repair_order_discounts", + "foo.bar.avg_repair_order_discounts", + "foo.bar.avg_time_to_dispatch", + "foo.bar.cube_one", + "foo.bar.repair_orders_thin", + "foo.bar.with_custom_metadata", + } + assert set(client.namespace("foo.bar").sources()) == { + "foo.bar.repair_orders", + "foo.bar.repair_order_details", + "foo.bar.repair_type", + "foo.bar.contractors", + "foo.bar.municipality_municipality_type", + "foo.bar.municipality_type", + "foo.bar.municipality", + "foo.bar.dispatchers", + "foo.bar.hard_hats", + "foo.bar.hard_hat_state", + "foo.bar.us_states", + "foo.bar.us_region", + } + assert set(client.list_dimensions(namespace="foo.bar")) == { + "foo.bar.repair_order", + "foo.bar.contractor", + "foo.bar.hard_hat", + "foo.bar.local_hard_hats", + "foo.bar.us_state", + "foo.bar.dispatcher", + "foo.bar.municipality_dim", + } + assert set(client.list_metrics(namespace="foo.bar")) == { + "foo.bar.num_repair_orders", + "foo.bar.avg_repair_price", + "foo.bar.total_repair_cost", + "foo.bar.avg_length_of_employment", + "foo.bar.total_repair_order_discounts", + "foo.bar.avg_repair_order_discounts", + "foo.bar.avg_time_to_dispatch", + } + assert client.namespace("foo.bar").transforms() == [ + "foo.bar.with_custom_metadata", + "foo.bar.repair_orders_thin", + ] + assert client.namespace("foo.bar").cubes() == ["foo.bar.cube_one"] + + def test_all_nodes(self, client): + """ + Verifies that retrieving nodes with `client.nodes()` or node-type + specific calls like `client.sources()` work. + """ + expected_names_only = { + "default.repair_orders", + "default.repair_orders_foo", + "default.repair_order_details", + "default.repair_type", + "default.contractors", + "default.municipality_municipality_type", + "default.municipality_type", + "default.municipality", + "default.dispatchers", + "default.hard_hats", + "default.hard_hat_state", + "default.us_states", + "default.us_region", + "default.repair_order", + "default.contractor", + "default.hard_hat", + "default.local_hard_hats", + "default.us_state", + "default.dispatcher", + "default.municipality_dim", + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + "default.avg_length_of_employment", + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + "default.avg_time_to_dispatch", + "default.cube_two", + "default.repair_orders_thin", + } + result_names_only = client.namespace("default").nodes() + assert set(result_names_only) == expected_names_only + + # sources + result_names_only = client.namespace("default").sources() + assert set(result_names_only) == { + "default.repair_orders", + "default.repair_orders_foo", + "default.repair_order_details", + "default.repair_type", + "default.contractors", + "default.municipality_municipality_type", + "default.municipality_type", + "default.municipality", + "default.dispatchers", + "default.hard_hats", + "default.hard_hat_state", + "default.us_states", + "default.us_region", + } + + repair_orders = client.source("default.repair_orders") + assert repair_orders.name == "default.repair_orders" + assert repair_orders.catalog == "default" + assert repair_orders.schema_ == "roads" + assert repair_orders.table == "repair_orders" + assert repair_orders.type == "source" + assert repair_orders.description == "All repair orders" + assert repair_orders.tags == [] + assert repair_orders.primary_key == [] + assert repair_orders.current_version == "v1.0" + assert repair_orders.columns[0] == Column( + name="repair_order_id", + type="int", + display_name="Repair Order Id", + attributes=[], + dimension=None, + ) + + # dimensions (all) + all_dimensions = client.list_dimensions() + assert set(all_dimensions) == { + "default.repair_order", + "default.contractor", + "default.hard_hat", + "default.local_hard_hats", + "default.us_state", + "default.dispatcher", + "default.municipality_dim", + "foo.bar.repair_order", + "foo.bar.contractor", + "foo.bar.hard_hat", + "foo.bar.local_hard_hats", + "foo.bar.us_state", + "foo.bar.dispatcher", + "foo.bar.municipality_dim", + } + # dimensions (namespace: default) + result_names_only = client.list_dimensions(namespace="default") + assert set(result_names_only) == { + "default.repair_order", + "default.contractor", + "default.hard_hat", + "default.local_hard_hats", + "default.us_state", + "default.dispatcher", + "default.municipality_dim", + } + repair_order_dim = client.dimension("default.repair_order") + assert repair_order_dim.name == "default.repair_order" + assert "FROM default.repair_orders" in repair_order_dim.query + assert repair_order_dim.type == "dimension" + assert repair_order_dim.primary_key == ["repair_order_id"] + assert repair_order_dim.description == "Repair order dimension" + assert repair_order_dim.tags == [] + assert repair_order_dim.current_version == "v1.0" + assert repair_order_dim.columns[0] == Column( + name="repair_order_id", + type="int", + display_name="Repair Order Id", + attributes=[ColumnAttribute(name="primary_key", namespace="system")], + dimension=None, + ) + + # transforms + result = client.namespace("default").transforms() + assert result == ["default.repair_orders_thin"] + thin = client.transform("default.repair_orders_thin") + assert thin.name == "default.repair_orders_thin" + assert "FROM default.repair_orders" in thin.query + assert thin.type == "transform" + assert thin.primary_key == [] + assert thin.description == "3 columns from default.repair_orders" + assert thin.tags == [] + assert thin.current_version == "v1.0" + assert thin.columns[0] == Column( + name="repair_order_id", + type="int", + display_name="Repair Order Id", + attributes=[], + dimension=None, + ) + + # metrics + result_names_only = client.list_metrics(namespace="default") + assert set(result_names_only) == { + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + "default.avg_length_of_employment", + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + "default.avg_time_to_dispatch", + } + + num_repair_orders = client.metric("default.num_repair_orders") + assert num_repair_orders.name == "default.num_repair_orders" + assert num_repair_orders.query == ( + "SELECT count(repair_order_id) FROM default.repair_orders" + ) + assert num_repair_orders.type == "metric" + assert num_repair_orders.required_dimensions == [] + assert num_repair_orders.description == "Number of repair orders" + assert num_repair_orders.tags == [] + assert num_repair_orders.metric_metadata is None + assert num_repair_orders.current_version == "v1.0" + assert num_repair_orders.columns[0] == Column( + name="default_DOT_num_repair_orders", + type="bigint", + display_name="Num Repair Orders", + attributes=[], + dimension=None, + ) + + # cubes + result = client.namespace("default").cubes() + assert result == ["default.cube_two"] + cube_two = client.cube("default.cube_two") + assert cube_two.type == "cube" + assert cube_two.metrics == ["default.num_repair_orders"] + assert cube_two.dimensions == ["default.municipality_dim.local_region"] + assert cube_two.filters is None + assert cube_two.columns[0] == Column( + name="default.num_repair_orders", + type="bigint", + display_name="Num Repair Orders", + attributes=[], + dimension=None, + ) + + with pytest.raises(DJClientException) as exc_info: + client.cube("a_cube") + assert "Cube `a_cube` does not exist" in str(exc_info) + + def test_deactivating_node(self, client): # pylint: disable=unused-argument + """ + Verifies that deactivating and reactivating a node works. + """ + length_metric = client.metric("default.avg_length_of_employment") + response = length_metric.delete() + assert response is None + assert "default.avg_length_of_employment" not in client.list_metrics( + namespace="default", + ) + response = length_metric.restore() + assert response is None + assert "default.avg_length_of_employment" in client.list_metrics( + namespace="default", + ) + + def test_register_table(self, client): # pylint: disable=unused-argument + """ + Verifies that registering a table works. + """ + try: + client.create_namespace("source") + except DJNamespaceAlreadyExists: + pass + store_comments = client.register_table( + catalog="default", + schema="store", + table="comments", + ) + assert store_comments.name == "source.default.store.comments" + assert ( + "source.default.store.comments" + in client.namespace("source.default.store").sources() + ) + # and that errors are handled properly + with patch("starlette.testclient.TestClient.post") as post_mock: + post_mock.side_effect = HTTPError("409 Client Error: Conflict") + with pytest.raises(DJTableAlreadyRegistered): + client.register_table( + catalog="default", + schema="store", + table="comments", + ) + with patch("starlette.testclient.TestClient.post") as post_mock: + post_mock.side_effect = Exception("Boom!") + with pytest.raises(DJClientException): + client.register_table( + catalog="default", + schema="store", + table="comments", + ) + + def test_register_view(self, client): # pylint: disable=unused-argument + """ + Verifies that registering a view works. + """ + try: + client.create_namespace("source") + except DJNamespaceAlreadyExists: + pass + store_comments = client.register_view( + catalog="default", + schema="store", + view="comments_view", + query="SELECT * FROM store.comments", + replace=True, + ) + assert store_comments.name == "source.default.store.comments_view" + assert ( + "source.default.store.comments_view" + in client.namespace("source.default.store").sources() + ) + # and that errors are handled properly + with patch("starlette.testclient.TestClient.post") as post_mock: + post_mock.side_effect = HTTPError("409 Client Error: Conflict") + with pytest.raises(DJViewAlreadyRegistered): + client.register_view( + catalog="default", + schema="store", + view="comments_view", + query="SELECT * FROM store.comments", + ) + with patch("starlette.testclient.TestClient.post") as post_mock: + post_mock.side_effect = Exception("Boom!") + with pytest.raises(DJClientException): + client.register_view( + catalog="default", + schema="store", + view="comments_view", + query="SELECT * FROM store.comments", + replace=True, + ) + + def test_create_and_update_node(self, client): # pylint: disable=unused-argument + """ + Verifies that creating nodes works. + """ + # create it + account_type_table = client.create_source( + name="default.account_type_table", + description="A source table for account type data", + display_name="Account Type Table", + catalog="default", + schema="store", + table="account_type_table", + columns=[ + Column(name="id", type="int"), + Column(name="account_type_name", type="string"), + Column(name="account_type_classification", type="int"), + Column(name="preferred_payment_method", type="int"), + ], + mode=NodeMode.DRAFT, + ) + assert account_type_table.name == "default.account_type_table" + assert "default.account_type_table" in client.namespace("default").sources() + + # update it + # ... should fail since update_if_exists is set to False + with pytest.raises(DJClientException): + account_type_table = client.create_source( + name="default.account_type_table", + description="new description", + update_if_exists=False, + ) + + # ... should work since update_if_exists is set to True + account_type_table = client.create_source( + name="default.account_type_table", + description="new description", + update_if_exists=True, + ) + assert account_type_table.description == "new description" + + def test_saving_a_node(self, client): + """ + Verifies that saving a node works. + """ + client._create_node = MagicMock(return_value=MagicMock(status_code=200)) + client._update_node_tags = MagicMock(return_value=MagicMock(status_code=200)) + + client.create_tag( + name="foo", + description="Foo", + tag_type="test", + tag_metadata={"foo": "bar"}, + ) + account_type_table = client.create_source( + name="default.account_type_table", + description="A source table for account type data", + display_name="Account Type Table", + catalog="default", + schema="store", + table="account_type_table", + columns=[ + Column(name="id", type="int"), + Column(name="account_type_name", type="string"), + Column(name="account_type_classification", type="int"), + Column(name="preferred_payment_method", type="int"), + ], + mode=NodeMode.DRAFT, + tags=["foo"], + update_if_exists=True, + ) + assert account_type_table.name == "default.account_type_table" + assert account_type_table.display_name == "Account Type Table" + + new_node = account_type_table + new_node.name = "default.account_type_table_new" + new_node.display_name = "New: Account Type Table" + new_node.save() + new_node.refresh() + assert new_node.name == "default.account_type_table_new" + assert new_node.display_name == "New: Account Type Table" + + def test_create_nodes(self, client): # pylint: disable=unused-argument + """ + Verifies that creating nodes works. + """ + client.create_tag( + name="foo", + description="Foo Bar", + tag_type="test", + tag_metadata={"foo": "bar"}, + update_if_exists=True, + ) + foo_tag = client.tag("foo") + # source nodes + account_type_table = client.create_source( + name="default.account_type_table", + description="A source table for account type data", + display_name="Account Type Table", + catalog="default", + schema="store", + table="account_type_table", + columns=[ + Column(name="id", type="int"), + Column(name="account_type_name", type="string"), + Column(name="account_type_classification", type="int"), + Column(name="preferred_payment_method", type="int"), + ], + tags=[foo_tag], + mode=NodeMode.PUBLISHED, + update_if_exists=True, + ) + assert account_type_table.name == "default.account_type_table" + assert [tag["name"] for tag in account_type_table.tags] == ["foo"] + assert "default.account_type_table" in client.namespace("default").sources() + + payment_type_table = client.create_source( + name="default.payment_type_table", + description="A source table for payment type data", + display_name="Payment Type Table", + catalog="default", + schema="store", + table="payment_type_table", + columns=[ + Column(name="id", type="int"), + Column(name="payment_type_name", type="string"), + Column(name="payment_type_classification", type="string"), + ], + tags=[foo_tag], + mode=NodeMode.PUBLISHED, + ) + assert payment_type_table.name == "default.payment_type_table" + assert [tag["name"] for tag in payment_type_table.tags] == ["foo"] + assert "default.payment_type_table" in client.namespace("default").sources() + + revenue = client.create_source( + name="default.revenue", + description="Record of payments", + display_name="Payment Records", + catalog="default", + schema="accounting", + table="revenue", + columns=[ + Column(name="payment_id", type="int"), + Column(name="payment_amount", type="float"), + Column(name="payment_type", type="int"), + Column(name="customer_id", type="int"), + Column(name="account_type", type="string"), + ], + mode=NodeMode.PUBLISHED, + ) + assert revenue.name == "default.revenue" + assert "default.revenue" in client.namespace("default").sources() + + # dimension nodes + payment_type_dim = client.create_dimension( + name="default.payment_type", + description="Payment type dimension", + display_name="Payment Type", + query=( + "SELECT id, payment_type_name, payment_type_classification " + "FROM default.payment_type_table" + ), + primary_key=["id"], + mode=NodeMode.DRAFT, + tags=[foo_tag], + ) + payment_type_dim.validate() # pylint: disable=protected-access + assert payment_type_dim.name == "default.payment_type" + assert "default.payment_type" in client.list_dimensions(namespace="default") + assert [tag["name"] for tag in payment_type_dim.tags] == ["foo"] + payment_type_dim.publish() # Test changing a draft node to published + payment_type_dim.refresh() + assert payment_type_dim.mode == NodeMode.PUBLISHED + + account_type_dim = client.create_dimension( + name="default.account_type", + description="Account type dimension", + display_name="Account Type", + query=( + "SELECT id, account_type_name, " + "account_type_classification FROM " + "default.account_type_table" + ), + primary_key=["id"], + mode=NodeMode.PUBLISHED, + ) + assert account_type_dim.name == "default.account_type" + assert len(account_type_dim.columns) == 3 + assert "default.account_type" in client.list_dimensions(namespace="default") + + # transform nodes + large_revenue_payments_only = client.create_transform( + name="default.large_revenue_payments_only", + description="Only large revenue payments", + query=( + "SELECT payment_id, payment_amount, customer_id, account_type " + "FROM default.revenue WHERE payment_amount > 1000000" + ), + mode=NodeMode.PUBLISHED, + tags=[foo_tag], + custom_metadata={"foo": "bar"}, + ) + assert large_revenue_payments_only.name == "default.large_revenue_payments_only" + assert ( + "default.large_revenue_payments_only" + in client.namespace("default").transforms() + ) + assert len(large_revenue_payments_only.columns) == 4 + assert [tag["name"] for tag in large_revenue_payments_only.tags] == ["foo"] + assert large_revenue_payments_only.custom_metadata == {"foo": "bar"} + + client.transform("default.large_revenue_payments_only") + + result = large_revenue_payments_only.add_materialization( + Materialization( + job=MaterializationJobType.SPARK_SQL, + strategy=MaterializationStrategy.FULL, + schedule="0 * * * *", + config={}, + ), + ) + assert result == { + "message": "Successfully updated materialization config named `spark_sql__full` for " + "node `default.large_revenue_payments_only`", + "urls": [["http://fake.url/job"]], + } + + result = large_revenue_payments_only.deactivate_materialization( + materialization_name="spark_sql__full", + ) + assert result == { + "message": "The materialization named `spark_sql__full` on node " + "`default.large_revenue_payments_only` has been successfully deactivated", + } + + large_revenue_payments_and_business_only = client.create_transform( + name="default.large_revenue_payments_and_business_only", + description="Only large revenue payments from business accounts", + query=( + "SELECT payment_id, payment_amount, customer_id, account_type " + "FROM default.revenue WHERE " + "default.large_revenue_payments_and_business_only > 1000000 " + "AND account_type='BUSINESS'" + ), + mode=NodeMode.PUBLISHED, + ) + assert ( + large_revenue_payments_and_business_only.name + == "default.large_revenue_payments_and_business_only" + ) + assert ( + "default.large_revenue_payments_and_business_only" + in client.namespace( + "default", + ).transforms() + ) + + # metric nodes + number_of_account_types = client.create_metric( + name="default.number_of_account_types", + description="Total number of account types", + query="SELECT count(id) FROM default.account_type", + mode=NodeMode.PUBLISHED, + ) + assert number_of_account_types.name == "default.number_of_account_types" + assert "default.number_of_account_types" in client.list_metrics( + namespace="default", + ) + assert number_of_account_types.required_dimensions is None + assert number_of_account_types.metric_metadata is None + assert [tag["name"] for tag in number_of_account_types.tags] == [] + + # Test updating metric node + number_of_account_types = client.create_metric( + name="default.number_of_account_types", + query="SELECT count(*) FROM default.account_type", + required_dimensions=["account_type_name"], + direction=MetricDirection.HIGHER_IS_BETTER, + unit=MetricUnit.UNITLESS, + tags=[foo_tag], + update_if_exists=True, + ) + assert number_of_account_types.name == "default.number_of_account_types" + assert number_of_account_types.required_dimensions == ["account_type_name"] + assert ( + number_of_account_types.metric_metadata["direction"] == "higher_is_better" + ) + assert number_of_account_types.metric_metadata["unit"]["name"] == "UNITLESS" + + assert ( + number_of_account_types.query == "SELECT count(*) FROM default.account_type" + ) + assert number_of_account_types.description == "Total number of account types" + assert number_of_account_types.mode == "published" + assert [dim["name"] for dim in number_of_account_types.dimensions()] == [ + "default.account_type.account_type_classification", + "default.account_type.account_type_name", + "default.account_type.id", + ] + assert [tag["name"] for tag in number_of_account_types.tags] == ["foo"] + + # test setting required dims, direction, and unit at creation time (not update) + number_of_account_types2 = client.create_metric( + name="default.number_of_account_types2", + description="Total number of account types", + query="SELECT count(id) FROM default.account_type", + required_dimensions=["account_type_name"], + direction=MetricDirection.HIGHER_IS_BETTER, + unit=MetricUnit.UNITLESS, + tags=[foo_tag], + mode=NodeMode.PUBLISHED, + ) + assert number_of_account_types2.name == "default.number_of_account_types2" + assert number_of_account_types2.required_dimensions == ["account_type_name"] + assert ( + number_of_account_types2.metric_metadata["direction"] == "higher_is_better" + ) + assert number_of_account_types2.metric_metadata["unit"]["name"] == "UNITLESS" + + # cube nodes + cube_one = client.create_cube( + name="default.cube_one", + description="Ice ice cube.", + metrics=["default.number_of_account_types"], + dimensions=["default.account_type.account_type_name"], + mode=NodeMode.PUBLISHED, + tags=[foo_tag], + ) + assert cube_one.name == "default.cube_one" + assert cube_one.status == "valid" + assert cube_one.metrics == ["default.number_of_account_types"] + assert [tag["name"] for tag in cube_one.tags] == ["foo"] + + # Test updating cube node + cube_one = client.create_cube( + name="default.cube_one", + description="Ice cubes!", + metrics=[ + "default.number_of_account_types", + "default.number_of_account_types2", + ], + dimensions=["default.account_type.account_type_name"], + mode=NodeMode.PUBLISHED, + tags=[], + update_if_exists=True, + ) + assert cube_one.name == "default.cube_one" + assert cube_one.description == "Ice cubes!" + assert cube_one.metrics == [ + "default.number_of_account_types", + "default.number_of_account_types2", + ] + assert [tag["name"] for tag in cube_one.tags] == [] + + def test_link_unlink_dimension(self, client): # pylint: disable=unused-argument + """ + Check that linking and unlinking dimensions to a node's column works + """ + repair_type = client.source("foo.bar.repair_type") + result = repair_type.link_dimension( + "contractor_id", + "foo.bar.contractor", + "contractor_id", + ) + assert result["message"] == ( + "The dimension link between foo.bar.repair_type and foo.bar.contractor " + "has been successfully updated." + ) + + # Unlink the dimension + result = repair_type.unlink_dimension( + "contractor_id", + "foo.bar.contractor", + "contractor_id", + ) + assert result["message"] == ( + "Dimension link foo.bar.contractor to node foo.bar.repair_type has been removed." + ) + + def test_link_complex_dimension(self, client): + """ + Check that linking complex dimensions to a node works as expected + """ + + repair_type = client.source("foo.bar.repair_type") + result = repair_type.link_complex_dimension( + dimension_node="foo.bar.contractor", + join_type="inner", + join_on="foo.bar.repair_type.contractor_id = foo.bar.contractor.contractor_id", + role="repair_contractor", + ) + assert result["message"] == ( + "Dimension node foo.bar.contractor has been " + "successfully linked to node foo.bar.repair_type." + ) + + # Unlink the dimension + result = repair_type.remove_complex_dimension_link( + dimension_node="foo.bar.contractor", + role="repair_contractor", + ) + assert result["message"] == ( + "Dimension link foo.bar.contractor (role repair_contractor) to " + "node foo.bar.repair_type has been removed." + ) + + def test_add_remove_reference_dimension_link(self, client): + """ + Check that adding a reference dimension link works as expected + """ + + repair_type = client.source("foo.bar.repair_type") + result = repair_type.add_reference_dimension_link( + node_column="contractor_id", + dimension_node="foo.bar.contractor", + dimension_column="contractor_id", + ) + assert result["message"] == ( + "foo.bar.repair_type.contractor_id has been successfully linked " + "to foo.bar.contractor.contractor_id" + ) + + # Unlink the dimension + result = repair_type.remove_reference_dimension_link( + node_column="contractor_id", + ) + assert result["message"] == ( + "The reference dimension link on foo.bar.repair_type.contractor_id has been removed." + ) + + def test_sql(self, client): # pylint: disable=unused-argument + """ + Check that getting sql via the client works as expected. + """ + + # Retrieve SQL for multiple metrics using the client object + result = client.sql( + metrics=["default.total_repair_cost", "default.avg_repair_price"], + dimensions=[ + "default.hard_hat.city", + "default.hard_hat.state", + "default.dispatcher.company_name", + ], + filters=["default.hard_hat.state = 'AZ'"], + engine_name="spark", + engine_version="3.1.1", + ) + assert "SELECT" in result and "FROM" in result + + result = client.sql(metrics=["foo.bar.avg_repair_price"]) + assert "SELECT" in result and "FROM" in result + + # Retrieve SQL for a single metric + result = client.sql( + metrics=["foo.bar.avg_repair_price"], + dimensions=["foo.bar.dimension_that_does_not_exist"], + filters=[], + ) + assert ( + result["message"] + == "Please make sure that `dimension_that_does_not_exist` is a dimensional attribute." + or result["message"] + == "foo.bar.dimension_that_does_not_exist are not available dimensions on " + "foo.bar.avg_repair_price" + ) + + # Should fail due to dimension not being available + result = client.sql( + metrics=["foo.bar.num_repair_orders", "foo.bar.avg_repair_price"], + dimensions=["default.hard_hat.city"], + filters=["default.hard_hat.state = 'AZ'"], + engine_name="spark", + engine_version="3.1.1", + ) + assert result["message"] == ( + "The dimension attribute `default.hard_hat.city` is not available on " + "every metric and thus cannot be included." + ) or result["message"] == ( + "default.hard_hat.city are not available dimensions on " + "foo.bar.num_repair_orders, foo.bar.avg_repair_price" + ) + + def test_get_dimensions(self, client): + """ + Check that `metric.dimensions()` works as expected. + """ + metric = client.metric(node_name="foo.bar.avg_repair_price") + result = metric.dimensions() + assert { + "name": "foo.bar.dispatcher.company_name", + "type": "string", + "node_name": "foo.bar.dispatcher", + "node_display_name": "Dispatcher", + "properties": [], + "path": [ + "foo.bar.repair_order_details", + "foo.bar.repair_order", + ], + "filter_only": False, + } in result + + def test_create_namespace(self, client): + """ + Verifies that creating a new namespace works. + """ + with pytest.raises(DJClientException) as exc_info: + client.namespace(namespace="roads.demo") + assert "Namespace `roads.demo` does not exist" in str(exc_info.value) + + namespace = client.create_namespace(namespace="roads.demo") + assert namespace.namespace == "roads.demo" + + with pytest.raises(DJNamespaceAlreadyExists) as exc_info: + client.create_namespace(namespace="roads.demo") + assert "Node namespace `roads.demo` already exists" in str(exc_info.value) + + def test_create_delete_restore_namespace(self, client): + """ + Verifies that deleting a new namespace works. + """ + # create it first + namespace = client.create_namespace(namespace="roads.demo.foo") + assert namespace.namespace == "roads.demo.foo" + with pytest.raises(DJNamespaceAlreadyExists) as exc_info: + client.create_namespace(namespace="roads.demo.foo") + assert "Node namespace `roads.demo.foo` already exists" in str(exc_info.value) + + # then delete it + response = client.delete_namespace(namespace="roads.demo.foo") + assert response is None + with pytest.raises(DJClientException) as exc_info: + client.delete_namespace(namespace="roads.demo.foo") + assert "Namespace `roads.demo.foo` is already deactivated." in str( + exc_info.value, + ) + + # and then restore it + response = client.restore_namespace(namespace="roads.demo.foo") + assert response is None + with pytest.raises(DJClientException) as exc_info: + client.restore_namespace(namespace="roads.demo.foo") + assert "Node namespace `roads.demo.foo` already exists and is active" in str( + exc_info.value, + ) + + def test_get_node_revisions(self, client): + """ + Verifies that retrieving node revisions works + """ + local_hard_hats = client.dimension("foo.bar.local_hard_hats") + local_hard_hats.display_name = "local hard hats" + local_hard_hats.description = "Local hard hats dimension" + local_hard_hats.save() + local_hard_hats.primary_key = ["hard_hat_id", "last_name"] + local_hard_hats.save() + revs = local_hard_hats.list_revisions() + assert len(revs) == 3 + assert [rev["version"] for rev in revs] == ["v1.0", "v1.1", "v2.0"] + + def test_update_node_with_query(self, client): + """ + Verify that updating a node with a query works + """ + local_hard_hats = client.dimension("default.local_hard_hats") + local_hard_hats.query = """ + SELECT + hh.hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id, + hhs.state_id AS state_id + FROM default.hard_hats hh + LEFT JOIN default.hard_hat_state hhs + ON hh.hard_hat_id = hhs.hard_hat_id + WHERE hh.state_id = 'CA' + """ + response = local_hard_hats.save() + assert "WHERE hh.state_id = 'CA'" in response["query"] + assert response["version"] == "v2.0" + + local_hard_hats.display_name = "local hard hats" + local_hard_hats.description = "Local hard hats dimension" + response = local_hard_hats.save() + assert response["display_name"] == "local hard hats" + assert response["description"] == "Local hard hats dimension" + assert response["version"] == "v2.1" + + local_hard_hats.primary_key = ["hard_hat_id", "last_name"] + response = local_hard_hats.save() + + assert response["version"] == "v3.0" + assert { + "name": "hard_hat_id", + "type": "int", + "display_name": "Hard Hat Id", + "attributes": [ + {"attribute_type": {"namespace": "system", "name": "primary_key"}}, + ], + "dimension": None, + "partition": None, + } in response["columns"] + assert { + "name": "last_name", + "type": "string", + "display_name": "Last Name", + "attributes": [ + {"attribute_type": {"namespace": "system", "name": "primary_key"}}, + ], + "dimension": None, + "partition": None, + } in response["columns"] + + def test_update_custom_metadata(self, client): + """ + Verify that updating a node's custom metadata works. + """ + transform_node = client.transform("foo.bar.with_custom_metadata") + assert transform_node.custom_metadata == {"foo": "bar"} + + # update + transform_node.custom_metadata = {"bar": "baz"} + transform_node.save() + + # check again + transform_node_again = client.transform("foo.bar.with_custom_metadata") + assert transform_node_again.custom_metadata == {"bar": "baz"} + + def test_update_source_node(self, client): + """ + Verify that updating a source node's columns works + """ + us_states = client.source("default.us_states") + new_columns = [ + {"name": "state_id", "type": "int"}, + {"name": "name", "type": "string"}, + {"name": "abbr", "type": "string"}, + {"name": "region", "type": "int"}, + ] + us_states.columns = new_columns + response = us_states.save() + assert response["columns"] == [ + { + "attributes": [], + "dimension": None, + "display_name": "State Id", + "name": "state_id", + "type": "int", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Name", + "name": "name", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Abbr", + "name": "abbr", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Region", + "name": "region", + "type": "int", + "partition": None, + }, + ] + assert response["version"] == "v2.0" + + def test_add_availability(self, client): + """ + Verify adding an availability state to a node + """ + dim = client.dimension(node_name="default.contractor") + response = dim.add_availability( + AvailabilityState( + catalog="default", + schema_="materialized", + table="contractor", + valid_through_ts=1688660209, + ), + ) + assert response == {"message": "Availability state successfully posted"} + + def test_set_column_attributes(self, client): + """ + Verify setting column attributes on a node + """ + dim = client.source(node_name="default.contractors") + response = dim.set_column_attributes( + "contact_title", + [ + ColumnAttribute( + name="dimension", + ), + ], + ) + assert response == [ + { + "attributes": [ + {"attribute_type": {"name": "dimension", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "Contact Title", + "name": "contact_title", + "type": "string", + "partition": None, + }, + ] + + def test_set_column_display_name(self, client): + """ + Verify setting a column's display name + """ + dim = client.source(node_name="default.contractors") + response = dim.set_column_display_name( + "contact_title", + "My Contact's Title", + ) + assert response["display_name"] == "My Contact's Title" + + # + # Tags + # + def test_creating_a_tag(self, client): + """ + Test creating a tag + """ + client.create_tag( + name="foo.one", + description="Foo Bar", + tag_type="test", + tag_metadata={"foo": "bar"}, + ) + tag = client.tag("foo.one") + assert tag.name == "foo.one" + assert tag.description == "Foo Bar" + assert tag.tag_type == "test" + assert tag.tag_metadata == {"foo": "bar"} + + def test_tag_already_exists(self, client): + """ + Test that the client raises properly when a tag already exists + """ + client.create_tag( + name="foo.two", + description="Foo Bar", + tag_type="test", + tag_metadata={"foo": "bar"}, + ) + # update if a tag exists + client.create_tag( + name="foo.two", + description="Foo Bar", + tag_type="test", + tag_metadata={"foo": "bar"}, + update_if_exists=True, + ) + # fail if a tag exists + with pytest.raises(DJTagAlreadyExists) as exc_info: + client.create_tag( + name="foo.two", + description="Foo Bar", + tag_type="test", + tag_metadata={"foo": "bar"}, + ) + assert "Tag `foo.two` already exists" in str(exc_info.value) + + def test_updating_a_tag(self, client): + """ + Test updating a tag + """ + client.create_tag( + name="foo.three", + description="Foo Bar", + tag_type="test", + tag_metadata={"foo": "bar"}, + ) + tag = client.tag("foo.three") + assert tag.name == "foo.three" + assert tag.description == "Foo Bar" + assert tag.tag_type == "test" + assert tag.tag_metadata == {"foo": "bar"} + tag.description = "This is an updated description." + tag.save() + # refresh the tag + repulled_tag = client.tag("foo.three") + repulled_tag.refresh() + assert repulled_tag.description == "This is an updated description." + + def test_tag_does_not_exist(self, client): + """ + Test that the client raises properly when a tag does not exist + """ + with pytest.raises(DJClientException) as exc_info: + client.tag("does-not-exist") + assert "Tag `does-not-exist` does not exist" in str(exc_info.value) + + def test_tag_a_node(self, client): + """ + Test that a node can be tagged properly + """ + client.create_tag( + name="foo.four", + description="Foo Bar", + tag_type="test", + tag_metadata={"foo": "bar"}, + ) + tag = client.tag("foo.four") + node = client.source("default.repair_orders") + node.tags.append(tag) + node.save() + repull_node = client.source("default.repair_orders") + assert [tag.to_dict() for tag in repull_node.tags] == [tag.to_dict()] + + def test_list_nodes_with_tags(self, client): + """ + Test that we can list nodes with tags. + """ + # create some tags + tag_foo = client.create_tag( + name="foo.five", + description="Foo", + tag_type="test", + tag_metadata={"foo": "bar"}, + ) + tag_bar = client.create_tag( + name="bar", + description="Bar", + tag_type="test", + tag_metadata={"foo": "bar"}, + ) + + # tag some nodes + node_one = client.source("default.repair_orders") + node_one.tags.append(tag_foo) + node_one.tags.append(tag_bar) + node_one.save() + + node_two = client.metric("default.num_repair_orders") + node_two.tags.append(tag_foo) + node_two.save() + + node_three = client.dimension("default.repair_order") + node_three.tags.append(tag_foo) + node_three.tags.append(tag_bar) + node_three.save() + + # list nodes with tags + nodes_with_foo = client.list_nodes_with_tags(tag_names=["foo.five"]) + nodes_with_foo_and_bar = client.list_nodes_with_tags( + tag_names=["bar", "foo.five"], + ) + + # evaluate + with pytest.raises(DJClientException): + client.list_nodes_with_tags(tag_names=["does-not-exist"]) + assert ( + client.list_nodes_with_tags(tag_names=["does-not-exist"], skip_missing=True) + == [] + ) + assert set(nodes_with_foo) == set( + [ + "default.repair_order", + "default.repair_orders", + "default.num_repair_orders", + ], + ) + assert set(nodes_with_foo_and_bar) == set( + ["default.repair_orders", "default.repair_order"], + ) diff --git a/datajunction-clients/python/tests/test_cli.py b/datajunction-clients/python/tests/test_cli.py new file mode 100644 index 000000000..585c29f3f --- /dev/null +++ b/datajunction-clients/python/tests/test_cli.py @@ -0,0 +1,88 @@ +"""Tests DJ CLI""" +import os +import sys +from io import StringIO +from typing import Callable +from unittest import mock +from unittest.mock import patch + +import pytest + +from datajunction import DJBuilder +from datajunction.cli import main + + +def test_deploy( + change_to_project_dir: Callable, +): + """ + Test `dj deploy ` + """ + builder_client = mock.MagicMock() + + # Test deploy with dryrun + change_to_project_dir("./") + test_args = ["dj", "deploy", "./project9", "--dryrun"] + with patch.object(sys, "argv", test_args): + main(builder_client=builder_client) + + func_names = [mock_call[0] for mock_call in builder_client.mock_calls] + assert "basic_login" in func_names + assert "create_namespace" in func_names + assert "create_source" in func_names + assert "create_dimension" in func_names + assert "delete_namespace" in func_names + + # Test deploy without dryrun + change_to_project_dir("./") + test_args = ["dj", "deploy", "./project9"] + with patch.object(sys, "argv", test_args): + main(builder_client=builder_client) + + func_names = [mock_call[0] for mock_call in builder_client.mock_calls] + assert "basic_login" in func_names + assert "create_namespace" in func_names + assert "create_source" in func_names + assert "create_dimension" in func_names + assert "delete_namespace" in func_names + + +def test_pull( + tmp_path, + builder_client: DJBuilder, # pylint: disable=redefined-outer-name +): + """ + Test `dj pull ` + """ + test_args = ["dj", "pull", "default", tmp_path.absolute().as_posix()] + with patch.object(sys, "argv", test_args): + main(builder_client=builder_client) + assert len(os.listdir(tmp_path)) == 30 + + +def test_help(builder_client: DJBuilder): # pylint: disable=redefined-outer-name + """ + Test the '--help' output. + """ + test_args = ["dj", "--help"] + with patch.object(sys, "argv", test_args): + with patch("sys.stdout", new_callable=StringIO) as mock_stdout: + with pytest.raises(SystemExit) as excinfo: + main(builder_client=builder_client) + assert excinfo.value.code == 0 # Ensure exit code is 0 (success) + output = mock_stdout.getvalue() + assert "usage: dj" in output + assert "deploy" in output + assert "pull" in output + + +def test_invalid_command( + builder_client: DJBuilder, # pylint: disable=redefined-outer-name +): + """ + Test behavior for an invalid command. + """ + test_args = ["dj", "invalid_command"] + with patch.object(sys, "argv", test_args): + with pytest.raises(SystemExit): + main(builder_client=builder_client) diff --git a/datajunction-clients/python/tests/test_client.py b/datajunction-clients/python/tests/test_client.py new file mode 100644 index 000000000..7752cbe6e --- /dev/null +++ b/datajunction-clients/python/tests/test_client.py @@ -0,0 +1,503 @@ +"""Tests DJ client""" +import pandas +import pytest + +from datajunction import DJClient +from datajunction.exceptions import DJClientException +from datajunction.nodes import Cube, Dimension, Metric, Source, Transform + + +class TestDJClient: # pylint: disable=too-many-public-methods + """ + Tests for DJ client functionality. + """ + + @pytest.fixture + def client(self, module__session_with_examples): + """ + Returns a DJ client instance + """ + return DJClient(requests_session=module__session_with_examples) # type: ignore + + # + # List basic objects: namespaces, dimensions, metrics, cubes + # + def test_list_namespaces(self, client): + """ + Check that `client.list_namespaces()` works as expected. + """ + # full list + expected = ["default", "foo.bar"] + result = client.list_namespaces() + assert result == expected + + # partial list + partial = ["foo.bar"] + result = client.list_namespaces(prefix="foo") + assert result == partial + + def test_list_dimensions(self, client): + """ + Check that `client.list_dimensions()` works as expected. + """ + # full list + dims = client.list_dimensions() + assert set(dims) == { + "default.repair_order", + "default.contractor", + "default.hard_hat", + "default.local_hard_hats", + "default.us_state", + "default.dispatcher", + "default.municipality_dim", + "foo.bar.repair_order", + "foo.bar.contractor", + "foo.bar.hard_hat", + "foo.bar.local_hard_hats", + "foo.bar.us_state", + "foo.bar.dispatcher", + "foo.bar.municipality_dim", + } + + # partial list + dims = client.list_dimensions(namespace="foo.bar") + assert set(dims) == { + "foo.bar.repair_order", + "foo.bar.contractor", + "foo.bar.hard_hat", + "foo.bar.local_hard_hats", + "foo.bar.us_state", + "foo.bar.dispatcher", + "foo.bar.municipality_dim", + } + + def test_list_metrics(self, client): + """ + Check that `client.list_metrics()` works as expected. + """ + # full list + metrics = client.list_metrics() + assert set(metrics) == { + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + "default.avg_length_of_employment", + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + "default.avg_time_to_dispatch", + "foo.bar.num_repair_orders", + "foo.bar.avg_repair_price", + "foo.bar.total_repair_cost", + "foo.bar.avg_length_of_employment", + "foo.bar.total_repair_order_discounts", + "foo.bar.avg_repair_order_discounts", + "foo.bar.avg_time_to_dispatch", + } + + # partial list + metrics = client.list_metrics(namespace="foo.bar") + assert set(metrics) == { + "foo.bar.num_repair_orders", + "foo.bar.avg_repair_price", + "foo.bar.total_repair_cost", + "foo.bar.avg_length_of_employment", + "foo.bar.total_repair_order_discounts", + "foo.bar.avg_repair_order_discounts", + "foo.bar.avg_time_to_dispatch", + } + + def test_list_cubes(self, client): + """ + Check that `client.list_cubes()` works as expected. + """ + # full list + cubes = client.list_cubes() + assert set(cubes) == {"foo.bar.cube_one", "default.cube_two"} + + # partial list + cubes = client.list_cubes(namespace="foo.bar") + assert cubes == ["foo.bar.cube_one"] + + # + # List other nodes: sources, transforms, all. + # + def test_list_sources(self, client): + """ + Check that `client.list_sources()` works as expected. + """ + # full list + nodes = client.list_sources() + assert set(nodes) == { + "default.repair_orders", + "default.repair_orders_foo", + "default.repair_order_details", + "default.repair_type", + "default.contractors", + "default.municipality_municipality_type", + "default.municipality_type", + "default.municipality", + "default.dispatchers", + "default.hard_hats", + "default.hard_hat_state", + "default.us_states", + "default.us_region", + "foo.bar.repair_orders", + "foo.bar.repair_order_details", + "foo.bar.repair_type", + "foo.bar.contractors", + "foo.bar.municipality_municipality_type", + "foo.bar.municipality_type", + "foo.bar.municipality", + "foo.bar.dispatchers", + "foo.bar.hard_hats", + "foo.bar.hard_hat_state", + "foo.bar.us_states", + "foo.bar.us_region", + } + + # partial list + nodes = client.list_sources(namespace="foo.bar") + assert set(nodes) == { + "foo.bar.repair_orders", + "foo.bar.repair_order_details", + "foo.bar.repair_type", + "foo.bar.contractors", + "foo.bar.municipality_municipality_type", + "foo.bar.municipality_type", + "foo.bar.municipality", + "foo.bar.dispatchers", + "foo.bar.hard_hats", + "foo.bar.hard_hat_state", + "foo.bar.us_states", + "foo.bar.us_region", + } + + def test_list_transforms(self, client): + """ + Check that `client.list_transforms)()` works as expected. + """ + # full list + nodes = client.list_transforms() + assert set(nodes) == { + "default.repair_orders_thin", + "foo.bar.repair_orders_thin", + "foo.bar.with_custom_metadata", + } + + # partial list + nodes = client.list_transforms(namespace="foo.bar") + assert nodes == ["foo.bar.with_custom_metadata", "foo.bar.repair_orders_thin"] + + def test_list_nodes(self, client): + """ + Check that `client.list_nodes)()` works as expected. + """ + # full list + nodes = client.list_nodes() + assert set(nodes) == set( + [ + "default.repair_orders", + "default.repair_orders_foo", + "default.repair_order_details", + "default.repair_type", + "default.contractors", + "default.municipality_municipality_type", + "default.municipality_type", + "default.municipality", + "default.dispatchers", + "default.hard_hats", + "default.hard_hat_state", + "default.us_states", + "default.us_region", + "default.repair_order", + "default.contractor", + "default.hard_hat", + "default.local_hard_hats", + "default.us_state", + "default.dispatcher", + "default.municipality_dim", + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + "default.avg_length_of_employment", + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + "default.avg_time_to_dispatch", + "foo.bar.repair_orders", + "foo.bar.repair_order_details", + "foo.bar.repair_type", + "foo.bar.contractors", + "foo.bar.municipality_municipality_type", + "foo.bar.municipality_type", + "foo.bar.municipality", + "foo.bar.dispatchers", + "foo.bar.hard_hats", + "foo.bar.hard_hat_state", + "foo.bar.us_states", + "foo.bar.us_region", + "foo.bar.repair_order", + "foo.bar.contractor", + "foo.bar.hard_hat", + "foo.bar.local_hard_hats", + "foo.bar.us_state", + "foo.bar.dispatcher", + "foo.bar.municipality_dim", + "foo.bar.num_repair_orders", + "foo.bar.avg_repair_price", + "foo.bar.total_repair_cost", + "foo.bar.avg_length_of_employment", + "foo.bar.total_repair_order_discounts", + "foo.bar.avg_repair_order_discounts", + "foo.bar.avg_time_to_dispatch", + "foo.bar.cube_one", + "default.cube_two", + "default.repair_orders_thin", + "foo.bar.repair_orders_thin", + "foo.bar.with_custom_metadata", + ], + ) + + # partial list + nodes = client.list_nodes(namespace="foo.bar") + assert set(nodes) == set( + [ + "foo.bar.repair_orders", + "foo.bar.repair_order_details", + "foo.bar.repair_type", + "foo.bar.contractors", + "foo.bar.municipality_municipality_type", + "foo.bar.municipality_type", + "foo.bar.municipality", + "foo.bar.dispatchers", + "foo.bar.hard_hats", + "foo.bar.hard_hat_state", + "foo.bar.us_states", + "foo.bar.us_region", + "foo.bar.repair_order", + "foo.bar.contractor", + "foo.bar.hard_hat", + "foo.bar.local_hard_hats", + "foo.bar.us_state", + "foo.bar.dispatcher", + "foo.bar.municipality_dim", + "foo.bar.num_repair_orders", + "foo.bar.avg_repair_price", + "foo.bar.total_repair_cost", + "foo.bar.avg_length_of_employment", + "foo.bar.total_repair_order_discounts", + "foo.bar.avg_repair_order_discounts", + "foo.bar.avg_time_to_dispatch", + "foo.bar.cube_one", + "foo.bar.repair_orders_thin", + "foo.bar.with_custom_metadata", + ], + ) + + def test_find_nodes_with_dimension(self, client): + """ + Check that `dimension.linked_nodes()` works as expected. + """ + repair_order_dim = client.dimension("default.repair_order") + assert repair_order_dim.linked_nodes() == [ + "default.repair_order_details", + "default.avg_repair_price", + "default.total_repair_cost", + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + ] + + def test_refresh_source_node(self, client): + """ + Check that `Source.validate()` works as expected. + """ + # change the source node + source_node = client.source("default.repair_orders_foo") + version_before = source_node.current_version + response = source_node.validate() + assert response == "valid" + version_after = source_node.current_version + assert version_before and version_after and version_before != version_after + + # change the source node (but not really) + source_node = client.source("default.repair_orders_foo") + version_before = source_node.current_version + response = source_node.validate() + assert response == "valid" + version_after = source_node.current_version + assert version_before and version_after and version_before == version_after + + # + # Get common metrics and dimensions + # + def test_common_dimensions(self, client): + """ + Test that getting common dimensions for metrics works + """ + dims = client.common_dimensions( + metrics=["default.num_repair_orders", "default.avg_repair_price"], + ) + assert len(dims) == 28 + + # + # SQL and data + # + def test_data(self, client): + """ + Test data retreval for a metric and dimension(s) + """ + # Should throw error when no name or metrics are passed in + with pytest.raises(DJClientException): + client.node_data("") + + with pytest.raises(DJClientException): + client.data([]) + + # Retrieve data for a single metric + expected_df = pandas.DataFrame.from_dict( + { + "default_DOT_hard_hat_DOT_city": ["Foo", "Bar"], + "default_DOT_avg_repair_price": [1.0, 2.0], + }, + ) + + result = client.data( + metrics=["default.avg_repair_price"], + dimensions=["default.hard_hat.city"], + ) + pandas.testing.assert_frame_equal(result, expected_df) + + # Retrieve data for a single node + result = client.node_data( + node_name="default.avg_repair_price", + dimensions=["default.hard_hat.city"], + ) + pandas.testing.assert_frame_equal(result, expected_df) + + # No data + with pytest.raises(DJClientException) as exc_info: + client.data( + metrics=["default.avg_repair_price"], + dimensions=["default.hard_hat.state"], + ) + assert "No data for query!" in str(exc_info) + + # Error propagation + # with pytest.raises(DJClientException) as exc_info: + # client.data( + # metrics=["default.avg_repair_price"], + # dimensions=["default.hard_hat.postal_code"], + # ) + # assert "Error response from query service" in str(exc_info) + + def test_sql(self, client): + """ + Test SQL retrieval + """ + # Retrieve sql for metrics + result = client.sql( + metrics=["default.avg_repair_price", "default.num_repair_orders"], + dimensions=["default.hard_hat.city"], + filters=["default.hard_hat.state = 'NY'"], + ) + assert isinstance(result, str) + + # Retrieve sql for a node + result = client.node_sql( + node_name="default.repair_order_details", + dimensions=["default.hard_hat.city"], + filters=["default.hard_hat.state = 'NY'"], + ) + assert isinstance(result, str) + + # Retrieve sql for a node (error) + result = client.node_sql( + node_name="default.repair_order_details", + dimensions=["default.repair_order.repair_order_id1"], + filters=["default.repair_order.repair_order_id = 1222"], + ) + assert result["message"] == ( + "default.repair_order.repair_order_id1 are not available dimensions" + " on default.repair_order_details" + ) + + # Retrieve measures sql for metrics + result = client.sql( + metrics=["default.avg_repair_price", "default.num_repair_orders"], + dimensions=["default.hard_hat.city"], + filters=["default.hard_hat.state = 'NY'"], + measures=True, + ) + for generated_sql in result: + assert generated_sql["node"]["name"] in ( + "default.repair_order_details", + "default.repair_orders", + ) + assert isinstance(generated_sql["sql"], str) + + # + # Data Catalog and Engines + # + def test_list_catalogs(self, client): + """ + Check that `client.list_catalogs()` works as expected. + """ + result = client.list_catalogs() + assert set(result) == {"unknown", "draft", "default", "public"} + + def test_list_engines(self, client): + """ + Check that `client.list_engines()` works as expected. + """ + result = client.list_engines() + assert result == [ + {"name": "spark", "version": "3.1.1"}, + {"name": "postgres", "version": "15.2"}, + ] + + def test_get_dag(self, client): + """ + Check that `node.upstreams()`, `node.downstreams()`, and `node.dimensions()` + all work as expected + """ + num_repair_orders = client.metric("default.num_repair_orders") + result = num_repair_orders.get_upstreams() + assert result == ["default.repair_orders"] + result = num_repair_orders.get_downstreams() + assert result == ["default.cube_two"] + result = num_repair_orders.get_dimensions() + assert len(result) == 31 + + hard_hat = client.dimension("default.hard_hat") + result = hard_hat.get_upstreams() + assert result == ["default.hard_hats"] + result = hard_hat.get_downstreams() + assert result == [] + result = hard_hat.get_dimensions() + assert len(result) == 18 + + def test_get_node(self, client): + """ + Verifies that retrieving a node (of any type) works with: + dj.node() + """ + hard_hat = client.node("default.hard_hat") + assert isinstance(hard_hat, Dimension) + assert hard_hat.name == "default.hard_hat" + + num_repair_orders = client.node("default.num_repair_orders") + assert isinstance(num_repair_orders, Metric) + assert num_repair_orders.name == "default.num_repair_orders" + + repair_orders_thin = client.node("default.repair_orders_thin") + assert isinstance(repair_orders_thin, Transform) + assert repair_orders_thin.name == "default.repair_orders_thin" + + repair_orders = client.node("default.repair_orders") + assert isinstance(repair_orders, Source) + assert repair_orders.name == "default.repair_orders" + + cube_two = client.node("default.cube_two") + assert isinstance(cube_two, Cube) + assert cube_two.name == "default.cube_two" + assert cube_two.metrics == ["default.num_repair_orders"] + assert cube_two.dimensions == ["default.municipality_dim.local_region"] diff --git a/datajunction-clients/python/tests/test_compile.py b/datajunction-clients/python/tests/test_compile.py new file mode 100644 index 000000000..a0862a197 --- /dev/null +++ b/datajunction-clients/python/tests/test_compile.py @@ -0,0 +1,573 @@ +""" +Test YAML project related things +""" +# pylint: disable=unused-argument +import os +from typing import Callable +from unittest.mock import MagicMock, call + +import pytest +from fastapi.testclient import TestClient +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction import DJBuilder +from datajunction.compile import CompiledProject, Project, find_project_root +from datajunction.exceptions import DJClientException, DJDeploymentFailure +from datajunction.models import MetricDirection, MetricUnit, NodeMode + + +def test_compiled_project_deploy_namespaces(): + """ + Test deploying a namespace. + """ + mock_table = MagicMock() + cp = CompiledProject( + name="foo", + prefix="foo.fix", + root_path="/foo", + namespaces=["foo", "bar"], + ) + # namespace creation random error + builder_client = MagicMock( + create_namespace=MagicMock(side_effect=DJClientException("foo error")), + ) + cp._deploy_namespaces( # pylint: disable=protected-access + prefix="foo", + table=mock_table, + client=builder_client, + ) + assert cp.errors[0]["error"] == "foo error" + # namespace already exists, not an error + cp = CompiledProject( + name="foo", + prefix="foo.fix", + root_path="/foo", + namespaces=["foo", "bar"], + ) + builder_client = MagicMock( + create_namespace=MagicMock( + side_effect=DJClientException("foo bar already exists"), + ), + ) + cp._deploy_namespaces( # pylint: disable=protected-access + prefix="foo", + table=mock_table, + client=builder_client, + ) + assert cp.errors == [] + assert mock_table.add_row.call_args == call( + "foo.bar", + "namespace", + "[i][yellow]Namespace foo.bar already exists", + ) + + +def test_compiled_project__cleanup_namespace(): + """ + Test cleaning up a namespace. + """ + cp = CompiledProject( + name="foo", + prefix="foo.fix", + mode=NodeMode.DRAFT, + root_path="/foo", + ) + builder_client = MagicMock( + delete_namespace=MagicMock(side_effect=DJClientException("foo error")), + ) + cp._cleanup_namespace( # pylint: disable=protected-access + prefix="foo", + client=builder_client, + ) + assert cp.errors[0]["error"] == "foo error" + + +def test_find_project_root(): + """ + Test finding the project root + """ + with pytest.raises(DJClientException) as exc_info: + find_project_root(directory="foo") + assert "Directory foo does not exist" in str(exc_info) + + +def test_compile_loading_a_project(change_to_project_dir: Callable): + """ + Test loading a project + """ + change_to_project_dir("project1") + project = Project.load_current() + assert project.name == "My DJ Project 1" + assert project.prefix == "projects.project1" + assert project.tags[0].name == "deprecated" + assert project.build.priority == [ + "roads.date", + "roads.date_dim", + "roads.repair_orders", + "roads.repair_order_transform", + "roads.repair_order_details", + "roads.contractors", + "roads.hard_hats", + "roads.hard_hat_state", + "roads.us_states", + "roads.us_region", + "roads.dispatchers", + "roads.municipality", + "roads.municipality_municipality_type", + "roads.municipality_type", + ] + assert project.mode == NodeMode.PUBLISHED + assert project.root_path.endswith("project1") + + +def test_load_project_from_different_dir(change_to_project_dir: Callable): + """ + Test loading a project + """ + change_to_project_dir("./") + project = Project.load("./project1") + assert project.name == "My DJ Project 1" + assert project.prefix == "projects.project1" + assert project.tags[0].name == "deprecated" + assert project.build.priority == [ + "roads.date", + "roads.date_dim", + "roads.repair_orders", + "roads.repair_order_transform", + "roads.repair_order_details", + "roads.contractors", + "roads.hard_hats", + "roads.hard_hat_state", + "roads.us_states", + "roads.us_region", + "roads.dispatchers", + "roads.municipality", + "roads.municipality_municipality_type", + "roads.municipality_type", + ] + assert project.mode == NodeMode.PUBLISHED + assert project.root_path.endswith("project1") + + +def test_compile_loading_a_project_from_a_nested_dir(change_to_project_dir: Callable): + """ + Test loading a project while in a nested directory + """ + change_to_project_dir("project1") + os.chdir(os.path.join(os.getcwd(), "roads")) + project = Project.load_current() + assert project.name == "My DJ Project 1" + assert project.prefix == "projects.project1" + assert project.tags[0].name == "deprecated" + assert project.build.priority == [ + "roads.date", + "roads.date_dim", + "roads.repair_orders", + "roads.repair_order_transform", + "roads.repair_order_details", + "roads.contractors", + "roads.hard_hats", + "roads.hard_hat_state", + "roads.us_states", + "roads.us_region", + "roads.dispatchers", + "roads.municipality", + "roads.municipality_municipality_type", + "roads.municipality_type", + ] + assert project.mode == NodeMode.PUBLISHED + assert project.root_path.endswith("project1") + + +def test_compile_loading_a_project_from_a_flat_dir(change_to_project_dir: Callable): + """ + Test loading a project where everythign is flat (no sub-directories) + """ + change_to_project_dir("project11") + project = Project.load_current() + assert project.name == "My DJ Project 11" + assert project.prefix == "projects.project11" + assert project.tags[0].name == "deprecated" + assert project.build.priority == [ + "roads.date", + "roads.date_dim", + "roads.repair_orders", + "roads.repair_order_transform", + "roads.repair_order_details", + "roads.contractors", + "roads.hard_hats", + "roads.hard_hat_state", + "roads.us_states", + "roads.us_region", + "roads.dispatchers", + "roads.municipality", + "roads.municipality_municipality_type", + "roads.municipality_type", + ] + assert project.mode == NodeMode.PUBLISHED + assert project.root_path.endswith("project11") + + +def test_compile_raising_when_not_in_a_project_dir(): + """ + Test raising when using Project.load_current() while not in a project dir + """ + with pytest.raises(DJClientException) as exc_info: + Project.load_current() + assert ( + "Cannot find project root, make sure you've defined " + "a project in a dj.yaml file" + ) in str(exc_info.value) + + +def test_compile_compiling_a_project(change_to_project_dir: Callable): + """ + Test loading and compiling a project + """ + change_to_project_dir("project1") + project = Project.load_current() + compiled_project = project.compile() + assert compiled_project.name == "My DJ Project 1" + assert compiled_project.prefix == "projects.project1" + assert compiled_project.mode == NodeMode.PUBLISHED + assert compiled_project.build.priority == [ + "roads.date", + "roads.date_dim", + "roads.repair_orders", + "roads.repair_order_transform", + "roads.repair_order_details", + "roads.contractors", + "roads.hard_hats", + "roads.hard_hat_state", + "roads.us_states", + "roads.us_region", + "roads.dispatchers", + "roads.municipality", + "roads.municipality_municipality_type", + "roads.municipality_type", + ] + assert compiled_project.root_path.endswith("project1") + assert not compiled_project.validated + + +def test_compile_validating_a_project( + change_to_project_dir: Callable, + builder_client: DJBuilder, +): + """ + Test loading, compiling, and validating a project + """ + change_to_project_dir("project1") + project = Project.load_current() + compiled_project = project.compile() + compiled_project.validate(client=builder_client, with_cleanup=True) + + +def test_compile_deploying_a_project( + change_to_project_dir: Callable, + builder_client: DJBuilder, + module__session_with_examples: TestClient, +): + """ + Test loading, compiling, validating, and deploying a project + """ + change_to_project_dir("project1") + project = Project.load_current() + compiled_project = project.compile() + compiled_project.deploy(client=builder_client) # Deploying will validate as well + + # Check complex join dimension links + hard_hat = builder_client._session.get( # pylint: disable=protected-access + "/nodes/projects.project1.roads.hard_hat", + ).json() + assert [link["dimension"]["name"] for link in hard_hat["dimension_links"]] == [ + "projects.project1.roads.us_state", + "projects.project1.roads.date_dim", + "projects.project1.roads.date_dim", + ] + + # Check reference dimension links + local_hard_hats = builder_client._session.get( # pylint: disable=protected-access + "/nodes/projects.project1.roads.local_hard_hats", + ).json() + assert [ + link["dimension"]["name"] for link in local_hard_hats["dimension_links"] + ] == ["projects.project1.roads.us_state"] + assert [ + col["dimension"]["name"] + for col in local_hard_hats["columns"] + if col["name"] == "birth_date" + ] == ["projects.project1.roads.date_dim"] + + # Check metric metadata and required dimensions + avg_repair_price = builder_client.metric("projects.project1.roads.avg_repair_price") + assert avg_repair_price.metric_metadata is not None + assert avg_repair_price.metric_metadata.unit == MetricUnit.DOLLAR + assert ( + avg_repair_price.metric_metadata.direction == MetricDirection.HIGHER_IS_BETTER + ) + avg_length = builder_client.metric( + "projects.project1.roads.avg_length_of_employment", + ) + assert avg_length.required_dimensions == ["hard_hat_id"] + assert avg_length.metric_metadata is not None + assert avg_length.metric_metadata.unit == MetricUnit.SECOND + assert avg_length.metric_metadata.direction == MetricDirection.HIGHER_IS_BETTER + + # Check column-level settings + response = module__session_with_examples.get( + "/nodes/projects.project1.roads.contractor", + ).json() + assert response["columns"][1]["display_name"] == "Contractor Company Name" + response = module__session_with_examples.get( + "/nodes/projects.project1.roads.regional_level_agg", + ).json() + assert response["columns"][2]["display_name"] == "Location (Hierarchy)" + assert response["columns"][2]["attributes"] == [ + {"attribute_type": {"namespace": "system", "name": "dimension"}}, + ] + + # check custom metadata + national_level_agg = builder_client.transform( + "projects.project1.roads.national_level_agg", + ) + assert national_level_agg.custom_metadata == { + "level": "national", + "sublevel": "state", + } + + +def test_compile_redeploying_a_project( + change_to_project_dir: Callable, + builder_client: DJBuilder, +): + """ + Test deploying and then redeploying a project + """ + change_to_project_dir("project12") + project = Project.load_current() + compiled_project = project.compile() + compiled_project.deploy(client=builder_client) + compiled_project.deploy(client=builder_client) + + +def test_compile_raising_on_invalid_table_name( + change_to_project_dir: Callable, +): + """ + Test raising when a table name is missing a catalog + """ + change_to_project_dir("project2") + project = Project.load_current() + with pytest.raises(DJClientException) as exc_info: + project.compile() + assert ( + "Invalid table name roads.us_states, table name " + "must be fully qualified: ..
" + ) in str(exc_info.value) + + +def test_compile_raising_on_invalid_file_name( + change_to_project_dir: Callable, +): + """ + Test raising when a YAML file is missing a required node type identifier + """ + change_to_project_dir("project3") + project = Project.load_current() + with pytest.raises(DJClientException) as exc_info: + project.compile() + assert ( + "Invalid node definition filename some_node, node definition filename " + "must end with a node type i.e. my_node.source.yaml" + ) in str(exc_info.value) + + change_to_project_dir("project5") + project = Project.load_current() + with pytest.raises(DJClientException) as exc_info: + project.compile() + assert ( + "Invalid node definition filename stem some_node.a.b.c, stem must only have a " + "single dot separator and end with a node type i.e. my_node.source.yaml" + ) in str(exc_info.value) + + +def test_compile_error_on_invalid_dimension_link( + change_to_project_dir: Callable, + builder_client: DJBuilder, +): + """ + Test compiling and receiving an error on a dimension link + """ + change_to_project_dir("project7") + project = Project.load_current() + compiled_project = project.compile() + with pytest.raises(DJDeploymentFailure) as exc_info: + compiled_project.deploy(client=builder_client) + + assert str("Node definition contains references to nodes that do not exist") in str( + exc_info.value.errors[0], + ) + + +def test_compile_deeply_nested_namespace( + change_to_project_dir: Callable, + builder_client: DJBuilder, +): + """ + Test compiling a node in a deeply nested namespace + """ + change_to_project_dir("project4") + project = Project.load_current() + compiled_project = project.compile() + compiled_project.deploy(client=builder_client) + + +def test_compile_error_on_individual_node( + change_to_project_dir: Callable, + builder_client: DJBuilder, +): + """ + Test compiling and receiving an error on an individual node definition + """ + change_to_project_dir("project6") + project = Project.load_current() + compiled_project = project.compile() + with pytest.raises(DJDeploymentFailure) as exc_info: + compiled_project.deploy(client=builder_client) + + assert str("Node definition contains references to nodes that do not exist") in str( + exc_info.value.errors[0], + ) + + +def test_compile_raise_on_priority_with_node_missing_a_definition( + change_to_project_dir: Callable, +): + """ + Test raising an error when the priority list includes a node name + that has no corresponding definition + """ + change_to_project_dir("project8") + project = Project.load_current() + with pytest.raises(DJClientException) as exc_info: + project.compile() + + assert str( + "Build priority list includes node name " + "node.that.does.not.exist which has no corresponding definition", + ) in str(exc_info.value) + + +def test_compile_duplicate_tags( + change_to_project_dir: Callable, + builder_client: DJBuilder, +): + """ + Test that deploying duplicate tags are gracefully handled + """ + change_to_project_dir("project10") + project = Project.load_current() + compiled_project = project.compile() + compiled_project.deploy(client=builder_client) + compiled_project.deploy(client=builder_client) + + +@pytest.mark.asyncio +@pytest.mark.skip +async def test_deploy_remove_dimension_links( + change_to_project_dir: Callable, + builder_client: DJBuilder, + module__session: AsyncSession, + module__session_with_examples: TestClient, +): + """ + Test deploying nodes with dimension links removed + """ + change_to_project_dir("project9") + project = Project.load_current() + compiled_project = project.compile() + compiled_project.deploy(client=builder_client) + await module__session.commit() + response = module__session_with_examples.get( + "/nodes/projects.project7.roads.contractor", + ).json() + assert response["dimension_links"][0]["dimension"] == { + "name": "projects.project7.roads.us_state", + } + + change_to_project_dir("project12") + project = Project.load_current() + compiled_project = project.compile() + compiled_project.deploy(client=builder_client) + await module__session.commit() + response = module__session_with_examples.get( + "/nodes/projects.project7.roads.contractor", + ).json() + assert response["dimension_links"] == [] + + +def test_compile_pull_a_namespaces(builder_client: DJBuilder, tmp_path): + """ + Test pulling a namespace down into a local YAML project + """ + # Link a dimension so that we can test that path + node = builder_client.source("default.repair_order_details") + node.link_dimension(column="repair_order_id", dimension="default.repair_order") + + os.chdir(tmp_path) + Project.pull(client=builder_client, namespace="default", target_path=tmp_path) + project = Project.load_current() + assert project.name == "Project default (Autogenerated)" + assert project.prefix == "default" + compiled_project = project.compile() + + # The cube should be last in the topological ordering + assert compiled_project.build.priority[-1] == "cube_two" + + assert {node.name for node in compiled_project.definitions} == { + "municipality_municipality_type", + "contractors", + "hard_hats", + "municipality", + "repair_order_details", + "municipality_type", + "repair_type", + "us_states", + "dispatchers", + "hard_hat_state", + "repair_orders", + "repair_orders_foo", + "us_region", + "repair_orders_thin", + "repair_order", + "municipality_dim", + "dispatcher", + "contractor", + "local_hard_hats", + "us_state", + "hard_hat", + "total_repair_order_discounts", + "avg_repair_order_discounts", + "avg_time_to_dispatch", + "avg_length_of_employment", + "total_repair_cost", + "num_repair_orders", + "avg_repair_price", + "cube_two", + } + + +def test_compile_pull_raise_error_if_dir_not_empty(builder_client: DJBuilder, tmp_path): + """ + Test raising an error when pulling a namespace down into a non-empty directory + """ + os.chdir(tmp_path) + open( # pylint: disable=consider-using-with + "random_file.txt", + "w", + encoding="utf-8", + ).close() + with pytest.raises(DJClientException) as exc_info: + Project.pull(client=builder_client, namespace="default", target_path=tmp_path) + assert "The target path must be empty" in str(exc_info.value) diff --git a/datajunction-clients/python/tests/test_generated_client.py b/datajunction-clients/python/tests/test_generated_client.py new file mode 100644 index 000000000..f6d9120d5 --- /dev/null +++ b/datajunction-clients/python/tests/test_generated_client.py @@ -0,0 +1,106 @@ +"""Check the generated client code from various server endpoints.""" + + +class TestGeneratedClient: # pylint: disable=too-many-public-methods, protected-access + """ + Tests that verify the server's generated Python client code by running it + """ + + def run_generated_create_node_client_code( + self, + module__session_with_examples, + node: str, + ): + """ + Calls the server endpoint to get the generated client code for this node and runs it. + """ + response = module__session_with_examples.get( + f"/datajunction-clients/python/new_node/{node}?replace_namespace=roadscopy", + ) + create_node_client_code = str(response.json()) + create_node_client_code = create_node_client_code.replace( + "DJBuilder(DJ_URL)", + "DJBuilder(requests_session=module__session_with_examples)", + ).replace('dj.basic_login("dj", "dj")', "") + exec(create_node_client_code) # pylint: disable=exec-used + + def test_export_nodes(self, module__session_with_examples): + """ + Verify that the code generated by /datajunction-clients/python/new_node works + """ + response = module__session_with_examples.post("/namespaces/roadscopy") + assert response.status_code == 201 + + # Copy all source nodes to new namespace + for source_node in set( + module__session_with_examples.get("/nodes?node_type=source").json(), + ): + if source_node.startswith("default"): + response = module__session_with_examples.post( + f"/nodes/{source_node}/copy?new_name=roadscopy.{source_node.split('.')[-1]}", + ) + assert response.status_code == 200 + + # Verify that the remaining nodes can be recreated with the generated Python client code + # under the new namespace `roadscopy` + nodes = [ + node["name"] + for node in module__session_with_examples.get("/namespaces/default").json() + if node["type"] not in ("source", "cube") + ] + for node in nodes: + self.run_generated_create_node_client_code( + module__session_with_examples, + node, + ) + shortname = node.split(".")[-1] + result = module__session_with_examples.get( + f"/nodes/roadscopy.{shortname}", + ).json() + assert result["name"] == f"roadscopy.{shortname}" + assert "roadscopy" in result["query"] + + for node in nodes: + # Verify that the generated linking dimensions code also works + response = module__session_with_examples.get( + f"/datajunction-clients/python/dimension_links/{node}" + "?replace_namespace=roadscopy&include_client_setup=true", + ) + dimension_links_client_code = str(response.json()) + dimension_links_client_code = dimension_links_client_code.replace( + "DJBuilder(DJ_URL)", + "DJBuilder(requests_session=module__session_with_examples)", + ).replace('dj.basic_login("dj", "dj")', "") + exec(dimension_links_client_code) # pylint: disable=exec-used + + # Check linked dimensions + dimensions = module__session_with_examples.get( + "/nodes/roadscopy.num_repair_orders/dimensions", + ).json() + assert len(dimensions) == 31 + + nodes = [ + node["name"] + for node in module__session_with_examples.get("/namespaces/default").json() + if node["type"] in ("cube") + ] + # Check that cubes can now be created after the dimension linking + for node in nodes: + shortname = node.split(".")[-1] + self.run_generated_create_node_client_code( + module__session_with_examples, + node, + ) + result = module__session_with_examples.get( + f"/cubes/roadscopy.{shortname}", + ).json() + assert result["name"] == f"roadscopy.{shortname}" + assert result["cube_node_metrics"] == ["roadscopy.num_repair_orders"] + assert result["cube_node_dimensions"] == [ + "default.municipality_dim.local_region", + ] + + result = module__session_with_examples.get( + f"/nodes/roadscopy.{shortname}/dimensions", + ).json() + assert len(result) > 0 diff --git a/datajunction-clients/python/tests/test_integration.py b/datajunction-clients/python/tests/test_integration.py new file mode 100644 index 000000000..f3a7ec07e --- /dev/null +++ b/datajunction-clients/python/tests/test_integration.py @@ -0,0 +1,125 @@ +""" +Integration tests to be run against the latest full demo datajunction environment +""" +# pylint: disable=protected-access + +import logging +from time import sleep + +import pytest + +from datajunction import DJBuilder, models +from datajunction.compile import ColumnYAML +from datajunction.exceptions import DJClientException + +_logger = logging.getLogger(__name__) + + +@pytest.mark.skipif("not config.getoption('integration')") +def test_integration(): + """ + Integration test + """ + dj = DJBuilder() + + max_retries = 5 + initial_delay = 1 # seconds + delay_factor = 5 + + for attempt in range(max_retries): + try: + dj.basic_login(username="dj", password="dj") + break + except DJClientException: + if attempt == max_retries - 1: + raise + delay = initial_delay * (delay_factor**attempt) + _logger.info( + "Login attempt %s failed. Retrying in %d seconds...", + attempt + 1, + delay, + ) + sleep(delay) + + dj._session.post("/catalogs", json={"name": "tpch"}) + dj._session.post("/engines", json={"name": "trino", "version": "451"}) + dj._session.post( + "/catalogs/tpch/engines", + json=[{"name": "trino", "version": "451"}], + ) + dj.create_namespace("integration.tests") + dj.create_namespace("integration.tests.trino") + source = dj.create_source( + name="integration.tests.source1", + catalog="unknown", + schema="db", + table="tbl", + display_name="Test Source with Columns", + description="A test source node with columns", + columns=[ + ColumnYAML(name="id", type="int"), + ColumnYAML(name="name", type="string"), + ColumnYAML(name="price", type="double"), + ColumnYAML(name="created_at", type="timestamp"), + ], + primary_key=["id"], + mode=models.NodeMode.PUBLISHED, + update_if_exists=True, + ) + assert source.name == "integration.tests.source1" + dj.register_table(catalog="tpch", schema="sf1", table="orders") + transform = dj.create_transform( + name="integration.tests.trino.transform1", + display_name="Filter to last 1000 records", + description="The last 1000 purchases", + mode=models.NodeMode.PUBLISHED, + query=( + "select custkey, totalprice, orderdate from " + "source.tpch.sf1.orders order by orderdate desc limit 1000" + ), + update_if_exists=True, + ) + assert transform.name == "integration.tests.trino.transform1" + dimension = dj.create_dimension( + name="integration.tests.trino.dimension1", + display_name="Customer keys", + description="All custkey values in the source table", + mode=models.NodeMode.PUBLISHED, + primary_key=[ + "id", + ], + tags=[], + query="select custkey as id, 'attribute' as foo from source.tpch.sf1.orders", + update_if_exists=True, + ) + assert dimension.name == "integration.tests.trino.dimension1" + dj._link_dimension_to_node( + node_name="integration.tests.trino.transform1", + column_name="custkey", + dimension_name="integration.tests.trino.dimension1", + dimension_column=None, + ) + metric = dj.create_metric( + name="integration.tests.trino.metric1", + display_name="Total of last 1000 purchases", + description="This is the total amount from the last 1000 purchases", + mode=models.NodeMode.PUBLISHED, + query="select sum(totalprice) from integration.tests.trino.transform1", + update_if_exists=True, + ) + assert metric.name == "integration.tests.trino.metric1" + common_dimensions = dj.common_dimensions( + metrics=["integration.tests.trino.metric1"], + ) + assert len(common_dimensions) == 2 + assert set( + [ + "integration.tests.trino.dimension1.id", + "integration.tests.trino.dimension1.foo", + ], + ) == {attribute["name"] for attribute in common_dimensions} + sql = dj.sql( + metrics=["integration.tests.trino.metric1"], + dimensions=["integration.tests.trino.dimension1.id"], + ) + assert "SELECT" in sql diff --git a/datajunction-clients/python/tests/test_models.py b/datajunction-clients/python/tests/test_models.py new file mode 100644 index 000000000..f77c5663e --- /dev/null +++ b/datajunction-clients/python/tests/test_models.py @@ -0,0 +1,17 @@ +"""Tests for models.""" +from datajunction.models import QueryState + + +def test_enum_list(): + """ + Check list of query states works + """ + assert QueryState.list() == [ + "UNKNOWN", + "ACCEPTED", + "SCHEDULED", + "RUNNING", + "FINISHED", + "CANCELED", + "FAILED", + ] diff --git a/datajunction-clients/python/tox.ini b/datajunction-clients/python/tox.ini new file mode 100644 index 000000000..f72ab293e --- /dev/null +++ b/datajunction-clients/python/tox.ini @@ -0,0 +1,91 @@ +# Tox configuration file + +[tox] +minversion = 3.24 +envlist = default +isolated_build = True + + +[testenv] +description = Invoke pytest to run automated tests +setenv = + TOXINIDIR = {toxinidir} +passenv = + HOME + SETUPTOOLS_* +extras = + testing +commands = + pytest {posargs} + + +# # To run `tox -e lint` you need to make sure you have a +# # `.pre-commit-config.yaml` file. See https://pre-commit.com +# [testenv:lint] +# description = Perform static analysis and style checks +# skip_install = True +# deps = pre-commit +# passenv = +# HOMEPATH +# PROGRAMDATA +# SETUPTOOLS_* +# commands = +# pre-commit run --all-files {posargs:--show-diff-on-failure} + + +[testenv:{build,clean}] +description = + build: Build the package in isolation according to PEP517, see https://github.com/pypa/build + clean: Remove old distribution files and temporary build artifacts (./build and ./dist) +# https://setuptools.pypa.io/en/stable/build_meta.html#how-to-use-it +skip_install = True +changedir = {toxinidir} +deps = + build: build[virtualenv] +passenv = + SETUPTOOLS_* +commands = + clean: python -c 'import shutil; [shutil.rmtree(p, True) for p in ("build", "dist", "docs/_build")]' + clean: python -c 'import pathlib, shutil; [shutil.rmtree(p, True) for p in pathlib.Path("src").glob("*.egg-info")]' + build: python -m build {posargs} +# By default, both `sdist` and `wheel` are built. If your sdist is too big or you don't want +# to make it available, consider running: `tox -e build -- --wheel` + + +[testenv:{docs,doctests,linkcheck}] +description = + docs: Invoke sphinx-build to build the docs + doctests: Invoke sphinx-build to run doctests + linkcheck: Check for broken links in the documentation +passenv = + SETUPTOOLS_* +setenv = + DOCSDIR = {toxinidir}/docs + BUILDDIR = {toxinidir}/docs/_build + docs: BUILD = html + doctests: BUILD = doctest + linkcheck: BUILD = linkcheck +deps = + -r {toxinidir}/docs/requirements.txt + # ^ requirements.txt shared with Read The Docs +commands = + sphinx-build --color -b {env:BUILD} -d "{env:BUILDDIR}/doctrees" "{env:DOCSDIR}" "{env:BUILDDIR}/{env:BUILD}" {posargs} + + +[testenv:publish] +description = + Publish the package you have been developing to a package index server. + By default, it uses testpypi. If you really want to publish your package + to be publicly accessible in PyPI, use the `-- --repository pypi` option. +skip_install = True +changedir = {toxinidir} +passenv = + # See: https://twine.readthedocs.io/en/latest/ + TWINE_USERNAME + TWINE_PASSWORD + TWINE_REPOSITORY + TWINE_REPOSITORY_URL +deps = twine +commands = + python -m twine check dist/* + python -m twine upload {posargs:--repository {env:TWINE_REPOSITORY:testpypi}} dist/* diff --git a/datajunction-query/.coveragerc b/datajunction-query/.coveragerc new file mode 100644 index 000000000..84a1b76ce --- /dev/null +++ b/datajunction-query/.coveragerc @@ -0,0 +1,33 @@ +# .coveragerc to control coverage.py +[run] +branch = True +source = dj +omit = + djqs/config.py + djqs/exceptions.py + +[paths] +source = + djqs/ + */site-packages/ + +[report] +sort = -Cover +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + if TYPE_CHECKING: diff --git a/datajunction-query/.env b/datajunction-query/.env new file mode 100644 index 000000000..e0645f1cf --- /dev/null +++ b/datajunction-query/.env @@ -0,0 +1,4 @@ +CONFIGURATION_FILE="/code/config.djqs.yml" +DEFAULT_CATALOG=warehouse +DEFAULT_ENGINE=trino +DEFAULT_ENGINE_VERSION="451" diff --git a/datajunction-query/.flake8 b/datajunction-query/.flake8 new file mode 100644 index 000000000..d9ad0b409 --- /dev/null +++ b/datajunction-query/.flake8 @@ -0,0 +1,5 @@ +[flake8] +ignore = E203, E266, E501, W503, F403, F401 +max-line-length = 79 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 diff --git a/datajunction-query/.gitignore b/datajunction-query/.gitignore new file mode 100644 index 000000000..e48e6169a --- /dev/null +++ b/datajunction-query/.gitignore @@ -0,0 +1,117 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/api/* +docs/_rst/* +docs/_build/* + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +*.sqlite +*.db +*.swp + +# https://pypi.org/project/python-dotenv/ +.env + +# VS Code +.vscode + +# IDEA +.idea + +# Spark +spark-warehouse +metastore_db diff --git a/datajunction-query/.isort.cfg b/datajunction-query/.isort.cfg new file mode 100644 index 000000000..93c4c7eae --- /dev/null +++ b/datajunction-query/.isort.cfg @@ -0,0 +1,3 @@ +[settings] +profile = black +known_first_party = djqs diff --git a/datajunction-query/.pre-commit-config.yaml b/datajunction-query/.pre-commit-config.yaml new file mode 100644 index 000000000..ea27c1255 --- /dev/null +++ b/datajunction-query/.pre-commit-config.yaml @@ -0,0 +1,99 @@ +files: ^datajunction-query/ +exclude: (^datajunction-query/docker/) + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + - id: check-ast + exclude: ^templates/ + - id: check-json + - id: check-merge-conflict + - id: check-xml + - id: check-yaml + - id: debug-statements + exclude: ^templates/ + - id: end-of-file-fixer + exclude: openapi.json + - id: requirements-txt-fixer + exclude: ^templates/ + - id: mixed-line-ending + args: ['--fix=auto'] # replace 'auto' with 'lf' to enforce Linux/Mac line endings or 'crlf' for Windows + +## If you want to avoid flake8 errors due to unused vars or imports: +# - repo: https://github.com/myint/autoflake.git +# rev: v1.4 +# hooks: +# - id: autoflake +# args: [ +# --in-place, +# --remove-all-unused-imports, +# --remove-unused-variables, +# ] + +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + +- repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black + language_version: python3 + exclude: ^templates/ + +## If like to embrace black styles even in the docs: +# - repo: https://github.com/asottile/blacken-docs +# rev: v1.9.1 +# hooks: +# - id: blacken-docs +# additional_dependencies: [black] + +- repo: https://github.com/PyCQA/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + exclude: ^templates/ + ## You can add flake8 plugins via `additional_dependencies`: + # additional_dependencies: [flake8-bugbear] + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v0.981' # Use the sha / tag you want to point at + hooks: + - id: mypy + exclude: ^templates/ + additional_dependencies: + - types-requests + - types-freezegun + - types-python-dateutil + - types-setuptools + - types-PyYAML + - types-tabulate + - types-toml +- repo: https://github.com/asottile/add-trailing-comma + rev: v2.2.1 + hooks: + - id: add-trailing-comma +#- repo: https://github.com/asottile/reorder_python_imports +# rev: v2.5.0 +# hooks: +# - id: reorder-python-imports +# args: [--application-directories=.:src] +## Removing this for now due to this bug: https://github.com/hadialqattan/pycln/issues/249 +# - repo: https://github.com/hadialqattan/pycln +# rev: v2.4.0 # Possible releases: https://github.com/hadialqattan/pycln/tags +# hooks: +# - id: pycln +# args: [--config=pyproject.toml] +# exclude: ^templates/ +- repo: local + hooks: + - id: pylint + name: pylint + entry: pylint --disable=duplicate-code,use-implicit-booleaness-not-comparison + language: system + types: [python] + exclude: ^templates/ diff --git a/datajunction-query/.pylintrc b/datajunction-query/.pylintrc new file mode 100644 index 000000000..ee77aead7 --- /dev/null +++ b/datajunction-query/.pylintrc @@ -0,0 +1,10 @@ +[MESSAGES CONTROL] +disable=c-extension-no-member + +[MASTER] +# https://github.com/samuelcolvin/pydantic/issues/1961#issuecomment-759522422 +extension-pkg-whitelist=pydantic,duckdb +ignore=templates,docs + +[IMPORTS] +ignore-modules=duckdb diff --git a/.readthedocs.yml b/datajunction-query/.readthedocs.yml similarity index 100% rename from .readthedocs.yml rename to datajunction-query/.readthedocs.yml diff --git a/datajunction-query/AUTHORS.rst b/datajunction-query/AUTHORS.rst new file mode 100644 index 000000000..30d306349 --- /dev/null +++ b/datajunction-query/AUTHORS.rst @@ -0,0 +1,9 @@ +============ +Contributors +============ + +* Beto Dealmeida +* Olek Gorajek +* Hamidreza Hashemi +* Ali Raza +* Sam Redai diff --git a/datajunction-query/CODE_OF_CONDUCT.md b/datajunction-query/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..18c914718 --- /dev/null +++ b/datajunction-query/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/datajunction-query/Dockerfile b/datajunction-query/Dockerfile new file mode 100644 index 000000000..e5beb0424 --- /dev/null +++ b/datajunction-query/Dockerfile @@ -0,0 +1,11 @@ +FROM jupyter/pyspark-notebook +USER root +WORKDIR /code +COPY . /code +RUN apt-get update && apt-get install -y \ + libpq-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* +RUN pip install -e .[uvicorn] +CMD ["uvicorn", "djqs.api.main:app", "--host", "0.0.0.0", "--port", "8001", "--reload"] +EXPOSE 8001 diff --git a/datajunction-query/LICENSE.txt b/datajunction-query/LICENSE.txt new file mode 100644 index 000000000..c4b4b0a2e --- /dev/null +++ b/datajunction-query/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Beto Dealmeida + +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. diff --git a/datajunction-query/Makefile b/datajunction-query/Makefile new file mode 100644 index 000000000..1badc73d4 --- /dev/null +++ b/datajunction-query/Makefile @@ -0,0 +1,50 @@ +pyenv: .python-version + +.python-version: setup.cfg + if [ -z "`pyenv virtualenvs | grep djqs`" ]; then\ + pyenv virtualenv djqs;\ + fi + if [ ! -f .python-version ]; then\ + pyenv local djqs;\ + fi + pdm install + touch .python-version + +docker-build: + docker build . + docker compose build + +docker-run: + docker compose up + +docker-run-with-postgres: + docker compose -f docker-compose.yml -f docker-compose.postgres.yml up + +docker-run-with-druid: + docker compose -f docker-compose.yml -f docker-compose.druid.yml up + +docker-run-with-cockroachdb: + docker compose -f docker-compose.yml -f docker-compose.cockroachdb.yml up + +test: pyenv + pdm run pytest --cov=djqs -vv tests/ --cov-report term-missing --doctest-modules djqs --without-integration --without-slow-integration ${PYTEST_ARGS} + +integration: pyenv + pdm run pytest --cov=djqs -vv tests/ --cov-report term-missing --doctest-modules djqs --with-integration --with-slow-integration + +clean: + pyenv virtualenv-delete djqs + +spellcheck: + codespell -L froms -S "*.json" djqs docs/*rst tests templates + +check: + pdm run pre-commit run --all-files + +lint: + make check + +dev-release: + hatch version dev + hatch build + hatch publish diff --git a/datajunction-query/README.rst b/datajunction-query/README.rst new file mode 100644 index 000000000..65b0dbf2a --- /dev/null +++ b/datajunction-query/README.rst @@ -0,0 +1,334 @@ +========================== +DataJunction Query Service +========================== + +This repository (DJQS) is an open source implementation of a `DataJunction `_ +query service. It allows you to create catalogs and engines that represent sqlalchemy connections. Configuring +a DJ server to use a DJQS server allows DJ to query any of the database technologies supported by sqlalchemy. + +========== +Quickstart +========== + +To get started, clone this repo and start up the docker compose environment. + +.. code-block:: + + git clone https://github.com/DataJunction/djqs + cd djqs + docker compose up + +Creating Catalogs +================= + +Catalogs can be created using the :code:`POST /catalogs/` endpoint. + +.. code-block:: sh + + curl -X 'POST' \ + 'http://localhost:8001/catalogs/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "djdb" + }' + +Creating Engines +================ + +Engines can be created using the :code:`POST /engines/` endpoint. + +.. code-block:: sh + + curl -X 'POST' \ + 'http://localhost:8001/engines/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "sqlalchemy-postgresql", + "version": "15.2", + "uri": "postgresql://dj:dj@postgres-roads:5432/djdb" + }' + +Engines can be attached to existing catalogs using the :code:`POST /catalogs/{name}/engines/` endpoint. + +.. code-block:: sh + + curl -X 'POST' \ + 'http://localhost:8001/catalogs/djdb/engines/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '[ + { + "name": "sqlalchemy-postgresql", + "version": "15.2" + } + ]' + +Executing Queries +================= + +Queries can be submitted to DJQS for a specified catalog and engine. + +.. code-block:: sh + + curl -X 'POST' \ + 'http://localhost:8001/queries/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "catalog_name": "djdb", + "engine_name": "sqlalchemy-postgresql", + "engine_version": "15.2", + "submitted_query": "SELECT * from roads.repair_orders", + "async_": false + }' + +Async queries can be submitted as well. + +.. code-block:: sh + + curl -X 'POST' \ + 'http://localhost:8001/queries/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "catalog_name": "djdb", + "engine_name": "sqlalchemy-postgresql", + "engine_version": "15.2", + "submitted_query": "SELECT * from roads.repair_orders", + "async_": true + }' + +*response* + +.. code-block:: json + + { + "catalog_name": "djdb", + "engine_name": "sqlalchemy-postgresql", + "engine_version": "15.2", + "id": "", + "submitted_query": "SELECT * from roads.repair_orders", + "executed_query": null, + "scheduled": null, + "started": null, + "finished": null, + "state": "ACCEPTED", + "progress": 0, + "results": [], + "next": null, + "previous": null, + "errors": [] + } + +The query id provided in the response can then be used to check the status of the running query and get the results +once it's completed. + +.. code-block:: sh + + curl -X 'GET' \ + 'http://localhost:8001/queries//' \ + -H 'accept: application/json' + +*response* + +.. code-block:: json + + { + "catalog_name": "djdb", + "engine_name": "sqlalchemy-postgresql", + "engine_version": "15.2", + "id": "$QUERY_ID", + "submitted_query": "SELECT * from roads.repair_orders", + "executed_query": "SELECT * from roads.repair_orders", + "scheduled": "2023-02-28T07:27:55.367162", + "started": "2023-02-28T07:27:55.367387", + "finished": "2023-02-28T07:27:55.502412", + "state": "FINISHED", + "progress": 1, + "results": [ + { + "sql": "SELECT * from roads.repair_orders", + "columns": [...], + "rows": [...], + "row_count": 25 + } + ], + "next": null, + "previous": null, + "errors": [] + } + +Reflection +========== + +If running a [reflection service](https://github.com/DataJunction/djrs), that service can leverage the +:code:`POST /table/{table}/columns/` endpoint of DJQS to get column names and types for a given table. + +.. code-block:: sh + + curl -X 'GET' \ + 'http://localhost:8001/table/djdb.roads.repair_orders/columns/?engine=sqlalchemy-postgresql&engine_version=15.2' \ + -H 'accept: application/json' + +*response* + +.. code-block:: json + + { + "name": "djdb.roads.repair_orders", + "columns": [ + { + "name": "repair_order_id", + "type": "INT" + }, + { + "name": "municipality_id", + "type": "STR" + }, + { + "name": "hard_hat_id", + "type": "INT" + }, + { + "name": "order_date", + "type": "DATE" + }, + { + "name": "required_date", + "type": "DATE" + }, + { + "name": "dispatched_date", + "type": "DATE" + }, + { + "name": "dispatcher_id", + "type": "INT" + } + ] + } + +====== +DuckDB +====== + +DJQS includes an example of using DuckDB as an engine and it comes preloaded with the roads example database. + +Create a :code:`djduckdb` catalog and a :code:`duckdb` engine. + +.. code-block:: + + curl -X 'POST' \ + 'http://localhost:8001/catalogs/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "djduckdb" + }' + +.. code-block:: + + curl -X 'POST' \ + 'http://localhost:8001/engines/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "duckdb", + "version": "0.7.1", + "uri": "duckdb://local[*]" + }' + +.. code-block:: + + curl -X 'POST' \ + 'http://localhost:8001/catalogs/djduckdb/engines/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '[ + { + "name": "duckdb", + "version": "0.7.1" + } + ]' + +Now you can submit DuckDB SQL queries. + +.. code-block:: + + curl -X 'POST' \ + 'http://localhost:8001/queries/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "catalog_name": "djduckdb", + "engine_name": "duckdb", + "engine_version": "0.7.1", + "submitted_query": "SELECT * FROM roads.us_states LIMIT 10", + "async_": false + }' + +===== +Spark +===== + +DJQS includes an example of using Spark as an engine. To try it, start up the docker compose environment and then +load the example roads database into Spark. + +.. code-block:: + + docker exec -it djqs /bin/bash -c "python /code/docker/spark_load_roads.py" + +Next, create a :code:`djspark` catalog and a :code:`spark` engine. + +.. code-block:: + + curl -X 'POST' \ + 'http://localhost:8001/catalogs/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "djspark" + }' + +.. code-block:: + + curl -X 'POST' \ + 'http://localhost:8001/engines/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "spark", + "version": "3.3.2", + "uri": "spark://local[*]" + }' + +.. code-block:: + + curl -X 'POST' \ + 'http://localhost:8001/catalogs/djspark/engines/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '[ + { + "name": "spark", + "version": "3.3.2" + } + ]' + +Now you can submit Spark SQL queries. + +.. code-block:: + + curl -X 'POST' \ + 'http://localhost:8001/queries/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "catalog_name": "djspark", + "engine_name": "spark", + "engine_version": "3.3.2", + "submitted_query": "SELECT * FROM roads.us_states LIMIT 10", + "async_": false + }' diff --git a/datajunction-query/alembic.ini b/datajunction-query/alembic.ini new file mode 100644 index 000000000..5716e4937 --- /dev/null +++ b/datajunction-query/alembic.ini @@ -0,0 +1,104 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s +file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = +timezone = UTC + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/datajunction-query/alembic/README b/datajunction-query/alembic/README new file mode 100644 index 000000000..2500aa1bc --- /dev/null +++ b/datajunction-query/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. diff --git a/datajunction-query/alembic/env.py b/datajunction-query/alembic/env.py new file mode 100644 index 000000000..8ada95e1e --- /dev/null +++ b/datajunction-query/alembic/env.py @@ -0,0 +1,83 @@ +""" +Environment for Alembic migrations. +""" +# pylint: disable=no-member, unused-import, no-name-in-module, import-error +from logging.config import fileConfig + +from sqlalchemy import create_engine + +from alembic import context + +DEFAULT_URI = "postgresql+psycopg://dj:dj@postgres_metadata:5432/dj" + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = None # pylint: disable=invalid-name + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + x_args = context.get_x_argument(as_dictionary=True) + context.configure( + url=x_args.get("uri") or DEFAULT_URI, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + version_table="alembic_version_djqs", + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + x_args = context.get_x_argument(as_dictionary=True) + connectable = create_engine(x_args.get("uri") or DEFAULT_URI) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + version_table="alembic_version_djqs", + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/datajunction-query/alembic/script.py.mako b/datajunction-query/alembic/script.py.mako new file mode 100644 index 000000000..1c2bf5315 --- /dev/null +++ b/datajunction-query/alembic/script.py.mako @@ -0,0 +1,27 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +import sqlmodel +from alembic import op +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/datajunction-query/alembic/versions/2024_09_09_0000-b8f22b3549c7_initial_migration.py b/datajunction-query/alembic/versions/2024_09_09_0000-b8f22b3549c7_initial_migration.py new file mode 100644 index 000000000..3df270c15 --- /dev/null +++ b/datajunction-query/alembic/versions/2024_09_09_0000-b8f22b3549c7_initial_migration.py @@ -0,0 +1,41 @@ +"""Initial migration + +Revision ID: b8f22b3549c7 +Revises: +Create Date: 2024-09-09 06:00:00.000000+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "b8f22b3549c7" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute( + """ + CREATE TABLE query ( + id UUID PRIMARY KEY, + catalog_name VARCHAR NOT NULL, + engine_name VARCHAR NOT NULL, + engine_version VARCHAR NOT NULL, + submitted_query VARCHAR NOT NULL, + async_ BOOLEAN NOT NULL, + executed_query VARCHAR, + scheduled TIMESTAMP, + started TIMESTAMP, + finished TIMESTAMP, + state VARCHAR NOT NULL, + progress FLOAT NOT NULL + ) + """, + ) + + +def downgrade(): + op.execute("DROP TABLE query") diff --git a/datajunction-query/config.djqs.yml b/datajunction-query/config.djqs.yml new file mode 100644 index 000000000..dc271aa2e --- /dev/null +++ b/datajunction-query/config.djqs.yml @@ -0,0 +1,21 @@ +engines: + - name: duckdb + version: 0.7.1 + type: duckdb + uri: duckdb:////code/docker/default.duckdb + extra_params: + location: /code/docker/default.duckdb + - name: trino + version: 451 + type: sqlalchemy + uri: trino://trino-coordinator:8080/tpch/sf1 + extra_params: + http_scheme: http + user: admin +catalogs: + - name: warehouse + engines: + - duckdb + - name: tpch + engines: + - trino diff --git a/datajunction-query/config.jsonschema b/datajunction-query/config.jsonschema new file mode 100644 index 000000000..7b7637f39 --- /dev/null +++ b/datajunction-query/config.jsonschema @@ -0,0 +1,83 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$ref": "#/definitions/DJQSConfig", + "definitions": { + "DJQSConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "engines": { + "type": "array", + "items": { + "$ref": "#/definitions/Engine" + } + }, + "catalogs": { + "type": "array", + "items": { + "$ref": "#/definitions/Catalog" + } + } + }, + "required": [ + "catalogs", + "engines" + ], + "title": "DJQSConfig" + }, + "Catalog": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "engines": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "engines", + "name" + ], + "title": "Catalog" + }, + "Engine": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "extra_params": { + "$ref": "#/definitions/ExtraParams" + } + }, + "required": [ + "extra_params", + "name", + "type", + "uri", + "version" + ], + "title": "Engine" + }, + "ExtraParams": { + "type": "object", + "additionalProperties": true, + "title": "ExtraParams" + } + } +} diff --git a/datajunction-query/djqs/__about__.py b/datajunction-query/djqs/__about__.py new file mode 100644 index 000000000..2788edf4b --- /dev/null +++ b/datajunction-query/djqs/__about__.py @@ -0,0 +1,4 @@ +""" +Version for Hatch +""" +__version__ = "0.0.1a87" diff --git a/datajunction-query/djqs/__init__.py b/datajunction-query/djqs/__init__.py new file mode 100644 index 000000000..2cff295fb --- /dev/null +++ b/datajunction-query/djqs/__init__.py @@ -0,0 +1,14 @@ +""" +Package version and name. +""" + +from importlib.metadata import PackageNotFoundError, version # pragma: no cover + +try: + # Change here if project is renamed and does not equal the package name + DIST_NAME = __name__ + __version__ = version(DIST_NAME) +except PackageNotFoundError: # pragma: no cover + __version__ = "unknown" +finally: + del version, PackageNotFoundError diff --git a/tests/cli/__init__.py b/datajunction-query/djqs/api/__init__.py similarity index 100% rename from tests/cli/__init__.py rename to datajunction-query/djqs/api/__init__.py diff --git a/datajunction-query/djqs/api/helpers.py b/datajunction-query/djqs/api/helpers.py new file mode 100644 index 000000000..cee3a8109 --- /dev/null +++ b/datajunction-query/djqs/api/helpers.py @@ -0,0 +1,45 @@ +""" +Helper functions for API +""" +from typing import Any, Dict, List, Optional + +from sqlalchemy import create_engine, inspect +from sqlalchemy.exc import NoSuchTableError, OperationalError + +from djqs.exceptions import DJException, DJTableNotFound + + +def get_columns( + table: str, + schema: Optional[str], + catalog: Optional[str], + uri: Optional[str], + extra_params: Optional[Dict[str, Any]], +) -> List[Dict[str, str]]: # pragma: no cover + """ + Return all columns in a given table. + """ + if not uri: + raise DJException("Cannot retrieve columns without a uri") + + engine = create_engine(uri, connect_args=extra_params) + try: + inspector = inspect(engine) + column_metadata = inspector.get_columns( + table, + schema=schema, + ) + except NoSuchTableError as exc: # pylint: disable=broad-except + raise DJTableNotFound( + message=f"No such table `{table}` in schema `{schema}` in catalog `{catalog}`", + http_status_code=404, + ) from exc + except OperationalError as exc: + if "unknown database" in str(exc): + raise DJException(message=f"No such schema `{schema}`") from exc + raise + + return [ + {"name": column["name"], "type": column["type"].python_type.__name__.upper()} + for column in column_metadata + ] diff --git a/datajunction-query/djqs/api/main.py b/datajunction-query/djqs/api/main.py new file mode 100644 index 000000000..0055831bb --- /dev/null +++ b/datajunction-query/djqs/api/main.py @@ -0,0 +1,80 @@ +""" +Main DJ query server app. +""" + +# All the models need to be imported here so that SQLModel can define their +# relationships at runtime without causing circular imports. +# See https://sqlmodel.tiangolo.com/tutorial/code-structure/#make-circular-imports-work. +# pylint: disable=unused-import,expression-not-assigned + +import logging +from contextlib import asynccontextmanager + +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from psycopg.rows import dict_row +from psycopg_pool import AsyncConnectionPool + +from djqs import __version__ +from djqs.api import queries, tables +from djqs.exceptions import DJException +from djqs.utils import get_settings + +_logger = logging.getLogger(__name__) + +settings = get_settings() + + +@asynccontextmanager +async def lifespan(fastapi_app: FastAPI): + """ + Create a postgres connection pool and store it in the app state + """ + _logger.info("Starting PostgreSQL connection pool...") + pool = AsyncConnectionPool( + settings.index, + kwargs={"row_factory": dict_row}, + check=AsyncConnectionPool.check_connection, + min_size=5, + max_size=20, + timeout=15, + ) + fastapi_app.state.pool = pool + try: + _logger.info("PostgreSQL connection pool started with DSN: %s", settings.index) + yield + finally: + _logger.info("Closing PostgreSQL connection pool") + await pool.close() + _logger.info("PostgreSQL connection pool closed") + + +app = FastAPI( + title=settings.name, + description=settings.description, + version=__version__, + license_info={ + "name": "MIT License", + "url": "https://mit-license.org/", + }, + lifespan=lifespan, +) +app.include_router(queries.router) +app.include_router(tables.router) + +app.router.lifespan_context = lifespan + + +@app.exception_handler(DJException) +async def dj_exception_handler( # pylint: disable=unused-argument + request: Request, + exc: DJException, +) -> JSONResponse: + """ + Capture errors and return JSON. + """ + return JSONResponse( # pragma: no cover + status_code=exc.http_status_code, + content=exc.to_dict(), + headers={"X-DJ-Error": "true", "X-DBAPI-Exception": exc.dbapi_exception}, + ) diff --git a/datajunction-query/djqs/api/queries.py b/datajunction-query/djqs/api/queries.py new file mode 100644 index 000000000..4d7b7c9ce --- /dev/null +++ b/datajunction-query/djqs/api/queries.py @@ -0,0 +1,259 @@ +""" +Query related APIs. +""" + +import json +import logging +import uuid +from dataclasses import asdict +from http import HTTPStatus +from typing import Any, Dict, List, Optional + +import msgpack +from accept_types import get_best_match +from fastapi import ( + APIRouter, + BackgroundTasks, + Body, + Depends, + Header, + HTTPException, + Request, + Response, +) +from psycopg_pool import AsyncConnectionPool + +from djqs.config import Settings +from djqs.db.postgres import DBQuery, get_postgres_pool +from djqs.engine import process_query +from djqs.models.query import ( + Query, + QueryCreate, + QueryResults, + QueryState, + StatementResults, + decode_results, + encode_results, +) +from djqs.utils import get_settings + +_logger = logging.getLogger(__name__) +router = APIRouter(tags=["SQL Queries"]) + + +@router.post( + "/queries/", + response_model=QueryResults, + status_code=HTTPStatus.OK, + responses={ + 200: { + "content": {"application/msgpack": {}}, + "description": "Return results as JSON or msgpack", + }, + }, +) +async def submit_query( # pylint: disable=too-many-arguments + accept: Optional[str] = Header(None), + *, + settings: Settings = Depends(get_settings), + request: Request, + response: Response, + postgres_pool: AsyncConnectionPool = Depends(get_postgres_pool), + background_tasks: BackgroundTasks, + body: Any = Body( + ..., + example={ + "catalog_name": "warehouse", + "engine_name": "trino", + "engine_version": "451", + "submitted_query": "select * from tpch.sf1.customer limit 10", + }, + ), +) -> QueryResults: + """ + Run or schedule a query. + + This endpoint is different from others in that it accepts both JSON and msgpack, and + can also return JSON or msgpack, depending on HTTP headers. + """ + content_type = request.headers.get("content-type") + if content_type == "application/json": + data = body + elif content_type == "application/msgpack": + data = json.loads(msgpack.unpackb(body, ext_hook=decode_results)) + elif content_type is None: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="Content type must be specified", + ) + else: + raise HTTPException( + status_code=HTTPStatus.UNPROCESSABLE_ENTITY, + detail=f"Content type not accepted: {content_type}", + ) + + # Set default catalog and engine if not explicitly specified in submitted query + data["engine_name"] = data.get("engine_name") or settings.default_engine + data["engine_version"] = ( + data.get("engine_version") or settings.default_engine_version + ) + data["catalog_name"] = data.get("catalog_name") or settings.default_catalog + + create_query = QueryCreate(**data) + + query_with_results = await save_query_and_run( + create_query=create_query, + settings=settings, + response=response, + background_tasks=background_tasks, + postgres_pool=postgres_pool, + headers=request.headers, + ) + + return_type = get_best_match(accept, ["application/json", "application/msgpack"]) + if not return_type: + raise HTTPException( + status_code=HTTPStatus.NOT_ACCEPTABLE, + detail="Client MUST accept: application/json, application/msgpack", + ) + + if return_type == "application/msgpack": + content = msgpack.packb( + asdict(query_with_results), + default=encode_results, + ) + else: + content = json.dumps(asdict(query_with_results), default=str) + + return Response( + content=content, + media_type=return_type, + status_code=response.status_code or HTTPStatus.OK, + ) + + +async def save_query_and_run( # pylint: disable=R0913 + create_query: QueryCreate, + settings: Settings, + response: Response, + background_tasks: BackgroundTasks, + postgres_pool: AsyncConnectionPool, + headers: Optional[Dict[str, str]] = None, +) -> QueryResults: + """ + Store a new query to the DB and run it. + """ + query = Query( + catalog_name=create_query.catalog_name, # type: ignore + engine_name=create_query.engine_name, # type: ignore + engine_version=create_query.engine_version, # type: ignore + submitted_query=create_query.submitted_query, + async_=create_query.async_, + ) + query.state = QueryState.ACCEPTED + + async with postgres_pool.connection() as conn: + results = ( + await DBQuery() + .save_query( + query_id=query.id, + catalog_name=query.catalog_name, + engine_name=query.engine_name, + engine_version=query.engine_version, + submitted_query=query.submitted_query, + async_=query.async_, + state=query.state.value, + ) + .execute(conn=conn) + ) + query_save_result = results[0] + if not query_save_result: # pragma: no cover + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Query failed to save", + ) + + if query.async_: + background_tasks.add_task( + process_query, + settings, + postgres_pool, + query, + headers, + ) + + response.status_code = HTTPStatus.CREATED + return QueryResults( + id=query.id, + catalog_name=query.catalog_name, + engine_name=query.engine_name, + engine_version=query.engine_version, + submitted_query=query.submitted_query, + executed_query=query.executed_query, + state=QueryState.SCHEDULED, + results=[], + errors=[], + ) + + query_results = await process_query( + settings=settings, + postgres_pool=postgres_pool, + query=query, + headers=headers, + ) + return query_results + + +def load_query_results( + settings: Settings, + key: str, +) -> List[StatementResults]: + """ + Load results from backend, if available. + + If ``paginate`` is true we also load the results into the cache, anticipating more + paginated queries. + """ + if settings.results_backend.has(key): + _logger.info("Reading results from results backend") + cached = settings.results_backend.get(key) + query_results = json.loads(cached) + else: # pragma: no cover + _logger.warning("No results found") + query_results = [] + + return query_results + + +@router.get("/queries/{query_id}/", response_model=QueryResults) +async def read_query( + query_id: uuid.UUID, + *, + settings: Settings = Depends(get_settings), + postgres_pool: AsyncConnectionPool = Depends(get_postgres_pool), +) -> QueryResults: + """ + Fetch information about a query. + + For paginated queries we move the data from the results backend to the cache for a + short period, anticipating additional requests. + """ + async with postgres_pool.connection() as conn: + dbquery_results = ( + await DBQuery().get_query(query_id=query_id).execute(conn=conn) + ) + queries = dbquery_results[0] + if not queries: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Query not found", + ) + query = queries[0] + + query_results = load_query_results(settings, str(query_id)) + + prev = next_ = None + + return QueryResults( + results=query_results, next=next_, previous=prev, errors=[], **query + ) diff --git a/datajunction-query/djqs/api/tables.py b/datajunction-query/djqs/api/tables.py new file mode 100644 index 000000000..5f66f2efa --- /dev/null +++ b/datajunction-query/djqs/api/tables.py @@ -0,0 +1,54 @@ +""" +Table related APIs. +""" +from typing import Optional + +from fastapi import APIRouter, Path, Query + +from djqs.api.helpers import get_columns +from djqs.exceptions import DJInvalidTableRef +from djqs.models.table import TableInfo +from djqs.utils import get_settings + +router = APIRouter(tags=["Table Reflection"]) + + +@router.get("/table/{table}/columns/", response_model=TableInfo) +def table_columns( + table: str = Path(..., example="tpch.sf1.customer"), + engine: Optional[str] = Query(None, example="trino"), + engine_version: Optional[str] = Query(None, example="451"), +) -> TableInfo: + """ + Get column information for a table + """ + table_parts = table.split(".") + if len(table_parts) != 3: + raise DJInvalidTableRef( + http_status_code=422, + message=f"The provided table value `{table}` is invalid. A valid value " + f"for `table` must be in the format `..
`", + ) + settings = get_settings() + + if engine and engine_version: + engine_config = settings.find_engine( + engine_name=engine, + engine_version=engine_version or settings.default_engine_version, + ) + else: + engine_config = settings.find_engine( + engine_name=settings.default_engine, + engine_version=engine_version or settings.default_engine_version, + ) + external_columns = get_columns( + uri=engine_config.uri, + extra_params=engine_config.extra_params, + catalog=table_parts[0], + schema=table_parts[1], + table=table_parts[2], + ) + return TableInfo( + name=table, + columns=external_columns, + ) diff --git a/datajunction-query/djqs/config.py b/datajunction-query/djqs/config.py new file mode 100644 index 000000000..0506653e9 --- /dev/null +++ b/datajunction-query/djqs/config.py @@ -0,0 +1,199 @@ +""" +Configuration for the query service +""" + +import json +import logging +import os +from datetime import timedelta +from enum import Enum +from typing import Dict, List, Optional + +import toml +import yaml +from cachelib.base import BaseCache +from cachelib.file import FileSystemCache + +from djqs.exceptions import DJUnknownCatalog, DJUnknownEngine + +_logger = logging.getLogger(__name__) + + +class EngineType(Enum): + """ + Supported engine types + """ + + DUCKDB = "duckdb" + SQLALCHEMY = "sqlalchemy" + SNOWFLAKE = "snowflake" + TRINO = "trino" + + +class EngineInfo: # pylint: disable=too-few-public-methods + """ + Information about a query engine + """ + + def __init__( # pylint: disable=too-many-arguments + self, + name: str, + version: str, + type: str, # pylint: disable=redefined-builtin + uri: str, + extra_params: Optional[Dict[str, str]] = None, + ): + self.name = name + self.version = str(version) + self.type = EngineType(type) + self.uri = uri + self.extra_params = extra_params or {} + + +class CatalogInfo: # pylint: disable=too-few-public-methods + """ + Information about a catalog + """ + + def __init__(self, name: str, engines: List[str]): + self.name = name + self.engines = engines + + +class Settings: # pylint: disable=too-many-instance-attributes + """ + Configuration for the query service + """ + + def __init__( # pylint: disable=too-many-arguments,too-many-locals,dangerous-default-value + self, + name: Optional[str] = "DJQS", + description: Optional[str] = "A DataJunction Query Service", + url: Optional[str] = "http://localhost:8001/", + index: Optional[str] = "postgresql://dj:dj@postgres_metadata:5432/dj", + default_catalog: Optional[str] = "", + default_engine: Optional[str] = "", + default_engine_version: Optional[str] = "", + results_backend: Optional[BaseCache] = None, + results_backend_path: Optional[str] = "/tmp/djqs", + results_backend_timeout: Optional[str] = "0", + paginating_timeout_minutes: Optional[str] = "5", + do_ping_timeout_seconds: Optional[str] = "5", + configuration_file: Optional[str] = None, + engines: Optional[List[EngineInfo]] = None, + catalogs: Optional[List[CatalogInfo]] = None, + ): + self.name: str = os.getenv("NAME", name or "") + self.description: str = os.getenv("DESCRIPTION", description or "") + self.url: str = os.getenv("URL", url or "") + + # SQLAlchemy URI for the metadata database. + self.index: str = os.getenv("INDEX", index or "") + + # The default catalog to use if not specified in query payload + self.default_catalog: str = os.getenv("DEFAULT_CATALOG", default_catalog or "") + + # The default engine to use if not specified in query payload + self.default_engine: str = os.getenv("DEFAULT_ENGINE", default_engine or "") + + # The default engine version to use if not specified in query payload + self.default_engine_version: str = os.getenv( + "DEFAULT_ENGINE_VERSION", + default_engine_version or "", + ) + + # Where to store the results from queries. + self.results_backend: BaseCache = results_backend or FileSystemCache( + os.getenv("RESULTS_BACKEND_PATH", results_backend_path or ""), + default_timeout=int( + os.getenv("RESULTS_BACKEND_TIMEOUT", results_backend_timeout or "0"), + ), + ) + + self.paginating_timeout: timedelta = timedelta( + minutes=int( + os.getenv( + "PAGINATING_TIMEOUT_MINUTES", + paginating_timeout_minutes or "5", + ), + ), + ) + + # How long to wait when pinging databases to find out the fastest online database. + self.do_ping_timeout: timedelta = timedelta( + seconds=int( + os.getenv("DO_PING_TIMEOUT_SECONDS", do_ping_timeout_seconds or "5"), + ), + ) + + # Configuration file for catalogs and engines + self.configuration_file: Optional[str] = ( + os.getenv("CONFIGURATION_FILE") or configuration_file + ) + + self.engines: List[EngineInfo] = engines or [] + self.catalogs: List[CatalogInfo] = catalogs or [] + + self._load_configuration() + + def _load_configuration(self): + config_file = self.configuration_file + + if config_file: + if config_file.endswith(".yaml") or config_file.endswith(".yml"): + with open(config_file, "r", encoding="utf-8") as file: + config = yaml.safe_load(file) + elif config_file.endswith(".toml"): + with open(config_file, "r", encoding="utf-8") as file: + config = toml.load(file) + elif config_file.endswith(".json"): + with open(config_file, "r", encoding="utf-8") as file: + config = json.load(file) + else: + raise ValueError( + f"Unsupported configuration file format: {config_file}", + ) + + self.engines = [ + EngineInfo(**engine) for engine in config.get("engines", []) + ] + self.catalogs = [ + CatalogInfo(**catalog) for catalog in config.get("catalogs", []) + ] + else: + _logger.warning("No settings configuration file has been set") + + def find_engine( + self, + engine_name: str, + engine_version: str, + ) -> EngineInfo: + """ + Find an engine defined in the server configuration + """ + found_engine = None + for engine in self.engines: + if engine.name == engine_name and engine.version == engine_version: + found_engine = engine + if not found_engine: + raise DJUnknownEngine( + ( + f"Configuration error, cannot find engine {engine_name} " + f"with version {engine_version}" + ), + ) + return found_engine + + def find_catalog(self, catalog_name: str) -> CatalogInfo: + """ + Find a catalog defined in the server configuration + """ + found_catalog = None + for catalog in self.catalogs: + if catalog.name == catalog_name: + found_catalog = catalog + if not found_catalog: + raise DJUnknownCatalog( + f"Configuration error, cannot find catalog {catalog_name}", + ) + return found_catalog diff --git a/datajunction-query/djqs/constants.py b/datajunction-query/djqs/constants.py new file mode 100644 index 000000000..3feceffd4 --- /dev/null +++ b/datajunction-query/djqs/constants.py @@ -0,0 +1,20 @@ +""" +Useful constants. +""" + +from datetime import timedelta +from uuid import UUID + +DJ_DATABASE_ID = 0 +DJ_DATABASE_UUID = UUID("594804bf-47cb-426c-83c4-94a348e95972") +SQLITE_DATABASE_ID = -1 +SQLITE_DATABASE_UUID = UUID("3619eeba-d628-4ab1-9dd5-65738ab3c02f") + +DEFAULT_DIMENSION_COLUMN = "id" + +# used by the SQLAlchemy client +QUERY_EXECUTE_TIMEOUT = timedelta(seconds=60) +GET_COLUMNS_TIMEOUT = timedelta(seconds=60) + +# Request header configuration params +SQLALCHEMY_URI = "SQLALCHEMY_URI" diff --git a/datajunction-query/djqs/db/postgres.py b/datajunction-query/djqs/db/postgres.py new file mode 100644 index 000000000..90c30067d --- /dev/null +++ b/datajunction-query/djqs/db/postgres.py @@ -0,0 +1,138 @@ +""" +Dependency for getting the postgres pool and running backend DB queries +""" +# pylint: disable=too-many-arguments +from datetime import datetime +from typing import List +from uuid import UUID + +from fastapi import Request +from psycopg import sql +from psycopg_pool import AsyncConnectionPool + +from djqs.exceptions import DJDatabaseError + + +async def get_postgres_pool(request: Request) -> AsyncConnectionPool: + """ + Get the postgres pool from the app instance + """ + app = request.app + return app.state.pool + + +class DBQuery: + """ + Metadata DB queries using the psycopg composition utility + """ + + def __init__(self): + self._reset() + + def _reset(self): + self.selects: List = [] + self.inserts: List = [] + + def get_query(self, query_id: UUID): + """ + Get metadata about a query + """ + self.selects.append( + sql.SQL( + """ + SELECT id, catalog_name, engine_name, engine_version, submitted_query, + async_, executed_query, scheduled, started, finished, state, progress + FROM query + WHERE id = {query_id} + """, + ).format(query_id=sql.Literal(query_id)), + ) + return self + + def save_query( + self, + query_id: UUID, + catalog_name: str = "", + engine_name: str = "", + engine_version: str = "", + submitted_query: str = "", + async_: bool = False, + state: str = "", + progress: float = 0.0, + executed_query: str = None, + scheduled: datetime = None, + started: datetime = None, + finished: datetime = None, + ): + """ + Save metadata about a query + """ + self.inserts.append( + sql.SQL( + """ + INSERT INTO query (id, catalog_name, engine_name, engine_version, + submitted_query, async_, executed_query, scheduled, + started, finished, state, progress) + VALUES ({query_id}, {catalog_name}, {engine_name}, {engine_version}, + {submitted_query}, {async_}, {executed_query}, {scheduled}, + {started}, {finished}, {state}, {progress}) + ON CONFLICT (id) DO UPDATE SET + catalog_name = EXCLUDED.catalog_name, + engine_name = EXCLUDED.engine_name, + engine_version = EXCLUDED.engine_version, + submitted_query = EXCLUDED.submitted_query, + async_ = EXCLUDED.async_, + executed_query = EXCLUDED.executed_query, + scheduled = EXCLUDED.scheduled, + started = EXCLUDED.started, + finished = EXCLUDED.finished, + state = EXCLUDED.state, + progress = EXCLUDED.progress + RETURNING * + """, + ).format( + query_id=sql.Literal(query_id), + catalog_name=sql.Literal(catalog_name), + engine_name=sql.Literal(engine_name), + engine_version=sql.Literal(engine_version), + submitted_query=sql.Literal(submitted_query), + async_=sql.Literal(async_), + executed_query=sql.Literal(executed_query), + scheduled=sql.Literal(scheduled), + started=sql.Literal(started), + finished=sql.Literal(finished), + state=sql.Literal(state), + progress=sql.Literal(progress), + ), + ) + return self + + async def execute(self, conn): + """ + Submit all statements to the backend DB, multiple statements are submitted together + """ + if not self.selects and not self.inserts: # pragma: no cover + return + + async with conn.cursor() as cur: + results = [] + if len(self.inserts) > 1: # pragma: no cover + async with conn.transaction(): + for statement in self.inserts: + await cur.execute(statement) + results.append(await cur.fetchall()) + + if len(self.inserts) == 1: + await cur.execute(self.inserts[0]) + if cur.rowcount == 0: # pragma: no cover + raise DJDatabaseError( + "Insert statement resulted in no records being inserted", + ) + results.append((await cur.fetchone())) + if self.selects: + for statement in self.selects: + await cur.execute(statement) + results.append(await cur.fetchall()) + await conn.commit() + self._reset() + return results diff --git a/datajunction-query/djqs/engine.py b/datajunction-query/djqs/engine.py new file mode 100644 index 000000000..8ad3253f8 --- /dev/null +++ b/datajunction-query/djqs/engine.py @@ -0,0 +1,240 @@ +""" +Query related functions. +""" +import json +import logging +import os +from dataclasses import asdict +from datetime import datetime, timezone +from typing import Dict, List, Optional, Tuple + +import duckdb +import snowflake.connector +from psycopg_pool import AsyncConnectionPool +from sqlalchemy import create_engine, text + +from djqs.config import EngineType, Settings +from djqs.constants import SQLALCHEMY_URI +from djqs.db.postgres import DBQuery +from djqs.exceptions import DJDatabaseError +from djqs.models.query import ( + ColumnMetadata, + Query, + QueryResults, + QueryState, + StatementResults, +) +from djqs.typing import ColumnType, Description, SQLADialect, Stream, TypeEnum +from djqs.utils import get_settings + +_logger = logging.getLogger(__name__) + + +def get_columns_from_description( + description: Description, + dialect: SQLADialect, +) -> List[ColumnMetadata]: + """ + Extract column metadata from the cursor description. + + For now this uses the information from the cursor description, which only allow us to + distinguish between 4 types (see ``TypeEnum``). In the future we should use a type + inferrer to determine the types based on the query. + """ + type_map = { + TypeEnum.STRING: ColumnType.STR, + TypeEnum.BINARY: ColumnType.BYTES, + TypeEnum.NUMBER: ColumnType.FLOAT, + TypeEnum.DATETIME: ColumnType.DATETIME, + } + + columns = [] + for column in description or []: + name, native_type = column[:2] + for dbapi_type in TypeEnum: + if native_type == getattr( + dialect.dbapi, + dbapi_type.value, + None, + ): # pragma: no cover + type_ = type_map[dbapi_type] + break + else: + # fallback to string + type_ = ColumnType.STR # pragma: no cover + + columns.append(ColumnMetadata(name=name, type=type_)) + + return columns + + +def run_query( # pylint: disable=R0914 + query: Query, + headers: Optional[Dict[str, str]] = None, +) -> List[Tuple[str, List[ColumnMetadata], Stream]]: + """ + Run a query and return its results. + + For each statement we return a tuple with the statement SQL, a description of the + columns (name and type) and a stream of rows (tuples). + """ + + _logger.info("Running query on catalog %s", query.catalog_name) + + settings = get_settings() + engine_name = query.engine_name or settings.default_engine + engine_version = query.engine_version or settings.default_engine_version + engine = settings.find_engine( + engine_name=engine_name, + engine_version=engine_version, + ) + query_server = headers.get(SQLALCHEMY_URI) if headers else None + + if query_server: + _logger.info( + "Creating sqlalchemy engine using request header param %s", + SQLALCHEMY_URI, + ) + sqla_engine = create_engine(query_server) + elif engine.type == EngineType.DUCKDB: + _logger.info("Creating duckdb connection") + conn = ( + duckdb.connect() + if engine.uri == "duckdb:///:memory:" + else duckdb.connect( + database=engine.extra_params["location"], + read_only=True, + ) + ) + return run_duckdb_query(query, conn) + elif engine.type == EngineType.SNOWFLAKE: + _logger.info("Creating snowflake connection") + conn = snowflake.connector.connect( + **engine.extra_params, + password=os.getenv("SNOWSQL_PWD"), + ) + cur = conn.cursor() + + return run_snowflake_query(query, cur) + + _logger.info( + "Creating sqlalchemy engine using engine name and version defined on query", + ) + sqla_engine = create_engine(engine.uri, connect_args=engine.extra_params) + connection = sqla_engine.connect() + + output: List[Tuple[str, List[ColumnMetadata], Stream]] = [] + results = connection.execute(text(query.executed_query)) + stream = (tuple(row) for row in results) + columns = get_columns_from_description( + results.cursor.description, + sqla_engine.dialect, + ) + output.append((query.executed_query, columns, stream)) # type: ignore + + return output + + +def run_duckdb_query( + query: Query, + conn: duckdb.DuckDBPyConnection, +) -> List[Tuple[str, List[ColumnMetadata], Stream]]: + """ + Run a duckdb query against the local duckdb database + """ + output: List[Tuple[str, List[ColumnMetadata], Stream]] = [] + rows = conn.execute(query.submitted_query).fetchall() + columns: List[ColumnMetadata] = [] + output.append((query.submitted_query, columns, rows)) + return output + + +def run_snowflake_query( + query: Query, + cur: snowflake.connector.cursor.SnowflakeCursor, +) -> List[Tuple[str, List[ColumnMetadata], Stream]]: + """ + Run a query against a snowflake warehouse + """ + output: List[Tuple[str, List[ColumnMetadata], Stream]] = [] + rows = cur.execute(query.submitted_query).fetchall() + columns: List[ColumnMetadata] = [] + output.append((query.submitted_query, columns, rows)) + return output + + +async def process_query( + settings: Settings, + postgres_pool: AsyncConnectionPool, + query: Query, + headers: Optional[Dict[str, str]] = None, +) -> QueryResults: + """ + Process a query. + """ + query.scheduled = datetime.now(timezone.utc) + query.state = QueryState.SCHEDULED + query.executed_query = query.submitted_query + + errors = [] + query.started = datetime.now(timezone.utc) + try: + results = [] + for sql, columns, stream in run_query( + query=query, + headers=headers, + ): + rows = list(stream) + results.append( + StatementResults( + sql=sql, + columns=columns, + rows=rows, + row_count=len(rows), + ), + ) + + query.state = QueryState.FINISHED + query.progress = 1.0 + except Exception as ex: # pylint: disable=broad-except + results = [] + query.state = QueryState.FAILED + errors = [str(ex)] + + query.finished = datetime.now(timezone.utc) + + async with postgres_pool.connection() as conn: + dbquery_results = ( + await DBQuery() + .save_query( + query_id=query.id, + submitted_query=query.submitted_query, + state=QueryState.FINISHED.value, + async_=query.async_, + ) + .execute(conn=conn) + ) + query_save_result = dbquery_results[0] + if not query_save_result: # pragma: no cover + raise DJDatabaseError("Query failed to save") + + settings.results_backend.add( + str(query.id), + json.dumps([asdict(statement_result) for statement_result in results]), + ) + + return QueryResults( + id=query.id, + catalog_name=query.catalog_name, + engine_name=query.engine_name, + engine_version=query.engine_version, + submitted_query=query.submitted_query, + executed_query=query.executed_query, + scheduled=query.scheduled, + started=query.started, + finished=query.finished, + state=query.state, + progress=query.progress, + results=results, + errors=errors, + ) diff --git a/datajunction-query/djqs/enum.py b/datajunction-query/djqs/enum.py new file mode 100644 index 000000000..24761c751 --- /dev/null +++ b/datajunction-query/djqs/enum.py @@ -0,0 +1,21 @@ +""" +Backwards-compatible StrEnum for both Python >= and < 3.11 +""" +import enum +import sys + +if sys.version_info >= (3, 11): + from enum import ( # noqa # pylint: disable=unused-import # pragma: no cover + IntEnum, + StrEnum, + ) +else: + + class StrEnum(str, enum.Enum): # pragma: no cover + """Backwards compatible StrEnum for Python < 3.11""" # pragma: no cover + + def __str__(self): + return str(self.value) + + class IntEnum(int, enum.Enum): # pragma: no cover + """Backwards compatible IntEnum for Python < 3.11""" # pragma: no cover diff --git a/datajunction-query/djqs/exceptions.py b/datajunction-query/djqs/exceptions.py new file mode 100644 index 000000000..f7592140f --- /dev/null +++ b/datajunction-query/djqs/exceptions.py @@ -0,0 +1,261 @@ +""" +Errors and warnings. +""" + +from typing import Any, Dict, List, Literal, Optional, TypedDict + +from djqs.enum import IntEnum + + +class ErrorCode(IntEnum): + """ + Error codes. + """ + + # generic errors + UNKWNON_ERROR = 0 + NOT_IMPLEMENTED_ERROR = 1 + ALREADY_EXISTS = 2 + + # metric API + INVALID_FILTER_PATTERN = 100 + INVALID_COLUMN_IN_FILTER = 101 + INVALID_VALUE_IN_FILTER = 102 + + # SQL API + INVALID_ARGUMENTS_TO_FUNCTION = 200 + + +class DebugType(TypedDict, total=False): + """ + Type for debug information. + """ + + # link to where an issue can be filed + issue: str + + # link to documentation about the problem + documentation: str + + # any additional context + context: Dict[str, Any] + + +class DJErrorType(TypedDict): + """ + Type for serialized errors. + """ + + code: int + message: str + debug: Optional[DebugType] + + +class DJError: + """ + An error. + """ + + def __init__( + self, + code: ErrorCode, + message: str, + debug: Optional[Dict[str, Any]] = None, + ): + self.code = code + self.message = message + self.debug = debug + + def __str__(self) -> str: + """ + Format the error nicely. + """ + return f"{self.message} (error code: {self.code})" + + def dict(self) -> Dict[str, Any]: + """ + Convert the error to a dictionary. + """ + return { + "code": self.code, + "message": self.message, + "debug": self.debug, + } + + +class DJWarningType(TypedDict): + """ + Type for serialized warnings. + """ + + code: Optional[int] + message: str + debug: Optional[DebugType] + + +class DJWarning: # pylint: disable=too-few-public-methods + """ + A warning. + """ + + def __init__( + self, + message: str, + code: Optional[ErrorCode] = None, + debug: Optional[Dict[str, Any]] = None, + ): + self.code = code + self.message = message + self.debug = debug + + def dict(self) -> Dict[str, Any]: + """ + Convert the warning to a dictionary. + """ + return { + "code": self.code, + "message": self.message, + "debug": self.debug, + } + + +DBAPIExceptions = Literal[ + "Warning", + "Error", + "InterfaceError", + "DatabaseError", + "DataError", + "OperationalError", + "IntegrityError", + "InternalError", + "ProgrammingError", + "NotSupportedError", +] + + +class DJExceptionType(TypedDict): + """ + Type for serialized exceptions. + """ + + message: Optional[str] + errors: List[DJErrorType] + warnings: List[DJWarningType] + + +class DJException(Exception): + """ + Base class for errors. + """ + + def __init__( # pylint: disable=too-many-arguments + self, + message: Optional[str] = None, + errors: Optional[List[DJError]] = None, + warnings: Optional[List[DJWarning]] = None, + dbapi_exception: Optional[DBAPIExceptions] = None, + http_status_code: Optional[int] = None, + ): + self.errors = errors or [] + self.warnings = warnings or [] + self.message = message or "\n".join(error.message for error in self.errors) + self.dbapi_exception = dbapi_exception or "Error" + self.http_status_code = http_status_code or 500 + + super().__init__(self.message) + + def to_dict(self) -> dict: + """ + Convert to dict. + """ + return { # pragma: no cover + "message": self.message, + "errors": [error.dict() for error in self.errors], + "warnings": [warning.dict() for warning in self.warnings], + } + + def __str__(self) -> str: + """ + Format the exception nicely. + """ + if not self.errors: + return self.message + + plural = "s" if len(self.errors) > 1 else "" + combined_errors = "\n".join(f"- {error}" for error in self.errors) + errors = f"The following error{plural} happened:\n{combined_errors}" + + return f"{self.message}\n{errors}" + + def __eq__(self, other) -> bool: + return ( # pragma: no cover + isinstance(other, DJException) + and self.message == other.message + and self.errors == other.errors + and self.warnings == other.warnings + and self.dbapi_exception == other.dbapi_exception + and self.http_status_code == other.http_status_code + ) + + +class DJInvalidInputException(DJException): + """ + Exception raised when the input provided by the user is invalid. + """ + + def __init__(self, *args, **kwargs): + super().__init__( + *args, dbapi_exception="ProgrammingError", http_status_code=422, **kwargs + ) + + +class DJNotImplementedException(DJException): + """ + Exception raised when some functionality hasn't been implemented in DJ yet. + """ + + def __init__(self, *args, **kwargs): + super().__init__( + *args, dbapi_exception="NotSupportedError", http_status_code=500, **kwargs + ) + + +class DJInternalErrorException(DJException): + """ + Exception raised when we do something wrong in the code. + """ + + def __init__(self, *args, **kwargs): + super().__init__( + *args, dbapi_exception="InternalError", http_status_code=500, **kwargs + ) + + +class DJInvalidTableRef(DJException): + """ + Raised for invalid table values + """ + + +class DJTableNotFound(DJException): + """ + Raised for tables that cannot be found + """ + + +class DJDatabaseError(DJException): + """ + Ran into an issue while submitting a query to the backend DB + """ + + +class DJUnknownCatalog(DJException): + """ + Raised when a catalog cannot be found + """ + + +class DJUnknownEngine(DJException): + """ + Raised when an engine or engine version cannot be found + """ diff --git a/datajunction-query/djqs/fixes.py b/datajunction-query/djqs/fixes.py new file mode 100644 index 000000000..2d348e839 --- /dev/null +++ b/datajunction-query/djqs/fixes.py @@ -0,0 +1,3 @@ +""" +Database-specific fixes. +""" diff --git a/datajunction-query/djqs/models/__init__.py b/datajunction-query/djqs/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-query/djqs/models/query.py b/datajunction-query/djqs/models/query.py new file mode 100644 index 000000000..61e788d66 --- /dev/null +++ b/datajunction-query/djqs/models/query.py @@ -0,0 +1,137 @@ +""" +Models for queries. +""" + +import uuid +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any, List, Optional +from uuid import UUID, uuid4 + +import msgpack + +from djqs.enum import IntEnum +from djqs.typing import QueryState, Row + + +@dataclass +class BaseQuery: + """ + Base class for query models. + """ + + catalog_name: Optional[str] = None + engine_name: Optional[str] = None + engine_version: Optional[str] = None + + +@dataclass +class Query(BaseQuery): # pylint: disable=too-many-instance-attributes + """ + A query. + """ + + id: UUID = field(default_factory=uuid4) # pylint: disable=invalid-name + submitted_query: str = "" + catalog_name: str = "" + engine_name: str = "" + engine_version: str = "" + async_: bool = False + executed_query: Optional[str] = None + scheduled: Optional[datetime] = None + started: Optional[datetime] = None + finished: Optional[datetime] = None + state: QueryState = QueryState.UNKNOWN + progress: float = 0.0 + + +@dataclass +class QueryCreate(BaseQuery): + """ + Model for submitted queries. + """ + + submitted_query: str = "" + async_: bool = False + + +@dataclass +class ColumnMetadata: + """ + A simple model for column metadata. + """ + + name: str + type: str + + +@dataclass +class StatementResults: + """ + Results for a given statement. + + This contains the SQL, column names and types, and rows + """ + + sql: str + columns: List[ColumnMetadata] = field(default_factory=list) + rows: List[Row] = field(default_factory=list) + row_count: int = 0 # used for pagination + + +@dataclass +class QueryResults(BaseQuery): # pylint: disable=too-many-instance-attributes + """ + Model for query with results. + """ + + id: uuid.UUID = field(default_factory=uuid4) # pylint: disable=invalid-name + engine_name: Optional[str] = None + engine_version: Optional[str] = None + submitted_query: str = "" + executed_query: Optional[str] = None + scheduled: Optional[datetime] = None + started: Optional[datetime] = None + finished: Optional[datetime] = None + state: QueryState = QueryState.UNKNOWN + async_: bool = False + progress: float = 0.0 + results: List[StatementResults] = field(default_factory=list) + next: Optional[str] = None # Changed to str, as AnyHttpUrl was from pydantic + previous: Optional[str] = None # Changed to str, as AnyHttpUrl was from pydantic + errors: List[str] = field(default_factory=list) + + +class QueryExtType(IntEnum): + """ + Custom ext type for msgpack. + """ + + UUID = 1 + DATETIME = 2 + + +def encode_results(obj: Any) -> Any: + """ + Custom msgpack encoder for ``QueryWithResults``. + """ + if isinstance(obj, uuid.UUID): + return msgpack.ExtType(QueryExtType.UUID, str(obj).encode("utf-8")) + + if isinstance(obj, datetime): + return msgpack.ExtType(QueryExtType.DATETIME, obj.isoformat().encode("utf-8")) + + return obj # pragma: no cover + + +def decode_results(code: int, data: bytes) -> Any: + """ + Custom msgpack decoder for ``QueryWithResults``. + """ + if code == QueryExtType.UUID: + return uuid.UUID(data.decode()) + + if code == QueryExtType.DATETIME: + return datetime.fromisoformat(data.decode()) + + return msgpack.ExtType(code, data) # pragma: no cover diff --git a/datajunction-query/djqs/models/table.py b/datajunction-query/djqs/models/table.py new file mode 100644 index 000000000..c7ecc500f --- /dev/null +++ b/datajunction-query/djqs/models/table.py @@ -0,0 +1,15 @@ +""" +Models for use in table API requests and responses +""" +from typing import Dict, List + +from pydantic import BaseModel + + +class TableInfo(BaseModel): + """ + Table information + """ + + name: str + columns: List[Dict[str, str]] diff --git a/datajunction-query/djqs/typing.py b/datajunction-query/djqs/typing.py new file mode 100644 index 000000000..e8f95e101 --- /dev/null +++ b/datajunction-query/djqs/typing.py @@ -0,0 +1,326 @@ +""" +Custom types for annotations. +""" + +# pylint: disable=missing-class-docstring + +from __future__ import annotations + +from types import ModuleType +from typing import Any, Iterator, List, Literal, Optional, Tuple, TypedDict, Union + +from typing_extensions import Protocol + +from djqs.enum import StrEnum + + +class SQLADialect(Protocol): # pylint: disable=too-few-public-methods + """ + A SQLAlchemy dialect. + """ + + dbapi: ModuleType + + +# The ``type_code`` in a cursor description -- can really be anything +TypeCode = Any + + +# Cursor description +Description = Optional[ + List[ + Tuple[ + str, + TypeCode, + Optional[str], + Optional[str], + Optional[str], + Optional[str], + Optional[bool], + ] + ] +] + + +# A stream of data +Row = Tuple[Any, ...] +Stream = Iterator[Row] + + +class ColumnType(StrEnum): + """ + Types for columns. + + These represent the values from the ``python_type`` attribute in SQLAlchemy columns. + """ + + BYTES = "BYTES" + STR = "STR" + FLOAT = "FLOAT" + INT = "INT" + DECIMAL = "DECIMAL" + BOOL = "BOOL" + DATETIME = "DATETIME" + DATE = "DATE" + TIME = "TIME" + TIMEDELTA = "TIMEDELTA" + LIST = "LIST" + DICT = "DICT" + + +class TypeEnum(StrEnum): + """ + PEP 249 basic types. + + Unfortunately SQLAlchemy doesn't seem to offer an API for determining the types of the + columns in a (SQL Core) query, and the DB API 2.0 cursor only offers very coarse + types. + """ + + STRING = "STRING" + BINARY = "BINARY" + NUMBER = "NUMBER" + DATETIME = "DATETIME" + UNKNOWN = "UNKNOWN" + + +class QueryState(StrEnum): + """ + Different states of a query. + """ + + UNKNOWN = "UNKNOWN" + ACCEPTED = "ACCEPTED" + SCHEDULED = "SCHEDULED" + RUNNING = "RUNNING" + FINISHED = "FINISHED" + CANCELED = "CANCELED" + FAILED = "FAILED" + + +# sqloxide type hints +# Reference: https://github.com/sqlparser-rs/sqlparser-rs/blob/main/src/ast/query.rs + + +class Value(TypedDict, total=False): + Number: Tuple[str, bool] + SingleQuotedString: str + Boolean: bool + + +class Limit(TypedDict): + Value: Value + + +class Identifier(TypedDict): + quote_style: Optional[str] + value: str + + +class Bound(TypedDict, total=False): + Following: int + Preceding: int + + +class WindowFrame(TypedDict): + end_bound: Bound + start_bound: Bound + units: str + + +class Expression(TypedDict, total=False): + CompoundIdentifier: List["Identifier"] + Identifier: Identifier + Value: Value + Function: Function # type: ignore + UnaryOp: UnaryOp # type: ignore + BinaryOp: BinaryOp # type: ignore + Case: Case # type: ignore + + +class Case(TypedDict): + conditions: List[Expression] + else_result: Optional[Expression] + operand: Optional[Expression] + results: List[Expression] + + +class UnnamedArgument(TypedDict): + Expr: Expression + + +class Argument(TypedDict, total=False): + Unnamed: Union[UnnamedArgument, Wildcard] + + +class Over(TypedDict): + order_by: List[Expression] + partition_by: List[Expression] + window_frame: WindowFrame + + +class Function(TypedDict): + args: List[Argument] + distinct: bool + name: List[Identifier] + over: Optional[Over] + + +class ExpressionWithAlias(TypedDict): + alias: Identifier + expr: Expression + + +class Offset(TypedDict): + rows: str + value: Expression + + +class OrderBy(TypedDict, total=False): + asc: Optional[bool] + expr: Expression + nulls_first: Optional[bool] + + +class Projection(TypedDict, total=False): + ExprWithAlias: ExpressionWithAlias + UnnamedExpr: Expression + + +Wildcard = Literal["Wildcard"] + + +class Fetch(TypedDict): + percent: bool + quantity: Value + with_ties: bool + + +Top = Fetch + + +class UnaryOp(TypedDict): + op: str + expr: Expression + + +class BinaryOp(TypedDict): + left: Expression + op: str + right: Expression + + +class LateralView(TypedDict): + lateral_col_alias: List[Identifier] + lateral_view: Expression + lateral_view_name: List[Identifier] + outer: bool + + +class TableAlias(TypedDict): + columns: List[Identifier] + name: Identifier + + +class Table(TypedDict): + alias: Optional[TableAlias] + args: List[Argument] + name: List[Identifier] + with_hints: List[Expression] + + +class Derived(TypedDict): + lateral: bool + subquery: "Body" # type: ignore + alias: Optional[TableAlias] + + +class Relation(TypedDict, total=False): + Table: Table + Derived: Derived + + +class JoinConstraint(TypedDict): + On: Expression + Using: List[Identifier] + + +class JoinOperator(TypedDict, total=False): + Inner: JoinConstraint + LeftOuter: JoinConstraint + RightOuter: JoinConstraint + FullOuter: JoinConstraint + + +CrossJoin = Literal["CrossJoin"] +CrossApply = Literal["CrossApply"] +OuterApply = Literal["Outerapply"] + + +class Join(TypedDict): + join_operator: Union[JoinOperator, CrossJoin, CrossApply, OuterApply] + relation: Relation + + +class From(TypedDict): + joins: List[Join] + relation: Relation + + +Select = TypedDict( + "Select", + { + "cluster_by": List[Expression], + "distinct": bool, + "distribute_by": List[Expression], + "from": List[From], + "group_by": List[Expression], + "having": Optional[BinaryOp], + "lateral_views": List[LateralView], + "projection": List[Union[Projection, Wildcard]], + "selection": Optional[BinaryOp], + "sort_by": List[Expression], + "top": Optional[Top], + }, +) + + +class Body(TypedDict): + Select: Select + + +CTETable = TypedDict( + "CTETable", + { + "alias": TableAlias, + "from": Optional[Identifier], + "query": "Query", # type: ignore + }, +) + + +class With(TypedDict): + cte_tables: List[CTETable] + + +Query = TypedDict( + "Query", + { + "body": Body, + "fetch": Optional[Fetch], + "limit": Optional[Limit], + "lock": Optional[Literal["Share", "Update"]], + "offset": Optional[Offset], + "order_by": List[OrderBy], + "with": Optional[With], + }, +) + + +# We could support more than just ``SELECT`` here. +class Statement(TypedDict): + Query: Query + + +# A parse tree, result of ``sqloxide.parse_sql``. +ParseTree = List[Statement] # type: ignore diff --git a/datajunction-query/djqs/utils.py b/datajunction-query/djqs/utils.py new file mode 100644 index 000000000..3de2be627 --- /dev/null +++ b/datajunction-query/djqs/utils.py @@ -0,0 +1,41 @@ +""" +Utility functions. +""" +# pylint: disable=line-too-long + +import logging +import os +from functools import lru_cache + +from dotenv import load_dotenv +from rich.logging import RichHandler + +from djqs.config import Settings + + +def setup_logging(loglevel: str) -> None: + """ + Setup basic logging. + """ + level = getattr(logging, loglevel.upper(), None) + if not isinstance(level, int): + raise ValueError(f"Invalid log level: {loglevel}") + + logformat = "[%(asctime)s] %(levelname)s: %(name)s: %(message)s" + logging.basicConfig( + level=level, + format=logformat, + datefmt="[%X]", + handlers=[RichHandler(rich_tracebacks=True)], + force=True, + ) + + +@lru_cache(1) +def get_settings() -> Settings: + """ + Return a cached settings object. + """ + dotenv_file = os.environ.get("DOTENV_FILE", ".env") + load_dotenv(dotenv_file) + return Settings() diff --git a/datajunction-query/docker/cockroachdb/cockroachdb_examples_init.sql b/datajunction-query/docker/cockroachdb/cockroachdb_examples_init.sql new file mode 100644 index 000000000..6887b966f --- /dev/null +++ b/datajunction-query/docker/cockroachdb/cockroachdb_examples_init.sql @@ -0,0 +1,35 @@ +CREATE DATABASE steam; +CREATE TABLE steam.playtime(user_id VARCHAR, game VARCHAR, behavior VARCHAR, amount FLOAT, zero INT); + +IMPORT INTO steam.playtime (user_id, game, behavior, amount, zero) +CSV DATA ( + 'nodelocal://self/steam-hours-played.csv' +); + +CREATE TABLE steam.games( + url VARCHAR, + types VARCHAR, + name VARCHAR, + desc_snippet VARCHAR, + recent_reviews VARCHAR, + all_reviews VARCHAR, + release_date VARCHAR, + developer VARCHAR, + publisher VARCHAR, + popular_tags VARCHAR, + game_details VARCHAR, + languages VARCHAR, + achievements VARCHAR, + genre VARCHAR, + game_description VARCHAR, + mature_content VARCHAR, + minimum_requirements VARCHAR, + recommended_requirements VARCHAR, + original_price VARCHAR, + discount_price VARCHAR +); + +IMPORT INTO steam.games (url,types,name,desc_snippet,recent_reviews,all_reviews,release_date,developer,publisher,popular_tags,game_details,languages,achievements,genre,game_description,mature_content,minimum_requirements,recommended_requirements,original_price,discount_price) +CSV DATA ( + 'nodelocal://self/steam-games.csv' +); diff --git a/datajunction-query/docker/cockroachdb/cockroachdb_metadata_init.sql b/datajunction-query/docker/cockroachdb/cockroachdb_metadata_init.sql new file mode 100644 index 000000000..3e196982b --- /dev/null +++ b/datajunction-query/docker/cockroachdb/cockroachdb_metadata_init.sql @@ -0,0 +1 @@ +CREATE DATABASE djqs; diff --git a/datajunction-query/docker/cockroachdb/steam-games.csv b/datajunction-query/docker/cockroachdb/steam-games.csv new file mode 100644 index 000000000..da70b3ce4 --- /dev/null +++ b/datajunction-query/docker/cockroachdb/steam-games.csv @@ -0,0 +1,19 @@ +https://store.steampowered.com/app/379720/DOOM/,app,DOOM,"Now includes all three premium DLC packs (Unto the Evil, Hell Followed, and Bloodfall), maps, modes, and weapons, as well as all feature updates including Arcade Mode, Photo Mode, and the latest Update 6.66, which brings further multiplayer improvements as well as revamps multiplayer progression.","Very Positive,(554),- 89% of the 554 user reviews in the last 30 days are positive.","Very Positive,(42,550),- 92% of the 42,550 user reviews for this game are positive.","May 12, 2016",id Software,"Bethesda Softworks,Bethesda Softworks","FPS,Gore,Action,Demons,Shooter,First-Person,Great Soundtrack,Multiplayer,Singleplayer,Fast-Paced,Sci-fi,Horror,Classic,Atmospheric,Difficult,Blood,Remake,Zombies,Co-op,Memes","Single-player,Multi-player,Co-op,Steam Achievements,Steam Trading Cards,Partial Controller Support,Steam Cloud","English,French,Italian,German,Spanish - Spain,Japanese,Polish,Portuguese - Brazil,Russian,Traditional Chinese",54,Action," About This Game Developed by id software, the studio that pioneered the first-person shooter genre and created multiplayer Deathmatch, DOOM returns as a brutally fun and challenging modern-day shooter experience. Relentless demons, impossibly destructive guns, and fast, fluid movement provide the foundation for intense, first-person combat – whether you’re obliterating demon hordes through the depths of Hell in the single-player campaign, or competing against your friends in numerous multiplayer modes. Expand your gameplay experience using DOOM SnapMap game editor to easily create, play, and share your content with the world. STORY: You’ve come here for a reason. The Union Aerospace Corporation’s massive research facility on Mars is overwhelmed by fierce and powerful demons, and only one person stands between their world and ours. As the lone DOOM Marine, you’ve been activated to do one thing – kill them all. KEY FEATURES: A Relentless Campaign There is no taking cover or stopping to regenerate health as you beat back Hell’s raging demon hordes. Combine your arsenal of futuristic and iconic guns, upgrades, movement and an advanced melee system to knock-down, slash, stomp, crush, and blow apart demons in creative and violent ways. Return of id Multiplayer Dominate your opponents in DOOM’s signature, fast-paced arena-style combat. In both classic and all-new game modes, annihilate your enemies utilizing your personal blend of skill, powerful weapons, vertical movement, and unique power-ups that allow you to play as a demon. Endless Possibilities DOOM SnapMap – a powerful, but easy-to-use game and level editor – allows for limitless gameplay experiences on every platform. Without any previous experience or special expertise, any player can quickly and easily snap together and visually customize maps, add pre-defined or completely custom gameplay, and even edit game logic to create new modes. Instantly play your creation, share it with a friend, or make it available to players around the world – all in-game with the push of a button. ",,"Minimum:,OS:,Windows 7/8.1/10 (64-bit versions),Processor:,Intel Core i5-2400/AMD FX-8320 or better,Memory:,8 GB RAM,Graphics:,NVIDIA GTX 670 2GB/AMD Radeon HD 7870 2GB or better,Storage:,55 GB available space,Additional Notes:,Requires Steam activation and broadband internet connection for Multiplayer and SnapMap","Recommended:,OS:,Windows 7/8.1/10 (64-bit versions),Processor:,Intel Core i7-3770/AMD FX-8350 or better,Memory:,8 GB RAM,Graphics:,NVIDIA GTX 970 4GB/AMD Radeon R9 290 4GB or better,Storage:,55 GB available space,Additional Notes:,Requires Steam activation and broadband internet connection for Multiplayer and SnapMap",$19.99,$14.99 +https://store.steampowered.com/app/578080/PLAYERUNKNOWNS_BATTLEGROUNDS/,app,PLAYERUNKNOWN'S BATTLEGROUNDS,PLAYERUNKNOWN'S BATTLEGROUNDS is a battle royale shooter that pits 100 players against each other in a struggle for survival. Gather supplies and outwit your opponents to become the last person standing.,"Mixed,(6,214),- 49% of the 6,214 user reviews in the last 30 days are positive.","Mixed,(836,608),- 49% of the 836,608 user reviews for this game are positive.","Dec 21, 2017",PUBG Corporation,"PUBG Corporation,PUBG Corporation","Survival,Shooter,Multiplayer,Battle Royale,PvP,FPS,Third-Person Shooter,Action,Online Co-Op,Tactical,Co-op,First-Person,Early Access,Strategy,Competitive,Third Person,Team-Based,Difficult,Simulation,Stealth","Multi-player,Online Multi-Player,Stats","English,Korean,Simplified Chinese,French,German,Spanish - Spain,Arabic,Japanese,Polish,Portuguese,Russian,Turkish,Thai,Italian,Portuguese - Brazil,Traditional Chinese,Ukrainian",37,"Action,Adventure,Massively Multiplayer"," About This Game PLAYERUNKNOWN'S BATTLEGROUNDS is a battle royale shooter that pits 100 players against each other in a struggle for survival. Gather supplies and outwit your opponents to become the last person standing. PLAYERUNKNOWN , aka Brendan Greene, is a pioneer of the battle royale genre and the creator of the battle royale game modes in the ARMA series and H1Z1: King of the Kill. At PUBG Corp., Greene is working with a veteran team of developers to make PUBG into the world's premiere battle royale experience."," Mature Content Description The developers describe the content like this: This Game may contain content not appropriate for all ages, or may not be appropriate for viewing at work: Frequent Violence or Gore, General Mature Content ","Minimum:,Requires a 64-bit processor and operating system,OS:,64-bit Windows 7, Windows 8.1, Windows 10,Processor:,Intel Core i5-4430 / AMD FX-6300,Memory:,8 GB RAM,Graphics:,NVIDIA GeForce GTX 960 2GB / AMD Radeon R7 370 2GB,DirectX:,Version 11,Network:,Broadband Internet connection,Storage:,30 GB available space","Recommended:,Requires a 64-bit processor and operating system,OS:,64-bit Windows 7, Windows 8.1, Windows 10,Processor:,Intel Core i5-6600K / AMD Ryzen 5 1600,Memory:,16 GB RAM,Graphics:,NVIDIA GeForce GTX 1060 3GB / AMD Radeon RX 580 4GB,DirectX:,Version 11,Network:,Broadband Internet connection,Storage:,30 GB available space",$29.99, +https://store.steampowered.com/app/637090/BATTLETECH/,app,BATTLETECH,"Take command of your own mercenary outfit of 'Mechs and the MechWarriors that pilot them, struggling to stay afloat as you find yourself drawn into a brutal interstellar civil war.","Mixed,(166),- 54% of the 166 user reviews in the last 30 days are positive.","Mostly Positive,(7,030),- 71% of the 7,030 user reviews for this game are positive.","Apr 24, 2018",Harebrained Schemes,"Paradox Interactive,Paradox Interactive","Mechs,Strategy,Turn-Based,Turn-Based Tactics,Sci-fi,Turn-Based Strategy,Tactical,Singleplayer,Robots,RPG,Action,Multiplayer,Futuristic,Character Customization,Management,Adventure,Space,Story Rich,Great Soundtrack,Difficult","Single-player,Multi-player,Online Multi-Player,Cross-Platform Multiplayer,Steam Achievements,Steam Trading Cards,Steam Cloud","English,French,German,Russian",128,"Action,Adventure,Strategy"," About This Game From original BATTLETECH/MechWarrior creator Jordan Weisman and the developers of the award-winning Shadowrun Returns series comes the next-generation of turn-based tactical 'Mech combat. The year is 3025 and the galaxy is trapped in a cycle of perpetual war, fought by noble houses with enormous, mechanized combat vehicles called BattleMechs. Take command of your own mercenary outfit of 'Mechs and the MechWarriors that pilot them, struggling to stay afloat as you find yourself drawn into a brutal interstellar civil war. Upgrade your starfaring base of operations, negotiate mercenary contracts with feudal lords, repair and maintain your stable of aging BattleMechs, and execute devastating combat tactics to defeat your enemies on the battlefield. COMMAND A SQUAD OF 'MECHS IN TURN-BASED COMBAT Deploy over 30 BattleMechs in a wide variety of combinations. Use terrain, positioning, weapon selection and special abilities to outmaneuver and outplay your opponents. MANAGE YOUR MERCENARY COMPANY Recruit, customize, and develop unique MechWarriors. Improve and customize your dropship. As a Mercenary, travel a wide stretch of space, taking missions and managing your reputation with a variety of noble houses and local factions. TAKE PART IN A DESPERATE CIVIL WAR Immerse yourself in the story of a violently deposed ruler, waging a brutal war to take back her throne with the support of your ragtag mercenary company. CUSTOMIZE YOUR 'MECHS Use your MechLab to maintain and upgrade your units, replacing damaged weapon systems with battlefield salvage taken from fallen foes. PVP MULTIPLAYER & SKIRMISH MODE Customize a Lance of 'Mechs and MechWarriors to go head-to-head with your friends, compete against opponents online, or jump into single-player skirmish mode to test your strategies against the AI. ",,"Minimum:,Requires a 64-bit processor and operating system,OS:,64-bit Windows 7 or Higher,Processor:,Intel® Core™ i3-2105 or AMD® Phenom™ II X3 720,Memory:,8 GB RAM,Graphics:,Nvidia® GeForce™ GTX 560 Ti or AMD® ATI Radeon™ HD 5870 (1 GB VRAM),DirectX:,Version 11,Network:,Broadband Internet connection,Storage:,35 GB available space,Sound Card:,DirectX 9 sound device,Additional Notes:,Multiplayer is compatible between Windows, Mac and Linux versions.,Minimum:,Requires a 64-bit processor and operating system,OS:,macOS High Sierra 10.13.3,Processor:,Intel® Core™ i5-4670,Memory:,8 GB RAM,Graphics:,Nvidia® GeForce™ GTX 775M (2 GB VRAM),Network:,Broadband Internet connection,Storage:,35 GB available space,Additional Notes:,Multiplayer is compatible between Windows, Mac and Linux versions.,Minimum:,Requires a 64-bit processor and operating system,OS:,64-bit Ubuntu 18.04 LTS and higher or SteamOS,Processor:,Intel® Core™ i3-3240 CPU,Memory:,8 GB RAM,Graphics:,Nvidia® GeForce™ GTX 560,Network:,Broadband Internet connection,Storage:,35 GB available space,Additional Notes:,Multiplayer is compatible between Windows, Mac and Linux versions.","Recommended:,Requires a 64-bit processor and operating system,OS:,64-bit Windows 7 or Higher,Processor:,Intel® Core™ i5-4460 or AMD® FX-4300,Memory:,16 GB RAM,Graphics:,Nvidia® GeForce™ GTX 670 or AMD® Radeon™ R9 285 (2 GB VRAM),DirectX:,Version 11,Network:,Broadband Internet connection,Storage:,35 GB available space,Sound Card:,DirectX 9 sound device,Additional Notes:,Multiplayer is compatible between Windows, Mac and Linux versions.,Recommended:,Requires a 64-bit processor and operating system,OS:,macOS High Sierra 10.13.3,Processor:,Intel® Core™ i7-7700K,Memory:,16 GB RAM,Graphics:,AMD® Radeon™ Pro 580 (8 GB VRAM),Network:,Broadband Internet connection,Storage:,35 GB available space,Additional Notes:,Multiplayer is compatible between Windows, Mac and Linux versions.,Recommended:,Requires a 64-bit processor and operating system,OS:,64-bit Ubuntu 18.04 LTS and higher or SteamOS,Processor:,Intel® Core™ i5-4460 or AMD® FX-4300,Memory:,16 GB RAM,Graphics:,Nvidia® GeForce™ GTX 670 or AMD® Radeon™ R9 285 (2 GB VRAM),Network:,Broadband Internet connection,Storage:,35 MB available space,Additional Notes:,Multiplayer is compatible between Windows, Mac and Linux versions.",$39.99, +https://store.steampowered.com/app/221100/DayZ/,app,DayZ,"The post-soviet country of Chernarus is struck by an unknown virus, turning the majority population into frenzied infected. Fighting over resources has bred a hostile mentality among survivors, driving what’s left of humanity to collapse. You are one of the few immune to the virus - how far will you go to survive?","Mixed,(932),- 57% of the 932 user reviews in the last 30 days are positive.","Mixed,(167,115),- 61% of the 167,115 user reviews for this game are positive.","Dec 13, 2018",Bohemia Interactive,"Bohemia Interactive,Bohemia Interactive","Survival,Zombies,Open World,Multiplayer,PvP,Massively Multiplayer,Action,Early Access,Simulation,FPS,Post-apocalyptic,Survival Horror,Shooter,Sandbox,Adventure,Indie,Co-op,Atmospheric,Horror,Military","Multi-player,Online Multi-Player,Steam Workshop,Steam Cloud,Valve Anti-Cheat enabled","English,French,Italian,German,Spanish - Spain,Czech,Russian,Simplified Chinese,Traditional Chinese",NaN,"Action,Adventure,Massively Multiplayer"," About This Game The post-soviet country of Chernarus is struck by an unknown virus, turning the majority population into frenzied infected. Fighting over resources has bred a hostile mentality among survivors, driving what’s left of humanity to collapse. You are one of the few immune to the virus - how far will you go to survive? This is DayZ, this is your story. DayZ is an unforgiving, authentic, open world sandbox online game where each one of 60 players on a server follows a single goal - to survive as long as they can, by all means necessary. There are no superficial tips, waypoints, built-in tutorials or help given to you. Every decision matters - with no save games, and no extra lives, every mistake can be lethal. If you fail, you lose everything and start over. Scavenging for supplies and roaming the open world never feels safe in DayZ, as you never know what's behind the next corner. Hostile player interactions, or simply just struggling through severe weather can easily turn into intense, nerve-racking moments where you experience very real emotions. On the other hand, meeting with another friendly survivor in DayZ can lead to a true friendship that lasts a lifetime... Your choices and your decisions create a gameplay experience that's completely unique and unequivocally personal - unmatched by any other multiplayer game out there. This is DayZ, this is your story. Key Features Detailed, authentic backdrop of Chernarus, an open world terrain featuring 230 square kilometers of hand-crafted environment based on real life locations. Real emotional experience driven by the emergent interactions of 60 players on the server, all fighting for survival by any means necessary. Environmental dangers including the infected, dynamic weather, and animal predators. Wide variety of complex survival mechanics - from hunting and crafting, through sophisticated injury simulation, to transferable diseases. Persistent servers with complex loot economy, and the ability to build improvised bases. Visceral, authentic gun play and melee combat systems. Smooth and reactive character controller utilizing a detailed animation system. Rewarding and authentic experience of driving vehicles for travel and material transport. Robust technology platform featuring modules of Bohemia's new Enfusion Engine. Seamless network synchronization and significantly improved game performance. A platform fully open to user created content, offering the same tool set that we use for actual game development. ",,"Minimum:,OS:,Windows 7/8.1 64-bit,Processor:,Intel Core i5-4430,Memory:,8 GB RAM,Graphics:,NVIDIA GeForce GTX 760 or AMD R9 270X,DirectX:,Version 11,Storage:,16 GB available space,Sound Card:,DirectX®-compatible,Additional Notes:,Internet connection","Recommended:,OS:,Windows 10 64-bit,Processor:,Intel Core i5-6600K or AMD R5 1600X,Memory:,12 GB RAM,Graphics:,NVIDIA GeForce GTX 1060 or AMD RX 580,DirectX:,Version 11,Storage:,25 GB available space,Sound Card:,DirectX®-compatible,Additional Notes:,Internet connection",$44.99, +https://store.steampowered.com/app/8500/EVE_Online/,app,EVE Online,"EVE Online is a community-driven spaceship MMO where players can play free, choosing their own path from countless options. Experience space exploration, immense PvP and PvE battles, mining, industry and a thriving player economy in an ever-expanding sandbox.","Mixed,(287),- 54% of the 287 user reviews in the last 30 days are positive.","Mostly Positive,(11,481),- 74% of the 11,481 user reviews for this game are positive.","May 6, 2003",CCP,"CCP,CCP","Space,Massively Multiplayer,Sci-fi,Sandbox,MMORPG,Open World,RPG,PvP,Multiplayer,Free to Play,Economy,Strategy,Space Sim,Simulation,Action,Difficult,Tactical,Capitalism,PvE,Atmospheric","Multi-player,Online Multi-Player,MMO,Co-op,Online Co-op,Steam Trading Cards","English,German,Russian,French",NaN,"Action,Free to Play,Massively Multiplayer,RPG,Strategy", About This Game ,,"Minimum:,OS:,Windows 7,Processor:,Intel Dual Core @ 2.0 GHz, AMD Dual Core @ 2.0 GHz),Memory:,2 GB,Hard Drive:,20 GB Free Space,Video:,AMD Radeon 2600 XT or NVIDIA GeForce 8600 GTS,Network:,ADSL connection (or faster),Minimum:,Supported OS:,Mac OS X 10.12,Processor:,CPU that supports SSE2 (Intel Dual Core @ 2.0 GHz),Memory:,2 GB,Hard Drive:,20 GB Free Space,Video:,NVIDIA GeForce 320m, Intel HD 3000","Recommended:,OS:,Windows 10,Processor:,Intel i7-7700 or AMD Ryzen 7 1700 @ 3.6 GHz or greater,Memory:,16 GB or greater,Hard Drive:,20 GB free space,Video:,NVIDIA Geforce GTX 1060, AMD Radeon RX 580 or better with at least 4 GB VRAM,Network:,ADSL connection or faster,Recommended:,OS:,Mac OS X 10.14,Processor:,Intel i5 Series @ 3.8 GHz or greater,Memory:,16 GB or higher,Hard Drive:,20 GB free space,Video:,AMD Radeon Pro 580 or better with at least 4 GB VRAM",Free, +https://store.steampowered.com/bundle/5699/Grand_Theft_Auto_V_Premium_Online_Edition/,bundle,Grand Theft Auto V: Premium Online Edition,Grand Theft Auto V: Premium Online Edition bundle,NaN,NaN,NaN,Rockstar North,Rockstar Games,NaN,"Single-player,Multi-player,Downloadable Content,Steam Achievements,Full controller support","English, French, Italian, German, Spanish - Spain, Korean, Polish, Portuguese - Brazil, Russian, Traditional Chinese, Japanese, Simplified Chinese",NaN,"Action,Adventure",NaN,NaN,NaN,NaN,,$35.18 +https://store.steampowered.com/app/601150/Devil_May_Cry_5/,app,Devil May Cry 5,"The ultimate Devil Hunter is back in style, in the game action fans have been waiting for.","Very Positive,(408),- 87% of the 408 user reviews in the last 30 days are positive.","Very Positive,(9,645),- 92% of the 9,645 user reviews for this game are positive.","Mar 7, 2019","CAPCOM Co., Ltd.","CAPCOM Co., Ltd.,CAPCOM Co., Ltd.","Action,Hack and Slash,Great Soundtrack,Demons,Character Action Game,Spectacle fighter,Third Person,Violent,Singleplayer,Classic,Stylized,Gore,Story Rich,Nudity,Multiplayer,Controller,Difficult,Adventure,Anime,Family Friendly","Single-player,Online Multi-Player,Online Co-op,Steam Achievements,Full controller support,Steam Trading Cards,Steam Cloud","English,French,Italian,German,Spanish - Spain,Portuguese - Brazil,Polish,Russian,Simplified Chinese,Traditional Chinese,Japanese,Korean",51,Action," About This Game The Devil you know returns in this brand new entry in the over-the-top action series available on the PC. Prepare to get downright demonic with this signature blend of high-octane stylized action and otherworldly & original characters the series is known for. Director Hideaki Itsuno and the core team have returned to create the most insane, technically advanced and utterly unmissable action experience of this generation! The threat of demonic power has returned to menace the world once again in Devil May Cry 5 . The invasion begins when the seeds of a “demon tree” take root in Red Grave City. As this hellish incursion starts to take over the city, a young demon hunter Nero, arrives with his partner Nico in their “Devil May Cry” motorhome. Finding himself without the use of his right arm, Nero enlists Nico, a self-professed weapons artist, to design a variety of unique mechanical Devil Breaker arms to give him extra powers to take on evil demons such as the blood sucking flying Empusa and giant colossus enemy Goliath. FEATURES High octane stylized action – Featuring three playable characters each with a radically different stylish combat play style as they take on the city overrun with demons Groundbreaking graphics – Developed with Capcom’s in-house proprietary RE engine, the series continues to achieve new heights in fidelity with graphics that utilize photorealistic character designs and stunning lighting and environmental effects. Take down the demonic invasion – Battle against epic bosses in adrenaline fueled fights across the over-run Red Grave City all to the beat of a truly killer soundtrack. Demon hunter – Nero, one of the series main protagonists and a young demon hunter who has the blood of Sparda, heads to Red Grave City to face the hellish onslaught of demons, with weapons craftswoman and new partner-in-crime, Nico. Nero is also joined by stylish, legendary demon hunter, Dante and the mysterious new character, V. "," Mature Content Description The developers describe the content like this: WARNING: This game contains strong language, violence and nudity. ","Minimum:,OS:,WINDOWS® 7, 8.1, 10 (64-BIT Required),Processor:,Intel® Core™ i5-4460, AMD FX™-6300, or better,Memory:,8 GB RAM,Graphics:,NVIDIA® GeForce® GTX 760 or AMD Radeon™ R7 260x with 2GB Video RAM, or better,DirectX:,Version 11,Storage:,35 GB available space,Additional Notes:,*Xinput support Controllers recommended *Internet connection required for game activation. (Network connectivity uses Steam® developed by Valve® Corporation.)","Recommended:,OS:,WINDOWS® 7, 8.1, 10 (64-BIT Required),Processor:,Intel® Core™ i7-3770, AMD FX™-9590, or better,Memory:,8 GB RAM,Graphics:,NVIDIA® GeForce® GTX 1060 with 6GB VRAM, AMD Radeon™ RX 480 with 8GB VRAM, or better,DirectX:,Version 11,Storage:,35 GB available space,Additional Notes:,*Xinput support Controllers recommended *Internet connection required for game activation. (Network connectivity uses Steam® developed by Valve® Corporation.)",$59.99,$70.42 +https://store.steampowered.com/app/477160/Human_Fall_Flat/,app,Human: Fall Flat,Human: Fall Flat is a quirky open-ended physics-based puzzle platformer set in floating dreamscapes. Your goal is to find the exit of these surreal levels by solving puzzles with nothing but your wits. Local co-op for 2 players and up to 8 online for even more mayhem!,"Very Positive,(629),- 91% of the 629 user reviews in the last 30 days are positive.","Very Positive,(23,763),- 91% of the 23,763 user reviews for this game are positive.","Jul 22, 2016",No Brakes Games,"Curve Digital,Curve Digital","Funny,Multiplayer,Co-op,Puzzle,Physics,Local Co-Op,Comedy,Adventure,Indie,Parkour,Puzzle-Platformer,Local Multiplayer,Sandbox,Casual,Open World,Singleplayer,3D Platformer,Split Screen,Simulation,Survival","Single-player,Online Multi-Player,Local Co-op,Shared/Split Screen,Steam Achievements,Full controller support,Steam Trading Cards,Steam Cloud,Stats","English,French,German,Spanish - Spain,Russian,Italian,Simplified Chinese,Japanese,Korean,Polish,Portuguese,Portuguese - Brazil,Thai,Turkish,Ukrainian",55,"Adventure,Indie"," About This Game ***NEW ""DARK"" LEVEL AVAILABLE NOW*** Discover the funniest cooperative physics-based puzzle-platformer! In Human: Fall Flat you play as Bob, a wobbly hero who keeps dreaming about surreal places filled with puzzles in which he’s yet to find the exit. Exploration and ingenuity are key, challenge your creativity as every option is welcome! Bob is just a normal Human with no superpowers, but given the right tools he can do a lot. Misuse the tools and he can do even more! The world of Human: Fall Flat features advanced physics and innovative controls that cater for a wide range of challenges. Bob’s dreams of falling are riddled with puzzles to solve and distractions to experiment with for hilarious results. The worlds may be fantastical, but the laws of physics are very real. FEATURES: ONLINE MULTIPLAYER UP TO 8 PLAYERS Fall into or host private/public lobbies with your friends and watch Bob fall, flail, wobble and stumble together. Need a hand getting that boulder on to a catapult, or need someone to break that wall? The more Bob the more Mayhem! THE WOBBLY ART OF PARKOUR Direct and complete control of Bob. Nothing is scripted and no limits imposed. Bob can walk (kinda straight), jump, grab anything, climb anything, carry anything. LOCAL CO-OP Play with a friend or a relative in split-screen, work together to achieve any task or spend an hour throwing each other about in the craziest ways possible. CUSTOMISATION Paint your own custom Bob, dress him with silly suits or even import your face onto his via webcam. SURREAL LANDSCAPES Explore open-ended levels which obey the rules of physics. Interact with almost every available object in the game and go almost everywhere - like playground of freedom. UNLIMITED REPLAY VALUE Fall into Bob's dreams as many time as you want, try new paths and discover all secrets. Think outside of the box is the philosophy! Fall into Bob's dreams now and see how good your catapulting skills are! ",,"Minimum:,OS:,Windows XP/Vista/7/8/8.1/10 x86 and x64,Processor:,Intel Core2 Duo E6750 (2 * 2660) or equivalent | AMD Athlon 64 X2 Dual Core 6000+ (2 * 3000) or equivalent,Memory:,1024 MB RAM,Graphics:,GeForce GT 740 (2048 MB) or equivalent | Radeon HD 5770 (1024 MB),Storage:,500 MB available space,Minimum:,OS:,OS X 10.9 and higher,Processor:,Intel Core2 Duo E6750 (2 * 2660) or equivalent | AMD Athlon 64 X2 Dual Core 6000+ (2 * 3000) or equivalent,Memory:,1024 MB RAM,Graphics:,GeForce GT 740 (2048 MB) or equivalent | Radeon HD 5770 (1024 MB),Storage:,500 MB available space,Additional Notes:,Requires a two-button mouse or controller","Recommended:,OS:,Windows XP/Vista/7/8/8.1/10 x86 and x64,Processor:,Intel Core2 Quad Q9300 (4 * 2500) or equivalent | AMD A10-5800K APU (4*3800) or equivalent,Memory:,2048 MB RAM,Graphics:,GeForce GTX 460 (1024 MB) or equivalent | Radeon HD 7770 (1024 MB),Storage:,500 MB available space,Recommended:,OS:,OS X 10.9 and higher,Processor:,Intel Core2 Quad Q9300 (4 * 2500) or equivalent | AMD A10-5800K APU (4*3800) or equivalent,Memory:,2048 MB RAM,Graphics:,GeForce GTX 460 (1024 MB) or equivalent | Radeon HD 7770 (1024 MB),Storage:,500 MB available space,Additional Notes:,Requires a two-button mouse or controller",$14.99,$17.58 +https://store.steampowered.com/app/644930/They_Are_Billions/,app,They Are Billions,They Are Billions is a Steampunk strategy game set on a post-apocalyptic planet. Build and defend colonies to survive against the billions of the infected that seek to annihilate the few remaining living humans. Can humanity survive after the zombie apocalypse?,"Very Positive,(192),- 83% of the 192 user reviews in the last 30 days are positive.","Very Positive,(12,127),- 85% of the 12,127 user reviews for this game are positive.","Dec 12, 2017",Numantian Games,"Numantian Games,Numantian Games","Early Access,Base Building,Strategy,Zombies,Survival,RTS,Steampunk,City Builder,Tower Defense,Post-apocalyptic,Building,Singleplayer,Resource Management,Real-Time with Pause,Early Access,Difficult,Tactical,Management,Indie,Isometric","Single-player,Steam Achievements,Steam Trading Cards,Steam Cloud,Stats,Steam Leaderboards","English,Spanish - Spain,French,German,Japanese,Korean,Polish,Portuguese - Brazil,Russian,Simplified Chinese,Traditional Chinese,Italian",34,"Strategy,Early Access"," About This Game They Are Billions is a strategy game in a distant future about building and managing human colonies after a zombie apocalypse destroyed almost all of human kind. Now there are only a few thousand humans left alive that must struggle to survive under the threat of the infection. Billions of infected roam around the world in massive swarms seeking the last living human colonies. Survival Mode - Available Now! In this mode, a random world is generated with its own events, weather, geography, and infected population. You must build a successful colony that must survive for a specific period of time against the swarms of infected. It is a fast and ultra addictive game mode. We plan to release a challenge of the week where all players must play the same random map. The best scores will be published in a leaderboard. Real Time with Pause This is a real-time strategy game, but don’t get too nervous. You can pause the action to take the best strategic and tactical decisions. In Pause Mode, you can place structures to build, give orders to your army, or consult all of the game’s information. This game is all about strategy, not player performance or the player’s skill to memorize and quickly execute dozens of key commands. Pause the game and take all the time you need! Build your Colony Build dwellings and acquire food for the colonists. They will come to live and work for the colony. Collect resources from the environment using various building structures. Upgrade buildings to make them more efficient. Expand the energy distribution of the colony by placing Tesla Towers and build mills and power plants to feed the energy to your buildings. Build walls, gates, towers, and structures to watch the surroundings. Don’t let the infected take over the colony! Build an Army What kind of people would want to combat the infected? Only the maddest ones. Train and contract mercenaries to protect the colony. They demand their money and food, and you will have to listen to their awful comments, but these tormented heroes will be your best weapon to destroy the infected. Every unit is unique and has their own skills and personality – discover them! Thousands of Units on Screen Yes! They are billions! The world is full of infected creatures… they roam, smell, and listen. Every one of them has their own AI. Make noise and they will come – kill some of them to access an oil deposit and hundred of them will come to investigate. We have created our custom engine to handle hordes of thousands of the infected, up to 20,000 units in real time. Do you think your colony is safe? Wait for the swarms of thousands of infected that are roaming the world. Sometimes your colony is in their path! Prevent the Infection If just one of the infected breaks into a building, all the colonies and workers inside will become infected. The infected workers will then run rabid to infect more buildings. Infections must be eradicated from the beginning, otherwise, it will grow exponentially becoming impossible to contain. Beautiful 4K Graphics! Prepare to enjoy these ultra high definition graphics. Our artists have created tons of art pieces: Beautiful buildings with their own animations, thousands of frames of animation to get the smoothest movements and everything with a crazy Steampunk and Victorian style! Future Game Mode - Campaign In the campaign mode, you have to help the surviving humans to reconquer their country. There are dozens of missions with different goals: building colonies, tactical missions, rescue missions... You can decide the path of progress and mission - research new technologies, advances and upgrades to improve your colonies and army. We are working right now on the campaign and we expect it will be available for Winter 2018. The campaign will be included for free to all the buyers of the Steam Early Access version.",,"Minimum:,OS:,Windows 7, 8, 10 (32 and 64 bits),Processor:,INTEL, AMD 2 cores CPU at 2Ghz,Memory:,4 GB RAM,Graphics:,Intel HD3000, Radeon, Nvidia card with shader model 3, 1GB video ram.,DirectX:,Version 9.0c,Storage:,4 GB available space,Additional Notes:,Minimum resolution: 1360x768, recomended FULL HD 1920x1080.","Recommended:,OS:,Windows 7, 8, 10 (64 bits),Processor:,INTEL. AMD 4 cores CPU at 3Ghz,Memory:,8 GB RAM,Graphics:,Radeon 7950 or above, Nvidia GTX 670 or above. 4GB video ram.,DirectX:,Version 9.0c,Storage:,4 GB available space,Additional Notes:,4K Monitor (3840x2160)",$29.99, +https://store.steampowered.com/app/774241/Warhammer_Chaosbane/,app,Warhammer: Chaosbane,"In a world ravaged by war and dominated by magic, you must rise up to face the Chaos hordes. Playing solo or with up to four players in local or online co-op, choose a hero from four character classes and prepare for epic battles wielding some of the most powerful artefacts of the Old World.",,"Mixed,(904),- 44% of the 904 user reviews for this game are positive.","May 31, 2019",Eko Software,"Bigben Interactive,Bigben Interactive","RPG,Adventure,Hack and Slash,Action,Action RPG,Games Workshop,Violent,Fantasy,Co-op,War,Isometric,Dark Fantasy,Warhammer 40K,Historical,Colorful","Single-player,Multi-player,Co-op,Online Co-op,Local Co-op,Steam Achievements,Full controller support,Steam Trading Cards,Steam Cloud","English,French,Italian,German,Spanish - Spain,Simplified Chinese,Traditional Chinese,Korean,Spanish - Latin America,Japanese,Polish,Portuguese - Brazil,Russian",43,"Action,Adventure,RPG"," About This Game “Keep your eyes on this one, because it’s one quality Action RPG” – Entertainment Buddha In a world ravaged by war and dominated by magic, you are the last hope for the Empire of Man against the Chaos hordes. Playing solo or with up to 4 in local or online co-op, choose a hero from 4 character classes with unique and complementary skills, and prepare for epic battles wielding some of the most powerful artefacts of the Old World. • THE FIRST HACK AND SLASH set in the Warhammer Fantasy world, told through an all-new story written by Mike Lee (a Black Library author) and featuring a soundtrack composed by Chance Thomas. • FEROCIOUS BATTLES: from the sewers of Nuln to the ruined streets of Praag, fight your way through monster hordes using over 180 different powers. Activate your bloodlust, a devastating skill, to escape the most perilous situations. • 4 CHARACTER CLASSES , each with unique skills and customisation: a soldier of the Empire who can take heavy damage, a Dwarf specialising in melee combat, a High Elf who deals ranged damage by manipulating magic or a Wood Elf who lays deadly traps and wields the bow like no other! • AN XXL BESTIARY with over 70 monsters aligned with the Chaos Gods and unique bosses. Battle Nurgle's minions, Khorne's spawn and waves of other vile creatures! • OPTIMIZED FOR CO-OP : solo or with up to 4 players, local or online, the class synergy and interface have been designed for co-op. Combine different skills and powers to create even more devastating effects. • HIGH REPLAY VALUE : Story mode, a boss rush mode, countless dungeons and regular updates offer a rich and varied gaming experience. And with 10 difficulty levels, you can find the right challenge to test your abilities. "," Mature Content Description The developers describe the content like this: This game may contain content not appropriate for all ages, or may not be appropriate for viewing at work: Frequent Violence ","Minimum:,Requires a 64-bit processor and operating system,OS:,64bits version of Windows® 7, Windows® 8, Windows® 10,Processor:,Intel® Core i3 or AMD Phenom™ II X3,Memory:,6 GB RAM,Graphics:,NVIDIA® GeForce® GTX 660 or AMD Radeon™ HD 7850 with 2 GB RAM,DirectX:,Version 11,Storage:,20 GB available space,Sound Card:,DirectX Compatible Soundcard","Recommended:,Requires a 64-bit processor and operating system,OS:,64bits version of Windows® 7, Windows® 8, Windows® 10,Processor:,Intel® Core i5 or AMD FX 8150,Memory:,6 GB RAM,Graphics:,NVIDIA® GeForce® GTX 780 or AMD Radeon™ R9 290 with 2 GB RAM,DirectX:,Version 11,Storage:,20 GB available space,Sound Card:,DirectX Compatible Soundcard",$49.99, +https://store.steampowered.com/app/527230/For_The_King/,app,For The King,"For The King is a strategic RPG that blends tabletop and roguelike elements in a challenging adventure that spans the realms. Set off on a single player experience or play cooperatively both online and locally. INTO THE DEEP ADVENTURE, NOW AVAILABLE FOR FREE!","Very Positive,(67),- 80% of the 67 user reviews in the last 30 days are positive.","Very Positive,(4,600),- 83% of the 4,600 user reviews for this game are positive.","Apr 19, 2018",IronOak Games,"Curve Digital,Curve Digital","RPG,Turn-Based Combat,Adventure,Online Co-Op,Co-op,Strategy,Rogue-like,Turn-Based,Multiplayer,Turn-Based Strategy,Party-Based RPG,Indie,Fantasy,Board Game,Strategy RPG,Hex Grid,Rogue-lite,Difficult,Local Co-Op,Early Access","Single-player,Multi-player,Online Multi-Player,Local Multi-Player,Co-op,Online Co-op,Local Co-op,Shared/Split Screen,Steam Achievements,Full controller support,Steam Trading Cards,Steam Cloud","English,French,Italian,German,Spanish - Spain,Portuguese - Brazil,Russian,Simplified Chinese,Traditional Chinese,Polish,Japanese,Korean",72,"Adventure,Indie,RPG,Strategy"," About This Game Into The Deep, a brand new adventure sets sail for free, NOW! Includes: Dungeon Crawl Adventure Frost Adventure Adventure Gold Rush Un-cooperative Mode Hildebrants Cellar Adventure Into The Deep Adventure The King is dead, murdered by an unknown assailant. Now the once peaceful kingdom of Fahrul is in chaos. With nowhere left to turn and stretched beyond her means, the queen has put out a desperate plea to the citizens of the land to rise up and help stem the tide of impending doom. Set off with your make-shift party, either single player , local , or online co-op . Choose to split your party up and cover more ground, or stick together for protection. A sound strategy can mean the difference between life and death. For The King is a challenging blend of Strategy , JRPG Combat , and Roguelike elements. Each play through is made unique with procedural maps , quests, and events. Brave the relentless elements, fight the wicked creatures, sail the seas and delve into the dark underworld. None before you have returned from their journey. Will you be the one to put an end to the Chaos? Fight and die as a party in fast paced and brutal turn-based combat using a unique slot system for attacks and special abilities. Find and gather herbs for your trusty pipe to heal your wounds and cure your maladies. Set up safe camps or brave the horrors that nightfall brings. Just remember adventurer, you do this not for the riches or fame but for your village, for your realm, For The King! ",,"Minimum:,Requires a 64-bit processor and operating system,OS:,Windows 7 / 8 / 8.1 / 10 x64,Processor:,Intel Core2 Duo E4300 (2 * 1800) / AMD Athlon Dual Core 4450e (2 * 2300) or equivalent,Memory:,4096 MB RAM,Graphics:,GeForce 8800 GTX (768 MB) / Intel HD 4600 / Radeon HD 3850 (512 MB),DirectX:,Version 9.0c,Storage:,3 GB available space,Minimum:,Requires a 64-bit processor and operating system,OS:,OSX 10.10.5 Yosemite or higher,Processor:,Intel Core i5-2520M (2 * 2500),Memory:,4096 MB RAM,Graphics:,GeForce GT 750M (1024 MB),Storage:,3 GB available space,Minimum:,Requires a 64-bit processor and operating system,OS:,Ubuntu 17.10 (x64) or Mint 18.3 (Cinnamon) (x64) or Ubuntu 16.04 (x64),Processor:,Intel Core2 Duo E4300 (2 * 1800) / AMD Athlon Dual Core 4450e (2 * 2300) or equivalent,Memory:,4096 MB RAM,Graphics:,GeForce 8800 GTX (768 MB) / Intel HD 4600 or equivalent,Storage:,3 GB available space","Recommended:,Requires a 64-bit processor and operating system,OS:,Windows 7 / 8 / 8.1 / 10 x64,Processor:,Intel Core i5-4570T (2* 2900) / AMD FX-6100 (6 * 3300) or equivalent,Memory:,4096 MB RAM,Graphics:,GeForce GTX 750 Ti (2048 MB) / Radeon HD 7850 (2048 MB),DirectX:,Version 9.0c,Storage:,3 GB available space,Recommended:,Requires a 64-bit processor and operating system,OS:,OSX 10.10.5 Yosemite or higher,Processor:,Intel Core i5-6500 (4 * 3200),Memory:,8192 MB RAM,Graphics:,AMD Radeon R9 M390 (2048 MB),Storage:,3 GB available space,Recommended:,Requires a 64-bit processor and operating system,OS:,Ubuntu 17.10 (x64) or Mint 18.3 (Cinnamon) (x64) or Ubuntu 16.04 (x64),Processor:,Intel Core i5-4570T (2* 2900) / AMD FX-6100 (6 * 3300) or equivalent,Memory:,4096 MB RAM,Graphics:,GeForce GTX 750 Ti (2048 MB) / Radeon HD 7850 (2048 MB),Storage:,3 GB available space",$19.99, +https://store.steampowered.com/app/567640/Danganronpa_V3_Killing_Harmony/,app,Danganronpa V3: Killing Harmony,"A new cast of 16 characters find themselves kidnapped and imprisoned in a school. Inside, some will kill, some will die, and some will be punished. Reimagine what you thought high-stakes, fast-paced investigation was as you investigate twisted murder cases and condemn your new friends to death.","Very Positive,(78),- 82% of the 78 user reviews in the last 30 days are positive.","Very Positive,(3,547),- 84% of the 3,547 user reviews for this game are positive.","Sep 25, 2017","Spike Chunsoft Co., Ltd.","Spike Chunsoft Co., Ltd.,Spike Chunsoft Co., Ltd.","Story Rich,Anime,Visual Novel,Detective,Mystery,Great Soundtrack,Female Protagonist,Singleplayer,Adventure,Psychological Horror,Dark Humor,Horror,Dark Comedy,Puzzle,Atmospheric,Memes,Dark,Funny,Comedy,Dating Sim","Single-player,Steam Achievements,Full controller support,Steam Trading Cards,Steam Cloud","English,French,Japanese,Simplified Chinese,Traditional Chinese",41,Adventure," About This Game Welcome to a new world of Danganronpa, and prepare yourself for the biggest, most exhilarating episode yet. Set in a “psycho-cool” environment, a new cast of 16 characters find themselves kidnapped and imprisoned in a school. Inside, some will kill, some will die, and some will be punished. Reimagine what you thought high-stakes, fast-paced investigation was as you investigate twisted murder cases and condemn your new friends to death. Key Features A New Danganronpa Begins: Forget what you thought you knew about Danganronpa and join a completely new cast of Ultimates for a brand-new beginning. Murder Mysteries: In a world where everyone is trying to survive, nobody’s motivations are quite what they seem. Use your skills to solve each new murder or meet a gruesome end. Lie, Panic, Debate! The world is shaped by our perception of it. Fast-paced trial scenes will require lies, quick wits, and logic to guide your classmates to the right conclusions. New Minigames: Between the madness of murdered peers and deadly trials, enjoy an abundance of brand-new minigames! ",,"Minimum:,Requires a 64-bit processor and operating system,OS:,Windows 7 64-bit,Processor:,Intel Core i3-4170 @ 3.70GHz,Memory:,4 GB RAM,Graphics:,NVIDIA@ GeForce@ GTX 460 or better,DirectX:,Version 11,Storage:,26 GB available space,Sound Card:,DirectX compatible soundcard or onboard chipset","Recommended:,Requires a 64-bit processor and operating system,OS:,Windows 7 64-bit,Processor:,Intel Core i5-4690K @3.50GHz,Memory:,8 GB RAM,Graphics:,NVIDIA@ GeForce@ GTX 960,DirectX:,Version 11,Storage:,26 GB available space,Sound Card:,DirectX compatible soundcard or onboard chipset",$39.99,$59.97 +https://store.steampowered.com/app/323370/TERA/,app,TERA,"From En Masse Entertainment, TERA is at the forefront of a new breed of MMO. With True Action Combat - aim, dodge, and time your attacks for intense and rewarding tactical combat. Add the deep social experience of a MMO to best-in-class action combat mechanics for a unique blend of both genres. Play now for free!","Mixed,(76),- 63% of the 76 user reviews in the last 30 days are positive.","Mostly Positive,(14,184),- 78% of the 14,184 user reviews for this game are positive.","May 5, 2015","Bluehole, Inc.","En Masse Entertainment,En Masse Entertainment","Free to Play,MMORPG,Massively Multiplayer,RPG,Open World,Action,Fantasy,Adventure,Anime,Third Person,Character Customization,Action RPG,Multiplayer,Co-op,PvP,Hack and Slash,PvE,Cute,Controller,Nudity","Multi-player,MMO,Co-op,Steam Trading Cards,Partial Controller Support",English,NaN,"Action,Adventure,Free to Play,Massively Multiplayer,RPG"," About This Game TERA is at the forefront of a new breed of MMO. With True Action Combat - aim, dodge, and time your attacks for intense and rewarding tactical combat. Add the deep social experience of a MMO to best-in-class action combat mechanics for a unique blend of both genres. Play now for free! Experience an action MMO that goes beyond ""point and click!"" In TERA , it's your skill, position, timing, and aim that determine the outcome of combat—not how quickly you cycle through targets with the Tab key. If you prefer to lie down on your couch while playing games (we do!), you'll be happy to hear that TERA also offers controller support. Team up with friends to take down Big Ass Monsters or put your combat skills to the test against other players in one of the many PvP battlegrounds TERA offers. Customize your choice of seven character races and nine classes with unique battle styles. Fight thousands of monsters throughout a variety of landscapes, and embark on thousands of quests in a game world rich in history and lore. In a truly free-to-play experience. TERA imposes no artificial cap on classes, zones, or what you can do, or how good you can be - all of the content in the game can be experienced without paying a single penny. To support the continued development of TERA , we offer account services and cosmetic items - costumes, accessories, weapon skins, mounts, and more. We heard players on Steam really like hats... Whether you fight as part of a guild or join an alliance, being part of TERA's vibrant community means that there is always someone to share your journey with."," Mature Content Description The developers describe the content like this: This Game may contain content not appropriate for all ages, or may not be appropriate for viewing at work: General Mature Content ","Minimum:,OS:,Windows 7, 32-bit,Processor:,Intel i3 2130 / AMD FX 4130,Memory:,4 GB RAM,Graphics:,GeForce 9800 GT / Radeon HD 3870,DirectX:,Version 9.0c,Network:,Broadband Internet connection,Storage:,45 GB available space","Recommended:,OS:,Windows 7, 8, 8.1, 10, 64-bit,Processor:,Intel i5 3570 / AMD FX 6350,Memory:,8 GB RAM,Graphics:,GeForce GTS 450 / Radeon HD 4890 1GB,DirectX:,Version 9.0c,Network:,Broadband Internet connection,Storage:,55 GB available space",Free to Play, +https://store.steampowered.com/app/393080/Call_of_Duty_Modern_Warfare_Remastered/,app,Call of Duty®: Modern Warfare® Remastered,"One of the most critically-acclaimed games in history, Call of Duty: Modern Warfare is back, remastered in true high-definition, featuring improved textures, physically based rendering, high-dynamic range lighting and much more.","Mixed,(33),- 51% of the 33 user reviews in the last 30 days are positive.","Mixed,(1,118),- 51% of the 1,118 user reviews for this game are positive.","Jul 27, 2017","Raven Software,Beenox","Activision,Activision","FPS,Action,Shooter,Multiplayer,Violent,War,Singleplayer,First-Person,Military,Remake,Controller,Casual,Classic","Single-player,Online Multi-Player,Online Co-op,Steam Achievements","English,French,Italian,German,Spanish - Spain,Japanese,Korean,Polish,Portuguese - Brazil,Russian,Simplified Chinese,Traditional Chinese",50,Action," About This Game One of the most critically-acclaimed games in history, Call of Duty: Modern Warfare is back, remastered in true high-definition, featuring improved textures, physically based rendering, high-dynamic range lighting and much more. Developed by Infinity Ward, the award-winning Call of Duty® 4: Modern Warfare® set a new standard upon its release for intense, cinematic action, while receiving universal praise as one of the most influential video games of all-time. Winner of numerous Game of the Year honors, Call of Duty 4: Modern Warfare became an instant classic and global phenomenon that set the bar for first-person shooters, and now it returns for a new generation of fans. Relive one of the most iconic campaigns in history, as you are transported around the globe, including fan favorite missions ""All Ghillied Up,"" ""Mile High Club,"" and ""Crew Expendable."" You’ll suit up as unforgettable characters Sgt. John ""Soap"" MacTavish, Capt. John Price and more, as you battle a rogue enemy group across global hotspots from Eastern Europe and rural Russia, all the way to the Middle East. Through an engaging narrative full of twists and turns, call on sophisticated technology and superior firepower as you coordinate land and air strikes on a battlefield where speed and accuracy are essential to victory. Additionally, team up with your friends in the online mode that redefined Call of Duty by introducing killstreaks, XP, Prestige and more in customizable, classic multiplayer modes.",,"Minimum:,Requires a 64-bit processor and operating system,OS:,Windows 7 64-Bit or later,Processor:,Intel Core i3-3225 @ 3.30GHz or equivalent,Memory:,8 GB RAM,Graphics:,NVIDIA GeForce GTX 660 2GB / AMD Radeon HD 7850 2GB,DirectX:,Version 11,Network:,Broadband Internet connection,Sound Card:,DirectX 11 Compatible,Additional Notes:,Disk space requirement may change over time.","Recommended:,Requires a 64-bit processor and operating system",1.020,$906.48 +https://store.steampowered.com/app/253250/Stonehearth/,app,Stonehearth,"Pioneer a living world full of warmth, heroism, and mystery. Help a small group of settlers build a home for themselves in a forgotten land. Establish a food supply, build shelter, defend your people, monitor their moods, and find a way to grow and expand, facing challenges at every step.","Mixed,(66),- 40% of the 66 user reviews in the last 30 days are positive.","Mostly Positive,(5,484),- 75% of the 5,484 user reviews for this game are positive.","Jul 25, 2018",Radiant Entertainment,"(none),(none)","City Builder,Building,Sandbox,Strategy,Survival,Simulation,Crafting,Voxel,Early Access,Indie,Singleplayer,Open World,RPG,Management,Multiplayer,Fantasy,Cute,Adventure,God Game,RTS","Single-player,Multi-player,Online Multi-Player,Local Multi-Player,Co-op,Online Co-op,Local Co-op,Steam Trading Cards,Steam Workshop",English,NaN,"Indie,Simulation,Strategy"," About This Game In Stonehearth, you pioneer a living world full of warmth, heroism, and mystery. Help a small group of settlers build a home for themselves in a forgotten land. You’ll need to establish a food supply, build shelter, defend your people, monitor their moods, and find a way to grow and expand, facing challenges at every step. Starting from procedurally generated terrain with dynamic AI encounters, Stonehearth combines community management and combat with infinite building possibilities. It’s designed to be moddable at every level, from your city to the people and creatures inhabiting the world, and will ship with the tools and documentation for you to add your own customizations to the game, and share them with friends. Build and Grow Your City The heart of the game is city building and management. When you're just starting out, you'll need to juggle tasks like obtaining a sustainable food supply, building shelter, and defending your fledgling settlement from raiders and other threats. Once you've achieved a foothold in the world, it's up to you to write the destiny for your people. You have the flexibility to choose your own path in this game. Do you want to build a great conquering empire? A vibrant trade city? A spiritual monastery? We really want you to feel like this is your settlement, and give you the tools that make it look and operate exactly as you wish. Level Up Your Settlers All the settlers in your towns have jobs. A job is like a class in a role playing game. Each job has a specific role like hauling materials, building, crafting, and fighting. As your hearthlings work at a job they will gain experience and levels. Some jobs, when they meet certain prerequisites, can upgrade to entirely new jobs with new capabilities. Usually, to assign someone a new job you'll also need to craft a tool for them. The Mason can craft blocks, statues, and tools from stone, but to do it he'll need a mallet and chisel crafted by the carpenter. Our goal is to have a job tree that's both very wide and very deep, so there will be plenty of different kinds of things to do in the game, but also a lot of depth to explore if you want to concentrate on any one area. Player Driven Legacy Through Modding We LOVE mods and want to make it as easy as possible to author and share mods. Want to see a new kind of sword in the game? You can model it, define its stats, and then craft it in game. You can also share the design with other players so they can enjoy it too, or bring their authored content into your game. As a modder you’ll be able to do basically anything that we as developers can do: introduce new items and monsters, write new scripted adventures, influence the AI, you name it. It goes back to that original pen and paper RPG experience, where “the game” is a collaboration between the core ruleset and the stories crafted by the gamemaster.",,"Minimum:,OS:,Windows 7/8/8.1/10 (32-bit or 64-bit),Processor:,Intel or AMD Dual-Core, 1.7 GHz+,Memory:,4 GB RAM,Graphics:,nVidia GeForce GT 430 512MB, Radeon HD 7570M, Intel HD 4000,Storage:,2 GB available space,Additional Notes:,OS Updates: Windows 7 SP1,Minimum:,OS:,macOS 10.11 (El Capitan) or later,Processor:,Intel Core i5 1.7GHz,Memory:,8 GB RAM,Graphics:,Radeon HD 5870 or GeForce 630, or Intel HD Graphics 4000,Storage:,2 GB available space","Recommended:,OS:,Windows 10,Processor:,Intel or AMD Quad-Core, 2.8 GHz+,Memory:,16 GB RAM,Graphics:,nVidia GeForce 780 or better, AMD Radeon RX 580 or better,Network:,Broadband Internet connection,Storage:,5 GB available space,Additional Notes:,Hard Drive: SSD for optimal performance,Recommended:,OS:,macOS 10.12 (Sierra) or later,Processor:,Intel Core i7 2.8 GHz+,Memory:,16 GB RAM,Graphics:,NVIDIA GeForce GT 750M or better,Storage:,5 GB available space",$19.99, +https://store.steampowered.com/bundle/5641/Hearts_of_Iron_IV_Mobilization_Pack/,bundle,Hearts of Iron IV: Mobilization Pack,Hearts of Iron IV: Mobilization Pack bundle,NaN,NaN,NaN,Paradox Development Studio,Paradox Interactive,NaN,"Single-player,Multi-player,Online Multi-Player,Co-op,Cross-Platform Multiplayer,Downloadable Content,Steam Achievements,Steam Trading Cards,Steam Workshop,Steam Cloud","English, French, German, Polish, Portuguese - Brazil, Russian, Spanish - Spain",NaN,"Simulation,Strategy",NaN,NaN,NaN,NaN,,$94.45 +https://store.steampowered.com/app/597170/Clone_Drone_in_the_Danger_Zone/,app,Clone Drone in the Danger Zone,"Clone Drone in the Danger Zone is a third person sword fighter where any part of your body can be sliced off. With your mind downloaded into a robot gladiator, you must survive the sinister trials of the arena.","Very Positive,(88),- 94% of the 88 user reviews in the last 30 days are positive.","Very Positive,(1,901),- 94% of the 1,901 user reviews for this game are positive.","Mar 16, 2017",Doborog Games,"Doborog Games,Doborog Games","Early Access,Robots,Action,Swordplay,Fighting,Early Access,Indie,Funny,Singleplayer,Voxel,Futuristic,Pixel Graphics,Multiplayer,Third Person,Comedy,Difficult,Survival,Dark Humor,Philisophical,Rogue-like","Single-player,Multi-player,Online Multi-Player,Steam Achievements,Full controller support,Captions available,Steam Workshop,Steam Cloud,Includes level editor",English,12,"Action,Indie,Early Access"," About This Game CONGRATULATIONS HUMAN! 🤖 Apologies for the brief agony while we harvested your mind. But now your thought patterns are safely encased in this sleek, shiny robot! With a laser sword. SURVIVE AND PERHAPS YOU WILL EARN UPGRADES. GOOD LUCK IN THE ARENA. We hope you survive longer than the last contestant... Oh, and the one before that... Game Modes 1. Story Mode - One part epic tale of human defiance, another part laser swords. 2. Endless Mode - Challenge yourself to fight through 86 level variants spread across 5 difficulty tiers. Can you make it to 🏆TITANIUM🏆 ? 3. Twitch Mode - TWITCH PLAYS YOU . Your Twitch stream viewers earn coins, bet on your gameplay and spawn enemies to kill you (or give you ❤️s—if they really like you). !spawn jetpack2 Kappa Kappa Kappa 4. Challenge Mode - Bow-only, hammer-only, can you still survive the arena? 5. Level Editor - Make custom levels and challenges and share them with other humans! Explore the rich Workshop library of awesome human-built levels! 6. Online Multiplayer Duels (1v1) - Head to head combat! 7. Last Bot Standing​ (2-15 players online) - a Battle-Royale-like game mode with short, 5-10 minute play sessions. Survive, and shower in the remains of your fellow robot competitors. Features (in the game right now!) Epic Voxel Dismemberment : In addition to looking cool, the ability to cut off body parts is central to the gameplay. Jumping on one leg is a common occurrence. Large enemies need to be cut down to size before a fatal blow can be delivered. Sharp Sword Combat : Combat is fast and intense, putting great emphasis on movement, timing and positioning. You are never safe, as any blow can instantly kill or dismember. Entertaining Commentary : With 7,421 spoken words, Commentatron and Analysis-Bot provide a running commentary of your performance and react to your activities. Upgrade your robot: jetpack, bow, kicking, deflection, clones, giant hammers, FIRE 🔥🔥🔥. Terrifying robotic enemies that actually get pretty hard! Sword robots, bow robots, Spidertron 5000, Spidertron 6000, gigantic hammer bots. So many things to dismember. Alpha Limitations: See Early Access description above. Hone your sword skills now, before the rest of humanity gets its turn. JOIN THE HUMAN RESISTANCE! Or don't! It's your future-robo-funeral... 😈",,,,$14.99, +https://store.steampowered.com/app/899440/GOD_EATER_3/,app,GOD EATER 3,"Set in a post-apocalyptic setting, it’s up to your special team of God Eaters to take down god-like monsters devastating the world. With an epic story, unique characters, and all new God Arcs and Aragami, the latest evolution in ACTION is here!","Mostly Positive,(56),- 71% of the 56 user reviews in the last 30 days are positive.","Mostly Positive,(1,945),- 77% of the 1,945 user reviews for this game are positive.","Feb 7, 2019",BANDAI NAMCO Studios Inc.,"BANDAI NAMCO Entertainment,BANDAI NAMCO Entertainment","Anime,Action,Character Customization,Co-op,Hunting,Hack and Slash,JRPG,Multiplayer,RPG,Singleplayer,Great Soundtrack,Post-apocalyptic,Online Co-Op,Third Person,Story Rich,Action RPG,Adventure,Female Protagonist","Single-player,Online Multi-Player,Online Co-op,Steam Achievements,Steam Trading Cards,Partial Controller Support,Steam Cloud","English,Japanese,French,Italian,German,Spanish - Spain,Korean,Portuguese - Brazil,Russian,Spanish - Latin America,Traditional Chinese",32,Action," About This Game Rise Above a World of Desolation The latest entry in the hugely popular God Eater action series is here! • Fight in Style with Brand-new God Arcs! Expand your close-combat armory with the dual-wield God Arc “Biting Edge” and the two-handed moon axe """"Heavy Moon"""", or fight from afar with the new """"Ray Gun"""" God Arc! • New Abilities for Exhilarating Battles! Ground, Air, and Step attacks evolve into powerful techniques with Burst Arts, and the new Dive dash attack allows you full freedom of movement to hunt down wandering Aragami! • Fearsome New Threats: Ash Aragami and Devour Attacks! Dangerous new foes, Ash Aragami can utilize Devours Attacks and enter Burst Mode, increasing their strength exponentially! These enemies are not to be trifled with and will require you to take your weapon and your game to the next level!",,"Minimum:,Requires a 64-bit processor and operating system,OS:,Windows 7 64-bit, SP1,Processor:,Intel Core i5-3470 or AMD FX-8120,Memory:,4 GB RAM,Graphics:,GeForce GTX 760 or Radeon R9 290X,DirectX:,Version 11,Network:,Broadband Internet connection,Storage:,25 GB available space","Recommended:,Requires a 64-bit processor and operating system,OS:,Windows 10 64-bit,Processor:,Intel Core i7-3770 or AMD Ryzen 5 1600,Memory:,8 GB RAM,Graphics:,GeForce GTX 970 or Radeon R9 fury,DirectX:,Version 11,Network:,Broadband Internet connection,Storage:,25 GB available space",$59.99, +https://store.steampowered.com/app/767560/War_Robots/,app,War Robots,"War Robots is an online third-person 6v6 PvP shooter—we’re talking dozens of combat robots, hundreds of weapons combinations, and heated clan battles.","Mixed,(91),- 49% of the 91 user reviews in the last 30 days are positive.","Mixed,(1,797),- 44% of the 1,797 user reviews for this game are positive.","Apr 5, 2018",Pixonic,"Pixonic,Pixonic","Free to Play,Robots,Action,Multiplayer,FPS,Mechs,Massively Multiplayer,Shooter,PvP,Controller,Third-Person Shooter,Co-op,War","Multi-player,Online Multi-Player,Steam Trading Cards,In-App Purchases","English,French,Italian,German,Spanish - Spain,Dutch,Japanese,Korean,Polish,Portuguese - Brazil,Russian,Simplified Chinese,Thai,Traditional Chinese,Turkish",NaN,"Action,Free to Play"," About This Game War is raging, pilot! Are you ready for surprise attacks, intricate tactical maneuvers and the many sneaky tricks your rivals have in store for you? Destroy enemy robots, capture all the beacons, and upgrade your weapons to increase your battle robot’s combat strength, speed and durability. Prove yourself in each map and use different strategies and tactics to emerge victorious in battle. The renowned iOS and Android hit is coming to Steam! Fight other Pilots from all over the world and join millions of existing players! FEATURES: Tactical 6v6 PVP 45 battle robots with different strengths over 50 weapon types, including ballistic missiles, energy and plasma guns. What will you choose? 12 Maps to battle on! numerous possible combinations of robots and weapons. Create a war machine to fit your own playing style; create your own clan and lead it to glorious victories; join epic PvP battles against rivals from all over the world; complete military tasks for bonuses and earn the title Best Pilot. Onward, soldier! Victory is yours!",,"Minimum:,OS:,Windows 7,Processor:,2.5 GHz,Memory:,2 GB RAM,Graphics:,Intel HD Graphics 4000,DirectX:,Version 10,Storage:,1500 MB available space","Recommended:,OS:,Windows 10,Processor:,3.2 GHz,Memory:,8 GB RAM,Graphics:,NVIDIA GTX 960,DirectX:,Version 11,Network:,Broadband Internet connection,Storage:,2 GB available space",Free To Play, diff --git a/datajunction-query/docker/cockroachdb/steam-hours-played.csv b/datajunction-query/docker/cockroachdb/steam-hours-played.csv new file mode 100644 index 000000000..ea3a9ff01 --- /dev/null +++ b/datajunction-query/docker/cockroachdb/steam-hours-played.csv @@ -0,0 +1,50 @@ +151603712,"DOOM",purchase,1.0,0 +151603712,"DOOM",play,273.0,0 +151603712,"PLAYERUNKNOWN'S BATTLEGROUNDS",purchase,1.0,0 +151603712,"Grand Theft Auto V: Premium Online Edition",play,87.0,0 +151603712,"Devil May Cry 5",purchase,1.0,0 +151603712,"Devil May Cry 5",play,14.9,0 +151603712,"TERA",purchase,1.0,0 +151603712,"Call of Duty®: Modern Warfare® Remastered",play,12.1,0 +151603712,"Call of Duty®: Modern Warfare® Remastered",purchase,1.0,0 +151603712,"Clone Drone in the Danger Zone",play,8.9,0 +151603712,"Hearts of Iron IV: Mobilization Pack",purchase,1.0,0 +151603712,"DayZ",play,8.5,0 +151603712,"DayZ",purchase,1.0,0 +151603712,"Path of Exile",play,8.1,0 +151603712,"Warhammer: Chaosbane",purchase,1.0,0 +151603712,"Warhammer: Chaosbane",play,7.5,0 +151603712,"For The King",purchase,1.0,0 +151603712,"For The King",play,3.3,0 +151603712,"Danganronpa V3: Killing Harmony",purchase,1.0,0 +151603712,"Danganronpa V3: Killing Harmony",play,2.8,0 +151603712,"Human: Fall Flat",purchase,1.0,0 +151603712,"Human: Fall Flat",play,2.5,0 +151603712,"The Banner Saga",purchase,1.0,0 +151603712,"The Banner Saga",play,2.0,0 +151603712,"Call of Duty®: Modern Warfare® Remastered",purchase,1.0,0 +151603712,"Call of Duty®: Modern Warfare® Remastered",play,1.4,0 +151603712,"BioShock Infinite",purchase,1.0,0 +151603712,"BioShock Infinite",play,1.3,0 +151603712,"Call of Duty®: Modern Warfare® Remastered",purchase,1.0,0 +151603712,"Call of Duty®: Modern Warfare® Remastered",play,1.3,0 +151603712,"Call of Duty®: Modern Warfare® Remastered",purchase,1.0,0 +151603712,"Call of Duty®: Modern Warfare® Remastered",play,0.8,0 +151603712,"Grand Theft Auto IV",purchase,1.0,0 +151603712,"Grand Theft Auto IV",play,0.8,0 +151603712,"Grand Theft Auto IV",purchase,1.0,0 +151603712,"Grand Theft Auto IV",play,0.6,0 +151603712,"Grand Theft Auto V: Premium Online Edition",purchase,1.0,0 +151603712,"Grand Theft Auto V: Premium Online Edition",play,0.5,0 +151603712,"Grand Theft Auto V: Premium Online Edition",purchase,1.0,0 +151603712,"Grand Theft Auto V: Premium Online Edition",play,0.5,0 +151603712,"EVE Online",purchase,1.0,0 +151603712,"EVE Online",play,0.5,0 +151603712,"EVE Online",purchase,1.0,0 +151603712,"EVE Online",play,0.5,0 +151603712,"Stonehearth",purchase,1.0,0 +151603712,"Stonehearth",play,0.5,0 +151603712,"Hearts of Iron IV: Mobilization Pack",purchase,1.0,0 +151603712,"Hearts of Iron IV: Mobilization Pack",play,0.4,0 +151603712,"Hearts of Iron IV: Mobilization Pack",purchase,1.0,0 +151603712,"Hearts of Iron IV: Mobilization Pack",play,0.1,0 diff --git a/datajunction-query/docker/default.duckdb b/datajunction-query/docker/default.duckdb new file mode 100644 index 000000000..560fc4f37 Binary files /dev/null and b/datajunction-query/docker/default.duckdb differ diff --git a/examples/docker/environment b/datajunction-query/docker/druid_environment similarity index 91% rename from examples/docker/environment rename to datajunction-query/docker/druid_environment index 20eceb0b8..c8f7753b3 100644 --- a/examples/docker/environment +++ b/datajunction-query/docker/druid_environment @@ -9,11 +9,11 @@ druid_emitter_logging_logLevel=debug druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage"] -druid_zk_service_host=zookeeper +druid_zk_service_host=druid_zookeeper druid_metadata_storage_host= druid_metadata_storage_type=postgresql -druid_metadata_storage_connector_connectURI=jdbc:postgresql://postgres:5432/druid +druid_metadata_storage_connector_connectURI=jdbc:postgresql://druid_postgres:5432/druid druid_metadata_storage_connector_user=druid druid_metadata_storage_connector_password=FoolishPassword diff --git a/examples/docker/druid_init.sh b/datajunction-query/docker/druid_init.sh similarity index 100% rename from examples/docker/druid_init.sh rename to datajunction-query/docker/druid_init.sh diff --git a/examples/docker/druid_spec.json b/datajunction-query/docker/druid_spec.json similarity index 86% rename from examples/docker/druid_spec.json rename to datajunction-query/docker/druid_spec.json index 634481ad4..c71e75969 100644 --- a/examples/docker/druid_spec.json +++ b/datajunction-query/docker/druid_spec.json @@ -24,7 +24,7 @@ "type": "uniform", "queryGranularity": "HOUR", "rollup": true, - "segmentGranularity": "DAY" + "segmentGranularity": "day" }, "timestampSpec": { "column": "timestamp", @@ -32,6 +32,8 @@ }, "dimensionsSpec": { "dimensions": [ + "id", + "user_id", "text" ] }, @@ -39,16 +41,6 @@ { "name": "count", "type": "count" - }, - { - "name": "sum_id", - "type": "longSum", - "fieldName": "id" - }, - { - "name": "sum_user_id", - "type": "longSum", - "fieldName": "user_id" } ] } diff --git a/datajunction-query/docker/duckdb.sql b/datajunction-query/docker/duckdb.sql new file mode 100644 index 000000000..eee52213f --- /dev/null +++ b/datajunction-query/docker/duckdb.sql @@ -0,0 +1,584 @@ +CREATE SCHEMA roads; + +CREATE TABLE roads.repair_type ( + repair_type_id int, + repair_type_name string, + contractor_id string +); +INSERT INTO roads.repair_type VALUES +(1, 'Asphalt Overlay', 'Asphalt overlays restore roads to a smooth condition. This resurfacing uses the deteriorating asphalt as a base for which the new layer is added on top of, instead of tearing up the worsening one.'), +(2, 'Patching', 'Patching is the process of filling potholes or excavated areas in the asphalt pavement. Quick repair of potholes or other pavement disintegration helps control further deterioration and expensive repair of the pavement. Without timely patching, water can enter the sub-grade and cause larger and more serious pavement failures.'), +(3, 'Reshaping', 'This is necessary when a road surface it too damaged to be smoothed. Using a grader blade and scarifying if necessary, you rework the gravel sub-base to eliminate large potholes and rebuild a flattened crown.'), +(4, 'Slab Replacement', 'This refers to replacing sections of paved roads. It is a good option for when slabs are chipped, cracked, or uneven, and mitigates the need to replace the entire road when just a small section is damaged.'), +(5, 'Smoothing', 'This is when you lightly rework the gravel of a road without digging in too far to the sub-base. Typically, a motor grader is used in this operation with an attached blade. Smoothing is done when the road has minor damage or is just worn down a bit from use.'), +(6, 'Reconstruction', 'When roads have deteriorated to a point that it is no longer cost-effective to maintain, the entire street or road needs to be rebuilt. Typically, this work is done in phases to limit traffic restrictions. As part of reconstruction, the street may be realigned to improve safety or operations, grading may be changed to improve storm water flow, underground utilities may be added, upgraded or relocated, traffic signals and street lights may be relocated, and street trees and pedestrian ramps may be added.'); + +CREATE TABLE roads.municipality ( + municipality_id string, + contact_name string, + contact_title string, + local_region string, + state_id int, + phone string +); +INSERT INTO roads.municipality VALUES +('New York', 'Alexander Wilkinson', 'Assistant City Clerk', 'Manhattan', 33, '202-291-2922'), +('Los Angeles', 'Hugh Moser', 'Administrative Assistant', 'Santa Monica', 5, '808-211-2323'), +('Chicago', 'Phillip Bradshaw', 'Director of Community Engagement', 'West Ridge', 14, '425-132-3421'), +('Houston', 'Leo Ackerman', 'Municipal Roads Specialist', 'The Woodlands', 44, '413-435-8641'), +('Phoenix', 'Jessie Paul', 'Director of Finance and Administration', 'Old Town Scottsdale', 3, '321-425-5427'), +('Philadelphia', 'Willie Chaney', 'Municipal Manager', 'Center City', 39, '212-213-5361'), +('San Antonio', 'Chester Lyon', 'Treasurer', 'Alamo Heights', 44, '252-216-6938'), +('San Diego', 'Ralph Helms', 'Senior Electrical Project Manager', 'Del Mar', 5, '491-813-2417'), +('Dallas', 'Virgil Craft', 'Assistant Assessor (Town/Municipality)', 'Deep Ellum', 44, '414-563-7894'), +('San Jose', 'Charles Carney', 'Municipal Accounting Manager', 'Santana Row', 5, '408-313-0698'); + +CREATE TABLE roads.hard_hats ( + hard_hat_id int, + last_name string, + first_name string, + title string, + birth_date date, + hire_date date, + address string, + city string, + state string, + postal_code string, + country string, + manager int, + contractor_id int +); +INSERT INTO roads.hard_hats VALUES +(1, 'Brian', 'Perkins', 'Construction Laborer', cast('1978-11-28' as date), cast('2009-02-06' as date), '4 Jennings Ave.', 'Jersey City', 'NJ', '37421', 'USA', 9, 1), +(2, 'Nicholas', 'Massey', 'Carpenter', cast('1993-02-19' as date), cast('2003-04-14' as date), '9373 Southampton Street', 'Middletown', 'CT', '27292', 'USA', 9, 1), +(3, 'Cathy', 'Best', 'Framer', cast('1994-08-30' as date), cast('1990-07-02' as date), '4 Hillside Street', 'Billerica', 'MA', '13440', 'USA', 9, 2), +(4, 'Melanie', 'Stafford', 'Construction Manager', cast('1966-03-19' as date), cast('2003-02-02' as date), '77 Studebaker Lane', 'Southampton', 'PA', '71730', 'USA', 9, 2), +(5, 'Donna', 'Riley', 'Pre-construction Manager', cast('1983-03-14' as date), cast('2012-01-13' as date), '82 Taylor Drive', 'Southgate', 'MI', '33125', 'USA', 9, 4), +(6, 'Alfred', 'Clarke', 'Construction Superintendent', cast('1979-01-12' as date), cast('2013-10-17' as date), '7729 Catherine Street', 'Powder Springs', 'GA', '42001', 'USA', 9, 2), +(7, 'William', 'Boone', 'Construction Laborer', cast('1970-02-28' as date), cast('2013-01-02' as date), '1 Border St.', 'Niagara Falls', 'NY', '14304', 'USA', 9, 4), +(8, 'Luka', 'Henderson', 'Construction Laborer', cast('1988-12-09' as date), cast('2013-03-05' as date), '794 S. Chapel Ave.', 'Phoenix', 'AZ', '85021', 'USA', 9, 1), +(9, 'Patrick', 'Ziegler', 'Construction Laborer', cast('1976-11-27' as date), cast('2020-11-15' as date), '321 Gainsway Circle', 'Muskogee', 'OK', '74403', 'USA', 9, 3); + +CREATE TABLE roads.hard_hat_state ( + hard_hat_id int, + state_id int +); +INSERT INTO roads.hard_hat_state VALUES +(1, 2), +(2, 32), +(3, 28), +(4, 12), +(5, 5), +(6, 3), +(7, 16), +(8, 32), +(9, 41); + +CREATE TABLE roads.repair_order_details ( + repair_order_id int, + repair_type_id int, + price real NOT NULL, + quantity int, + discount real NOT NULL +); +INSERT INTO roads.repair_order_details VALUES +(10001, 1, 63708, 1, 0.05), +(10002, 4, 67253, 1, 0.05), +(10003, 2, 66808, 1, 0.05), +(10004, 4, 18497, 1, 0.05), +(10005, 7, 76463, 1, 0.05), +(10006, 4, 87858, 1, 0.05), +(10007, 1, 63918, 1, 0.05), +(10008, 6, 21083, 1, 0.05), +(10009, 3, 74555, 1, 0.05), +(10010, 5, 27222, 1, 0.05), +(10011, 5, 73600, 1, 0.05), +(10012, 3, 54901, 1, 0.01), +(10013, 5, 51594, 1, 0.01), +(10014, 1, 65114, 1, 0.01), +(10015, 1, 48919, 1, 0.01), +(10016, 3, 70418, 1, 0.01), +(10017, 1, 29684, 1, 0.01), +(10018, 2, 62928, 1, 0.01), +(10019, 2, 97916, 1, 0.01), +(10020, 5, 44120, 1, 0.01), +(10021, 1, 53374, 1, 0.01), +(10022, 2, 87289, 1, 0.01), +(10023, 2, 92366, 1, 0.01), +(10024, 2, 47857, 1, 0.01), +(10025, 1, 68745, 1, 0.01); + +CREATE TABLE roads.repair_orders ( + repair_order_id int, + municipality_id string, + hard_hat_id int, + order_date date, + required_date date, + dispatched_date date, + dispatcher_id int +); +INSERT INTO roads.repair_orders VALUES +(10001, 'New York', 1, cast('2007-07-04' as date), cast('2009-07-18' as date), cast('2007-12-01' as date), 3), +(10002, 'New York', 3, cast('2007-07-05' as date), cast('2009-08-28' as date), cast('2007-12-01' as date), 1), +(10003, 'New York', 5, cast('2007-07-08' as date), cast('2009-08-12' as date), cast('2007-12-01' as date), 2), +(10004, 'Dallas', 1, cast('2007-07-08' as date), cast('2009-08-01' as date), cast('2007-12-01' as date), 1), +(10005, 'San Antonio', 8, cast('2007-07-09' as date), cast('2009-08-01' as date), cast('2007-12-01' as date), 2), +(10006, 'New York', 3, cast('2007-07-10' as date), cast('2009-08-01' as date), cast('2007-12-01' as date), 2), +(10007, 'Philadelphia', 4, cast('2007-04-21' as date), cast('2009-08-08' as date), cast('2007-12-01' as date), 2), +(10008, 'Philadelphia', 5, cast('2007-04-22' as date), cast('2009-08-09' as date), cast('2007-12-01' as date), 3), +(10009, 'Philadelphia', 3, cast('2007-04-25' as date), cast('2009-08-12' as date), cast('2007-12-01' as date), 2), +(10010, 'Philadelphia', 4, cast('2007-04-26' as date), cast('2009-08-13' as date), cast('2007-12-01' as date), 3), +(10011, 'Philadelphia', 4, cast('2007-04-27' as date), cast('2009-08-14' as date), cast('2007-12-01' as date), 1), +(10012, 'Philadelphia', 8, cast('2007-04-28' as date), cast('2009-08-15' as date), cast('2007-12-01' as date), 3), +(10013, 'Philadelphia', 4, cast('2007-04-29' as date), cast('2009-08-16' as date), cast('2007-12-01' as date), 1), +(10014, 'Philadelphia', 6, cast('2007-04-29' as date), cast('2009-08-16' as date), cast('2007-12-01' as date), 2), +(10015, 'Philadelphia', 2, cast('2007-04-12' as date), cast('2009-08-19' as date), cast('2007-12-01' as date), 3), +(10016, 'Philadelphia', 9, cast('2007-04-13' as date), cast('2009-08-20' as date), cast('2007-12-01' as date), 3), +(10017, 'Philadelphia', 2, cast('2007-04-14' as date), cast('2009-08-21' as date), cast('2007-12-01' as date), 3), +(10018, 'Philadelphia', 6, cast('2007-04-15' as date), cast('2009-08-22' as date), cast('2007-12-01' as date), 1), +(10019, 'Philadelphia', 5, cast('2007-05-16' as date), cast('2009-09-06' as date), cast('2007-12-01' as date), 3), +(10020, 'Philadelphia', 1, cast('2007-05-19' as date), cast('2009-08-26' as date), cast('2007-12-01' as date), 1), +(10021, 'Philadelphia', 7, cast('2007-05-10' as date), cast('2009-08-27' as date), cast('2007-12-01' as date), 3), +(10022, 'Philadelphia', 5, cast('2007-05-11' as date), cast('2009-08-14' as date), cast('2007-12-01' as date), 1), +(10023, 'Philadelphia', 1, cast('2007-05-11' as date), cast('2009-08-29' as date), cast('2007-12-01' as date), 1), +(10024, 'Philadelphia', 5, cast('2007-05-11' as date), cast('2009-08-29' as date), cast('2007-12-01' as date), 2), +(10025, 'Philadelphia', 6, cast('2007-05-12' as date), cast('2009-08-30' as date), cast('2007-12-01' as date), 2); + +CREATE TABLE roads.dispatchers ( + dispatcher_id int, + company_name string, + phone string +); +INSERT INTO roads.dispatchers VALUES +(1, 'Pothole Pete', '(111) 111-1111'), +(2, 'Asphalts R Us', '(222) 222-2222'), +(3, 'Federal Roads Group', '(333) 333-3333'), +(4, 'Local Patchers', '1-800-888-8888'), +(5, 'Gravel INC', '1-800-000-0000'), +(6, 'DJ Developers', '1-111-111-1111'); + +CREATE TABLE roads.contractors ( + contractor_id int, + company_name string, + contact_name string, + contact_title string, + address string, + city string, + state string, + postal_code string, + country string, + phone string +); +INSERT INTO roads.contractors VALUES +(1, 'You Need Em We Find Em', 'Max Potter', 'Assistant Director', '4 Plumb Branch Lane', 'Goshen', 'IN', '46526', 'USA', '(111) 111-1111'), +(2, 'Call Forwarding', 'Sylvester English', 'Administrator', '9650 Mill Lane', 'Raeford', 'NC', '28376', 'USA', '(222) 222-2222'), +(3, 'The Connect', 'Paul Raymond', 'Administrator', '7587 Myrtle Ave.', 'Chaska', 'MN', '55318', 'USA', '(333) 333-3333'); + +CREATE TABLE roads.us_region ( + us_region_id int, + us_region_description string +); +INSERT INTO roads.us_region VALUES +(1, 'Eastern'), +(2, 'Western'), +(3, 'Northern'), +(4, 'Southern'); + +CREATE TABLE roads.us_states ( + state_id int, + state_name string, + state_abbr string, + state_region string +); +INSERT INTO roads.us_states VALUES +(1, 'Alabama', 'AL', 'Southern'), +(2, 'Alaska', 'AK', 'Northern'), +(3, 'Arizona', 'AZ', 'Western'), +(4, 'Arkansas', 'AR', 'Southern'), +(5, 'California', 'CA', 'Western'), +(6, 'Colorado', 'CO', 'Western'), +(7, 'Connecticut', 'CT', 'Eastern'), +(8, 'Delaware', 'DE', 'Eastern'), +(9, 'District of Columbia', 'DC', 'Eastern'), +(10, 'Florida', 'FL', 'Southern'), +(11, 'Georgia', 'GA', 'Southern'), +(12, 'Hawaii', 'HI', 'Western'), +(13, 'Idaho', 'ID', 'Western'), +(14, 'Illinois', 'IL', 'Western'), +(15, 'Indiana', 'IN', 'Western'), +(16, 'Iowa', 'IO', 'Western'), +(17, 'Kansas', 'KS', 'Western'), +(18, 'Kentucky', 'KY', 'Southern'), +(19, 'Louisiana', 'LA', 'Southern'), +(20, 'Maine', 'ME', 'Northern'), +(21, 'Maryland', 'MD', 'Eastern'), +(22, 'Massachusetts', 'MA', 'Northern'), +(23, 'Michigan', 'MI', 'Northern'), +(24, 'Minnesota', 'MN', 'Northern'), +(25, 'Mississippi', 'MS', 'Southern'), +(26, 'Missouri', 'MO', 'Southern'), +(27, 'Montana', 'MT', 'Western'), +(28, 'Nebraska', 'NE', 'Western'), +(29, 'Nevada', 'NV', 'Western'), +(30, 'New Hampshire', 'NH', 'Eastern'), +(31, 'New Jersey', 'NJ', 'Eastern'), +(32, 'New Mexico', 'NM', 'Western'), +(33, 'New York', 'NY', 'Eastern'), +(34, 'North Carolina', 'NC', 'Eastern'), +(35, 'North Dakota', 'ND', 'Western'), +(36, 'Ohio', 'OH', 'Western'), +(37, 'Oklahoma', 'OK', 'Western'), +(38, 'Oregon', 'OR', 'Western'), +(39, 'Pennsylvania', 'PA', 'Eastern'), +(40, 'Rhode Island', 'RI', 'Eastern'), +(41, 'South Carolina', 'SC', 'Eastern'), +(42, 'South Dakota', 'SD', 'Western'), +(43, 'Tennessee', 'TN', 'Western'), +(44, 'Texas', 'TX', 'Western'), +(45, 'Utah', 'UT', 'Western'), +(46, 'Vermont', 'VT', 'Eastern'), +(47, 'Virginia', 'VA', 'Eastern'), +(48, 'Washington', 'WA', 'Western'), +(49, 'West Virginia', 'WV', 'Southern'), +(50, 'Wisconsin', 'WI', 'Western'), +(51, 'Wyoming', 'WY', 'Western'); + +CREATE TABLE roads.municipality_municipality_type ( + municipality_id string, + municipality_type_id string +); +INSERT INTO roads.municipality_municipality_type VALUES +('New York', 'A'), +('Los Angeles', 'B'), +('Chicago', 'B'), +('Houston', 'A'), +('Phoenix', 'A'), +('Philadelphia', 'B'), +('San Antonio', 'A'), +('San Diego', 'B'), +('Dallas', 'A'), +('San Jose', 'B'); + +CREATE TABLE roads.municipality_type ( + municipality_type_id string, + municipality_type_desc string +); +INSERT INTO roads.municipality_type VALUES +('A', 'Primary'), +('A', 'Secondary'); + +CREATE SCHEMA campaigns; + +CREATE TABLE campaigns.campaign_views ( + campaign_id int, + campaign_name string, + created_at timestamp, + views int +); + +INSERT INTO campaigns.campaign_views VALUES +(1, 'Clean Up Your Yard Marketing', epoch_ms(1526290800000), 120), +(2, 'Summer Sale Campaign', epoch_ms(1456472400000), 500), +(3, 'New Product Launch', epoch_ms(1341392400000), 250), +(4, 'Holiday Special Offers', epoch_ms(1451600400000), 800), +(5, 'Spring Cleaning Deals', epoch_ms(1472667600000), 300), +(6, 'Back to School Promotion', epoch_ms(1293848400000), 150), +(7, 'Winter Clearance Sale', epoch_ms(1512123600000), 900), +(8, 'Outdoor Adventure Campaign', epoch_ms(1288779600000), 400), +(9, 'Health and Wellness Expo', epoch_ms(1335795600000), 200), +(10, 'Summer Vacation Deals', epoch_ms(1462069200000), 600), +(11, 'Home Renovation Offers', epoch_ms(1396314000000), 350), +(12, 'Fashion Show Sponsorship', epoch_ms(1248478800000), 750), +(13, 'Charity Fundraising Drive', epoch_ms(1363563600000), 420), +(14, 'Tech Gadgets Showcase', epoch_ms(1433106000000), 230), +(15, 'Gourmet Food Festival', epoch_ms(1308032400000), 150), +(16, 'Music Concert Ticket Sales', epoch_ms(1380584400000), 550), +(17, 'Book Fair and Author Meet', epoch_ms(1262274000000), 180), +(18, 'Fitness Challenge Event', epoch_ms(1502053200000), 670), +(19, 'Pet Adoption Awareness', epoch_ms(1319638800000), 290), +(20, 'Art Exhibition Opening', epoch_ms(1409422800000), 390), +(21, 'Wedding Planning Expo', epoch_ms(1274245200000), 420), +(22, 'Sports Equipment Sale', epoch_ms(1419987600000), 780), +(23, 'Tech Startup Conference', epoch_ms(1370302800000), 210), +(24, 'Environmental Awareness', epoch_ms(1328053200000), 840), +(25, 'Travel and Adventure Expo', epoch_ms(1366832400000), 350), +(26, 'Automobile Showroom Launch', epoch_ms(1425162000000), 420), +(27, 'Film Festival Promotion', epoch_ms(1356997200000), 590), +(28, 'Summer Camp Enrollment', epoch_ms(1388778000000), 240), +(29, 'Online Shopping Festival', epoch_ms(1251776400000), 430), +(30, 'Healthcare Symposium', epoch_ms(1443642000000), 980); + +CREATE TABLE campaigns.email ( + campaign_id int, + email_address string, + email_id int, + last_contacted timestamp, + views int +); + +INSERT INTO campaigns.email VALUES +(1, 'mark@fakedomain.com', 1, to_timestamp(1451606400000), 2), +(1, 'john@fakedomain.com', 2, to_timestamp(1454284800000), 7), +(1, 'isaiah@fakedomain.com', 3, to_timestamp(1456790400000), 10), +(1, 'ruby@fakedomain.com', 4, to_timestamp(1459468800000), 5), +(1, 'oliver@fakedomain.com', 5, to_timestamp(1462060800000), 9), +(1, 'emma@fakedomain.com', 6, to_timestamp(1464739200000), 12), +(1, 'liam@fakedomain.com', 7, to_timestamp(1467331200000), 4), +(1, 'ava@fakedomain.com', 8, to_timestamp(1470009600000), 11), +(1, 'noah@fakedomain.com', 9, to_timestamp(1472688000000), 7), +(1, 'isabella@fakedomain.com', 10, to_timestamp(1475280000000), 3), +(2, 'sophia@fakedomain.com', 1, to_timestamp(1477958400000), 8), +(2, 'mason@fakedomain.com', 2, to_timestamp(1480550400000), 6), +(2, 'camila@fakedomain.com', 3, to_timestamp(1483228800000), 11), +(2, 'henry@fakedomain.com', 4, to_timestamp(1485907200000), 13), +(2, 'mia@fakedomain.com', 5, to_timestamp(1488326400000), 6), +(2, 'ethan@fakedomain.com', 6, to_timestamp(1491004800000), 9), +(2, 'lucas@fakedomain.com', 7, to_timestamp(1493596800000), 5), +(2, 'harper@fakedomain.com', 8, to_timestamp(1496275200000), 14), +(2, 'alexander@fakedomain.com', 9, to_timestamp(1498867200000), 12), +(2, 'abigail@fakedomain.com', 10, to_timestamp(1501545600000), 2), +(3, 'james@fakedomain.com', 1, to_timestamp(1504224000000), 7), +(3, 'amelia@fakedomain.com', 2, to_timestamp(1506816000000), 4), +(3, 'benjamin@fakedomain.com', 3, to_timestamp(1509494400000), 10), +(3, 'evelyn@fakedomain.com', 4, to_timestamp(1512086400000), 6), +(3, 'michael@fakedomain.com', 5, to_timestamp(1514764800000), 3), +(3, 'charlotte@fakedomain.com', 6, to_timestamp(1517443200000), 8), +(3, 'daniel@fakedomain.com', 7, to_timestamp(1520035200000), 12), +(3, 'harper@fakedomain.com', 8, to_timestamp(1522713600000), 9), +(3, 'lucas@fakedomain.com', 9, to_timestamp(1525305600000), 5), +(3, 'isabella@fakedomain.com', 10, to_timestamp(1527984000000), 11); + +CREATE TABLE campaigns.sms ( + campaign_id int, + phone_number string, + message string, + last_contacted timestamp, +); + +INSERT INTO campaigns.sms VALUES +(11, '(215) 111-1111', 'Click here to redeem 20% off!: http://smalllink', epoch_ms(1459468800000)), +(12, '(215) 222-2222', 'Interested in new lawn equipment?: http://smalllink', epoch_ms(1459468800001)), +(12, '(215) 333-3333', 'These sales will not last!: http://smalllink', epoch_ms(1459468800002)), +(13, '(215) 444-4444', 'Fall is here, enjoy half-off!: http://smalllink', epoch_ms(1459468800003)), +(14, '(215) 555-5555', 'Get the best deals today!: http://smalllink', epoch_ms(1459468800004)), +(15, '(215) 666-6666', 'Limited time offer - 30% off!: http://smalllink', epoch_ms(1459468800005)), +(16, '(215) 777-7777', 'Do not miss out on our sale!: http://smalllink', epoch_ms(1459468800006)), +(17, '(215) 888-8888', 'Exclusive discount for you!: http://smalllink', epoch_ms(1459468800007)), +(18, '(215) 999-9999', 'Shop now and save big!: http://smalllink', epoch_ms(1459468800008)), +(19, '(215) 000-0000', 'Huge clearance sale - up to 50% off!: http://smalllink', epoch_ms(1459468800009)), +(10, '(215) 123-4567', 'New arrivals with special discounts!: http://smalllink', epoch_ms(1459468800010)), +(11, '(215) 234-5678', 'Save money with our promo codes!: http://smalllink', epoch_ms(1459468800011)), +(12, '(215) 345-6789', 'Enjoy free shipping on all orders!: http://smalllink', epoch_ms(1459468800012)), +(13, '(215) 456-7890', 'Limited stock available - act fast!: http://smalllink', epoch_ms(1459468800013)), +(14, '(215) 567-8901', 'Upgrade your home with our deals!: http://smalllink', epoch_ms(1459468800014)), +(15, '(215) 678-9012', 'Special discount for loyal customers!: http://smalllink', epoch_ms(1459468800015)), +(16, '(215) 789-0123', 'Do not miss our summer sale!: http://smalllink', epoch_ms(1459468800016)), +(17, '(215) 890-1234', 'Exclusive offer - limited time only!: http://smalllink', epoch_ms(1459468800017)), +(18, '(215) 901-2345', 'Shop now and get a free gift!: http://smalllink', epoch_ms(1459468800018)), +(19, '(215) 012-3456', 'Big discounts on popular brands!: http://smalllink', epoch_ms(1459468800019)), +(10, '(215) 987-6543', 'Save up to 70% off on selected items!: http://smalllink', epoch_ms(1459468800020)), +(11, '(215) 876-5432', 'Limited time offer - buy one, get one free!: http://smalllink', epoch_ms(1459468800021)), +(12, '(215) 765-4321', 'Great deals for your next vacation!: http://smalllink', epoch_ms(1459468800022)), +(13, '(215) 654-3210', 'Get ready for the holiday season with our discounts!: http://smalllink', epoch_ms(1459468800023)), +(14, '(215) 543-2109', 'Save on electronics and gadgets!: http://smalllink', epoch_ms(1459468800024)), +(15, '(215) 432-1098', 'Limited stock available - you do not want to miss out!: http://smalllink', epoch_ms(1459468800025)), +(16, '(215) 321-0987', 'Shop now and enjoy free returns!: http://smalllink', epoch_ms(1459468800026)), +(17, '(215) 210-9876', 'Exclusive discount for online orders!: http://smalllink', epoch_ms(1459468800027)), +(18, '(215) 109-8765', 'Upgrade your wardrobe with our sale!: http://smalllink', epoch_ms(1459468800028)), +(19, '(215) 098-7654', 'Will you miss our clearance sale??: http://smalllink', epoch_ms(1459468800029)), +(10, '(215) 987-6543', 'Get the best deals for your home!: http://smalllink', epoch_ms(1459468800030)), +(12, '(215) 222-2222', 'New collection now available!: http://smalllink', epoch_ms(1459468800002)), +(13, '(215) 333-3333', 'Limited stock - dont miss out!: http://smalllink', epoch_ms(1459468800003)), +(14, '(215) 444-4444', 'Free shipping on all orders!: http://smalllink', epoch_ms(1459468800004)), +(15, '(215) 555-5555', 'Sign up for exclusive offers!: http://smalllink', epoch_ms(1459468800005)), +(16, '(215) 666-6666', 'Get ready for summer with our new arrivals!: http://smalllink', epoch_ms(1459468800006)), +(17, '(215) 777-7777', 'Limited time sale - up to 60% off!: http://smalllink', epoch_ms(1459468800007)), +(10, '(215) 111-2222', 'Shop now and receive a gift card!: http://smalllink', epoch_ms(1459468800010)), +(11, '(215) 222-3333', 'Final clearance - last chance to save!: http://smalllink', epoch_ms(1459468800011)), +(12, '(215) 333-4444', 'Get the latest fashion trends at discounted prices!: http://smalllink', epoch_ms(1459468800012)), +(13, '(215) 444-5555', 'Upgrade your electronics with our special offers!: http://smalllink', epoch_ms(1459468800013)), +(14, '(215) 555-6666', 'Big savings on home appliances!: http://smalllink', epoch_ms(1459468800014)), +(15, '(215) 666-7777', 'Limited stock - shop now before its gone!: http://smalllink', epoch_ms(1459468800015)), +(16, '(215) 777-8888', 'Get the best deals on beauty products!: http://smalllink', epoch_ms(1459468800016)), +(11, '(215) 012-3456', 'Dont miss our summer sale!: http://smalllink', epoch_ms(1459468800021)), +(12, '(215) 123-4567', 'Exclusive offer for our loyal customers!: http://smalllink', epoch_ms(1459468800022)), +(13, '(215) 234-5678', 'Shop now and enjoy free returns!: http://smalllink', epoch_ms(1459468800023)), +(14, '(215) 345-6789', 'Upgrade your gaming setup with our deals!: http://smalllink', epoch_ms(1459468800024)), +(15, '(215) 456-7890', 'Save on outdoor essentials for your next adventure!: http://smalllink', epoch_ms(1459468800025)), +(16, '(215) 567-8901', 'Limited time offer - buy one, get one free!: http://smalllink', epoch_ms(1459468800026)), +(19, '(215) 098-7654', 'Discover the latest tech gadgets at discounted prices!: http://smalllink', epoch_ms(1459468800029)), +(12, '(215) 876-5432', 'Get the perfect gift for your loved ones!: http://smalllink', epoch_ms(1459468800031)); + +CREATE TABLE campaigns.commercial ( + campaign_id int, + station string, + last_played timestamp, +); + +INSERT INTO campaigns.commercial VALUES +(23, 'Montgomery Oldies Station', epoch_ms(1688879124599)), +(21, 'Lithonia Jazz & Blues', epoch_ms(1688879124599)), +(20, 'Manchester 90s R&B', epoch_ms(1688879124599)), +(22, 'Los Angeles Rock Hits', epoch_ms(1688879124599)), +(23, 'Chicago Pop Mix', epoch_ms(1688879124599)), +(24, 'Houston Country Station', epoch_ms(1688879124599)), +(25, 'New York Hip Hop', epoch_ms(1688879124599)), +(26, 'Seattle Alternative Rock', epoch_ms(1688879124599)), +(27, 'Miami Latin Beats', epoch_ms(1688879124599)), +(28, 'Denver Indie Folk', epoch_ms(1688879124599)), +(29, 'Boston Classical', epoch_ms(1688879124599)), +(20, 'San Francisco Electronic', epoch_ms(1688879124599)), +(21, 'Philadelphia R&B Hits', epoch_ms(1688879124599)), +(22, 'Dallas Pop Punk', epoch_ms(1688879124599)), +(23, 'Atlanta Gospel', epoch_ms(1688879124599)), +(24, 'Las Vegas Hard Rock', epoch_ms(1688879124599)), +(25, 'Phoenix Country Hits', epoch_ms(1688879124599)), +(26, 'Portland Alternative', epoch_ms(1688879124599)), +(27, 'Austin Indie Rock', epoch_ms(1688879124599)), +(28, 'Nashville Country Classics', epoch_ms(1688879124599)), +(29, 'San Diego Surf Rock', epoch_ms(1688879124599)), +(20, 'Minneapolis Folk', epoch_ms(1688879124599)), +(21, 'Detroit Motown', epoch_ms(1688879124599)), +(22, 'Baltimore Jazz Lounge', epoch_ms(1688879124599)), +(23, 'Kansas City Blues', epoch_ms(1688879124599)), +(24, 'St. Louis Smooth Jazz', epoch_ms(1688879124599)), +(25, 'Cleveland Classic Rock', epoch_ms(1688879124599)), +(26, 'Pittsburgh Metal', epoch_ms(1688879124599)), +(27, 'Charlotte Pop Hits', epoch_ms(1688879124599)), +(28, 'Raleigh-Durham Indie Pop', epoch_ms(1688879124599)), +(29, 'Tampa Bay Reggae', epoch_ms(1688879124599)); + +CREATE SCHEMA games; + +CREATE TABLE games.titles ( + game_id int, + game_name string, + num_distinct_players int, + release_date timestamp, + platform string, + genre string, + publisher_id int, + developer_id int, + average_rating float, + online_mode boolean, + total_sales int, + active_monthly int, + dlcs int +); + +INSERT INTO games.titles ( + game_id, + game_name, + num_distinct_players, + release_date, + platform, + genre, + publisher_id, + developer_id, + average_rating, + online_mode, + total_sales, + active_monthly, + dlcs +) +VALUES + (1, 'Battle of the Chinchillas: Furry Fury', 1000, '2022-01-01 00:00:00', 'PS5', 'Action', 1, 928, 4.5, true, 1000000, 5000, 3), + (2, 'Grand Theft Toaster: Carb City Chronicles', 500, '2021-06-15 00:00:00', 'Xbox Series X', 'RPG', 1, 948, 4.2, false, 750000, 2500, 2), + (3, 'Super Slime Soccer: Gooey Goalkeepers', 2000, '2020-11-30 00:00:00', 'Nintendo Switch', 'Sports', 2, 937, 4.8, true, 500000, 3000, 4), + (4, 'Dance Dance Avocado: Guacamole Groove', 1500, '2022-08-20 00:00:00', 'PC', 'Rhythm', 22, 987, 4.6, true, 250000, 2000, 1), + (5, 'Zombie Zookeeper: Undead Menagerie', 800, '2023-02-10 00:00:00', 'PS4', 'Strategy', 13, 902, 4.4, false, 300000, 1500, 3), + (6, 'Squirrel Simulator: Nutty Adventure', 3000, '2021-03-05 00:00:00', 'Xbox One', 'Simulation', 15, 928, 4.2, true, 400000, 2500, 2), + (7, 'Crash Test Dummies: Wacky Collision', 1200, '2022-05-15 00:00:00', 'PC', 'Action', 12, 928, 4.7, false, 150000, 1000, 1), + (8, 'Pizza Delivery Panic: Cheesy Chaos', 1000, '2023-01-30 00:00:00', 'Nintendo Switch', 'Arcade', 8, 928, 4.3, true, 200000, 1800, 2), + (9, 'Alien Abduction Academy: Extraterrestrial Education', 2500, '2020-09-05 00:00:00', 'PS5', 'Adventure', 7, 987, 4.5, false, 800000, 3500, 3), + (10, 'Crazy Cat Circus: Meow Mayhem', 700, '2022-07-25 00:00:00', 'Xbox Series X', 'Puzzle', 18, 987, 4.1, true, 100000, 1200, 1), + (11, 'Robot Rampage: Mechanical Mayhem', 1800, '2021-04-12 00:00:00', 'PC', 'Action', 4, 987, 4.6, true, 450000, 2200, 2), + (12, 'Super Spy Squirrels: Nutty Espionage', 900, '2023-03-18 00:00:00', 'PS4', 'Stealth', 10, 934, 4.4, false, 400000, 1800, 3), + (13, 'Banana Blaster: Fruit Frenzy', 2800, '2021-02-08 00:00:00', 'Xbox One', 'Shooter', 10, 934, 4.3, true, 700000, 3000, 2), + (14, 'Penguin Paradise: Antarctic Adventure', 1100, '2022-04-05 00:00:00', 'PC', 'Simulation', 20, 902, 4.7, false, 200000, 1200, 1), + (15, 'Unicorn Universe: Rainbow Realm', 500, '2023-01-15 00:00:00', 'Nintendo Switch', 'Adventure', 1, 902, 4.2, true, 150000, 900, 2), + (16, 'Spaghetti Showdown: Saucy Shootout', 2100, '2020-10-20 00:00:00', 'PS5', 'Action', 4, 902, 4.4, true, 550000, 2500, 3), + (17, 'Bubblegum Bandits: Sticky Heist', 800, '2022-06-10 00:00:00', 'Xbox Series X', 'Stealth', 3, 902, 4.1, false, 180000, 800, 1), + (18, 'Safari Slingshot: Wild Wildlife', 1300, '2021-03-01 00:00:00', 'PC', 'Arcade', 3, 987, 4.6, true, 300000, 1500, 2), + (19, 'Monster Mop: Cleaning Catastrophe', 600, '2022-12-20 00:00:00', 'Nintendo Switch', 'Puzzle', 17, 934, 4.3, false, 120000, 700, 1), + (20, 'Galactic Golf: Space Swing', 1900, '2020-08-15 00:00:00', 'PS4', 'Sports', 5, 921, 4.5, true, 400000, 2000, 3), + (21, 'Funky Farm Friends: Groovy Gardening', 900, '2023-02-05 00:00:00', 'Xbox One', 'Simulation', 21, 921, 4.3, true, 250000, 1300, 2), + (22, 'Cake Crusaders: Sugary Siege', 1400, '2021-01-25 00:00:00', 'PC', 'Strategy', 23, 902, 4.7, false, 180000, 900, 1), + (23, 'Ninja Narwhal: Aquatic Assassin', 1000, '2022-03-15 00:00:00', 'Nintendo Switch', 'Action', 23, 901, 4.2, true, 150000, 1000, 2); + +CREATE TABLE games.publishers ( + publisher_id int PRIMARY KEY, + publisher_name string +); + +INSERT INTO games.publishers (publisher_id, publisher_name) +VALUES + (1, 'Wacky Game Studios'), + (2, 'Silly Monkey Games'), + (3, 'Laughing Unicorn Interactive'), + (4, 'Crazy Cat Games'), + (5, 'Quirky Penguin Productions'), + (6, 'Absurd Antelope Studios'), + (7, 'Hilarious Hedgehog Games'), + (8, 'Goofy Giraffe Studios'), + (9, 'Whimsical Walrus Entertainment'), + (10, 'Zany Zebra Games'), + (11, 'Ridiculous Rabbit Studios'), + (12, 'Funny Fox Interactive'), + (13, 'Surreal Snake Games'), + (14, 'Bizarre Bat Studios'), + (15, 'Madcap Moose Productions'), + (16, 'Cuckoo Clock Games'), + (17, 'Lunatic Llama Studios'), + (18, 'Silly Goose Games'), + (19, 'Bonkers Beaver Interactive'), + (20, 'Witty Walrus Games'); + +CREATE TABLE games.developers ( + developer_id int, + developer_name string, + num_games_developed int +); + +INSERT INTO games.developers (developer_id, developer_name, num_games_developed) +VALUES + (928, 'CrazyCodr', 10), + (948, 'PixelPirate', 5), + (937, 'CodeNinja', 3), + (987, 'GameWizard', 8), + (902, 'ByteBender', 15), + (934, 'CodeJester', 4), + (902, 'MadGenius', 12), + (921, 'GameGuru', 9), + (901, 'ScriptMage', 6); + +CREATE SCHEMA accounting; +CREATE TABLE accounting.payment_type_table ( + id int, + payment_type_name string, + payment_type_classification string +); + +INSERT INTO accounting.payment_type_table (id, payment_type_name, payment_type_classification) +VALUES + (1, 'VISA', 'CARD'), + (2, 'MASTERCARD', 'CARD'); + + +CREATE TABLE accounting.revenue ( + payment_id int, + payment_amount float, + payment_type int, + customer_id int, + account_type string +); + +INSERT INTO accounting.revenue (payment_id, payment_amount, payment_type, customer_id, account_type) +VALUES + (1, 25.5, 1, 2, 'ACTIVE'), + (2, 12.5, 2, 2, 'INACTIVE'), + (3, 89, 1, 3, 'ACTIVE'), + (4, 1293.2, 2, 2, 'ACTIVE'), + (5, 23, 1, 4, 'INACTIVE'), + (6, 398.13, 2, 3, 'ACTIVE'), + (7, 239.7, 2, 4, 'ACTIVE'),; diff --git a/datajunction-query/docker/duckdb_load.py b/datajunction-query/docker/duckdb_load.py new file mode 100644 index 000000000..163eab834 --- /dev/null +++ b/datajunction-query/docker/duckdb_load.py @@ -0,0 +1,11 @@ +import duckdb + +con = duckdb.connect("default.duckdb") + +with open("duckdb.sql", "r") as f: + queries = f.read().split(";") + +for q in queries: + if q.strip(): + con.sql(q) + print("Query executed successfully") diff --git a/datajunction-query/docker/postgres_init.roads.sql b/datajunction-query/docker/postgres_init.roads.sql new file mode 100644 index 000000000..24a28a63f --- /dev/null +++ b/datajunction-query/docker/postgres_init.roads.sql @@ -0,0 +1,273 @@ +DROP DATABASE IF EXISTS djdb; +CREATE DATABASE djdb; + +\connect djdb; + +CREATE SCHEMA IF NOT EXISTS roads; + +DROP TABLE IF EXISTS roads.municipality_municipality_type; +DROP TABLE IF EXISTS roads.municipality_type; +DROP TABLE IF EXISTS roads.repair_order_details; +DROP TABLE IF EXISTS roads.repair_orders; +DROP TABLE IF EXISTS roads.municipality; +DROP TABLE IF EXISTS roads.repair_type; +DROP TABLE IF EXISTS roads.dispatchers; +DROP TABLE IF EXISTS roads.contractors; +DROP TABLE IF EXISTS roads.us_states; +DROP TABLE IF EXISTS roads.us_region; +DROP TABLE IF EXISTS roads.hard_hats; +DROP TABLE IF EXISTS roads.hard_hat_state; + +CREATE TABLE roads.repair_type ( + repair_type_id smallint NOT NULL, + repair_type_name character varying(50) NOT NULL, + contractor_id text +); +INSERT INTO roads.repair_type VALUES (1, 'Asphalt Overlay', 'Asphalt overlays restore roads to a smooth condition. This resurfacing uses the deteriorating asphalt as a base for which the new layer is added on top of, instead of tearing up the worsening one.'); +INSERT INTO roads.repair_type VALUES (2, 'Patching', 'Patching is the process of filling potholes or excavated areas in the asphalt pavement. Quick repair of potholes or other pavement disintegration helps control further deterioration and expensive repair of the pavement. Without timely patching, water can enter the sub-grade and cause larger and more serious pavement failures.'); +INSERT INTO roads.repair_type VALUES (3, 'Reshaping', 'This is necessary when a road surface it too damaged to be smoothed. Using a grader blade and scarifying if necessary, you rework the gravel sub-base to eliminate large potholes and rebuild a flattened crown.'); +INSERT INTO roads.repair_type VALUES (4, 'Slab Replacement', 'This refers to replacing sections of paved roads. It is a good option for when slabs are chipped, cracked, or uneven, and mitigates the need to replace the entire road when just a small section is damaged.'); +INSERT INTO roads.repair_type VALUES (5, 'Smoothing', 'This is when you lightly rework the gravel of a road without digging in too far to the sub-base. Typically, a motor grader is used in this operation with an attached blade. Smoothing is done when the road has minor damage or is just worn down a bit from use.'); +INSERT INTO roads.repair_type VALUES (6, 'Reconstruction', 'When roads have deteriorated to a point that it is no longer cost-effective to maintain, the entire street or road needs to be rebuilt. Typically, this work is done in phases to limit traffic restrictions. As part of reconstruction, the street may be realigned to improve safety or operations, grading may be changed to improve storm water flow, underground utilities may be added, upgraded or relocated, traffic signals and street lights may be relocated, and street trees and pedestrian ramps may be added.'); + +CREATE TABLE roads.municipality ( + municipality_id character varying(20) NOT NULL, + contact_name character varying(30), + contact_title character varying(50), + local_region character varying(30), + state_id smallint NOT NULL +); +INSERT INTO roads.municipality VALUES ('New York', 'Alexander Wilkinson', 'Assistant City Clerk', 'Manhattan', 33); +INSERT INTO roads.municipality VALUES ('Los Angeles', 'Hugh Moser', 'Administrative Assistant', 'Santa Monica',5 ); +INSERT INTO roads.municipality VALUES ('Chicago', 'Phillip Bradshaw', 'Director of Community Engagement', 'West Ridge', 14); +INSERT INTO roads.municipality VALUES ('Houston', 'Leo Ackerman', 'Municipal Roads Specialist', 'The Woodlands', 44); +INSERT INTO roads.municipality VALUES ('Phoenix', 'Jessie Paul', 'Director of Finance and Administration', 'Old Town Scottsdale', 3); +INSERT INTO roads.municipality VALUES ('Philadelphia', 'Willie Chaney', 'Municipal Manager', 'Center City', 39); +INSERT INTO roads.municipality VALUES ('San Antonio', 'Chester Lyon', 'Treasurer', 'Alamo Heights', 44); +INSERT INTO roads.municipality VALUES ('San Diego', 'Ralph Helms', 'Senior Electrical Project Manager', 'Del Mar', 5); +INSERT INTO roads.municipality VALUES ('Dallas', 'Virgil Craft', 'Assistant Assessor (Town/Municipality)', 'Deep Ellum', 44); +INSERT INTO roads.municipality VALUES ('San Jose', 'Charles Carney', 'Municipal Accounting Manager', 'Santana Row', 5); + +CREATE TABLE roads.hard_hats ( + hard_hat_id smallint NOT NULL, + last_name character varying(20) NOT NULL, + first_name character varying(10) NOT NULL, + title character varying(30), + birth_date date, + hire_date date, + address character varying(60), + city character varying(15), + state character varying(15), + postal_code character varying(10), + country character varying(15), + manager smallint, + contractor_id smallint +); +INSERT INTO roads.hard_hats VALUES (1, 'Brian', 'Perkins', 'Construction Laborer', '1978-11-28', '2009-02-06', '4 Jennings Ave.', 'Jersey City', 'NJ', '37421', 'USA', 9, 1); +INSERT INTO roads.hard_hats VALUES (2, 'Nicholas', 'Massey', 'Carpenter', '1993-02-19', '2003-04-14', '9373 Southampton Street', 'Middletown', 'CT', '27292', 'USA', 9, 1); +INSERT INTO roads.hard_hats VALUES (3, 'Cathy', 'Best', 'Framer', '1994-08-30', '1990-07-02', '4 Hillside Street', 'Billerica', 'MA', '13440', 'USA', 9, 2); +INSERT INTO roads.hard_hats VALUES (4, 'Melanie', 'Stafford', 'Construction Manager', '1966-03-19', '2003-02-02', '77 Studebaker Lane', 'Southampton', 'PA', '71730', 'USA', 9, 2); +INSERT INTO roads.hard_hats VALUES (5, 'Donna', 'Riley', 'Pre-construction Manager', '1983-03-14', '2012-01-13', '82 Taylor Drive', 'Southgate', 'MI', '33125', 'USA', 9, 4); +INSERT INTO roads.hard_hats VALUES (6, 'Alfred', 'Clarke', 'Construction Superintendent', '1979-01-12', '2013-10-17', '7729 Catherine Street', 'Powder Springs', 'GA', '42001', 'USA', 9, 2); +INSERT INTO roads.hard_hats VALUES (7, 'William', 'Boone', 'Construction Laborer', '1970-02-28', '2013-01-02', '1 Border St.', 'Niagara Falls', 'NY', '14304', 'USA', 9, 4); +INSERT INTO roads.hard_hats VALUES (8, 'Luka', 'Henderson', 'Construction Laborer', '1988-12-09', '2013-03-05', '794 S. Chapel Ave.', 'Phoenix', 'AZ', '85021', 'USA', 9, 1); +INSERT INTO roads.hard_hats VALUES (9, 'Patrick', 'Ziegler', 'Construction Laborer', '1976-11-27', '2020-11-15', '321 Gainsway Circle', 'Muskogee', 'OK', '74403', 'USA', 9, 3); + +CREATE TABLE roads.hard_hat_state ( + hard_hat_id smallint NOT NULL, + state_id smallint NOT NULL +); +INSERT INTO roads.hard_hat_state VALUES (1, 2); +INSERT INTO roads.hard_hat_state VALUES (2, 32); +INSERT INTO roads.hard_hat_state VALUES (3, 28); +INSERT INTO roads.hard_hat_state VALUES (4, 12); +INSERT INTO roads.hard_hat_state VALUES (5, 5); +INSERT INTO roads.hard_hat_state VALUES (6, 3); +INSERT INTO roads.hard_hat_state VALUES (7, 16); +INSERT INTO roads.hard_hat_state VALUES (8, 32); +INSERT INTO roads.hard_hat_state VALUES (9, 41); + +CREATE TABLE roads.repair_order_details ( + repair_order_id smallint NOT NULL, + repair_type_id smallint NOT NULL, + price real NOT NULL, + quantity smallint NOT NULL, + discount real NOT NULL +); +INSERT INTO roads.repair_order_details VALUES (10001, 1, 63708, 1, 0.05); +INSERT INTO roads.repair_order_details VALUES (10002, 4, 67253, 1, 0.05); +INSERT INTO roads.repair_order_details VALUES (10003, 2, 66808, 1, 0.05); +INSERT INTO roads.repair_order_details VALUES (10004, 4, 18497, 1, 0.05); +INSERT INTO roads.repair_order_details VALUES (10005, 7, 76463, 1, 0.05); +INSERT INTO roads.repair_order_details VALUES (10006, 4, 87858, 1, 0.05); +INSERT INTO roads.repair_order_details VALUES (10007, 1, 63918, 1, 0.05); +INSERT INTO roads.repair_order_details VALUES (10008, 6, 21083, 1, 0.05); +INSERT INTO roads.repair_order_details VALUES (10009, 3, 74555, 1, 0.05); +INSERT INTO roads.repair_order_details VALUES (10010, 5, 27222, 1, 0.05); +INSERT INTO roads.repair_order_details VALUES (10011, 5, 73600, 1, 0.05); +INSERT INTO roads.repair_order_details VALUES (10012, 3, 54901, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10013, 5, 51594, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10014, 1, 65114, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10015, 1, 48919, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10016, 3, 70418, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10017, 1, 29684, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10018, 2, 62928, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10019, 2, 97916, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10020, 5, 44120, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10021, 1, 53374, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10022, 2, 87289, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10023, 2, 92366, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10024, 2, 47857, 1, 0.01); +INSERT INTO roads.repair_order_details VALUES (10025, 1, 68745, 1, 0.01); + +CREATE TABLE roads.repair_orders ( + repair_order_id smallint NOT NULL, + municipality_id character varying(20), + hard_hat_id smallint, + order_date date, + required_date date, + dispatched_date date, + dispatcher_id smallint +); +INSERT INTO roads.repair_orders VALUES (10001, 'New York', 1, '2007-07-04', '2009-07-18', '2007-12-01', 3); +INSERT INTO roads.repair_orders VALUES (10002, 'New York', 3, '2007-07-05', '2009-08-28', '2007-12-01', 1); +INSERT INTO roads.repair_orders VALUES (10003, 'New York', 5, '2007-07-08', '2009-08-12', '2007-12-01', 2); +INSERT INTO roads.repair_orders VALUES (10004, 'Dallas', 1, '2007-07-08', '2009-08-01', '2007-12-01', 1); +INSERT INTO roads.repair_orders VALUES (10005, 'San Antonio', 8, '2007-07-09', '2009-08-01', '2007-12-01', 2); +INSERT INTO roads.repair_orders VALUES (10006, 'New York', 3, '2007-07-10', '2009-08-01', '2007-12-01', 2); +INSERT INTO roads.repair_orders VALUES (10007, 'Philadelphia', 4, '2007-04-21', '2009-08-08', '2007-12-01', 2); +INSERT INTO roads.repair_orders VALUES (10008, 'Philadelphia', 5, '2007-04-22', '2009-08-09', '2007-12-01', 3); +INSERT INTO roads.repair_orders VALUES (10009, 'Philadelphia', 3, '2007-04-25', '2009-08-12', '2007-12-01', 2); +INSERT INTO roads.repair_orders VALUES (10010, 'Philadelphia', 4, '2007-04-26', '2009-08-13', '2007-12-01', 3); +INSERT INTO roads.repair_orders VALUES (10011, 'Philadelphia', 4, '2007-04-27', '2009-08-14', '2007-12-01', 1); +INSERT INTO roads.repair_orders VALUES (10012, 'Philadelphia', 8, '2007-04-28', '2009-08-15', '2007-12-01', 3); +INSERT INTO roads.repair_orders VALUES (10013, 'Philadelphia', 4, '2007-04-29', '2009-08-16', '2007-12-01', 1); +INSERT INTO roads.repair_orders VALUES (10014, 'Philadelphia', 6, '2007-04-29', '2009-08-16', '2007-12-01', 2); +INSERT INTO roads.repair_orders VALUES (10015, 'Philadelphia', 2, '2007-04-12', '2009-08-19', '2007-12-01', 3); +INSERT INTO roads.repair_orders VALUES (10016, 'Philadelphia', 9, '2007-04-13', '2009-08-20', '2007-12-01', 3); +INSERT INTO roads.repair_orders VALUES (10017, 'Philadelphia', 2, '2007-04-14', '2009-08-21', '2007-12-01', 3); +INSERT INTO roads.repair_orders VALUES (10018, 'Philadelphia', 6, '2007-04-15', '2009-08-22', '2007-12-01', 1); +INSERT INTO roads.repair_orders VALUES (10019, 'Philadelphia', 5, '2007-05-16', '2009-09-06', '2007-12-01', 3); +INSERT INTO roads.repair_orders VALUES (10020, 'Philadelphia', 1, '2007-05-19', '2009-08-26', '2007-12-01', 1); +INSERT INTO roads.repair_orders VALUES (10021, 'Philadelphia', 7, '2007-05-10', '2009-08-27', '2007-12-01', 3); +INSERT INTO roads.repair_orders VALUES (10022, 'Philadelphia', 5, '2007-05-11', '2009-08-14', '2007-12-01', 1); +INSERT INTO roads.repair_orders VALUES (10023, 'Philadelphia', 1, '2007-05-11', '2009-08-29', '2007-12-01', 1); +INSERT INTO roads.repair_orders VALUES (10024, 'Philadelphia', 5, '2007-05-11', '2009-08-29', '2007-12-01', 2); +INSERT INTO roads.repair_orders VALUES (10025, 'Philadelphia', 6, '2007-05-12', '2009-08-30', '2007-12-01', 2); + +CREATE TABLE roads.dispatchers ( + dispatcher_id smallint NOT NULL, + company_name character varying(40) NOT NULL, + phone character varying(24) +); +INSERT INTO roads.dispatchers VALUES (1, 'Pothole Pete', '(111) 111-1111'); +INSERT INTO roads.dispatchers VALUES (2, 'Asphalts R Us', '(222) 222-2222'); +INSERT INTO roads.dispatchers VALUES (3, 'Federal Roads Group', '(333) 333-3333'); +INSERT INTO roads.dispatchers VALUES (4, 'Local Patchers', '1-800-888-8888'); +INSERT INTO roads.dispatchers VALUES (5, 'Gravel INC', '1-800-000-0000'); +INSERT INTO roads.dispatchers VALUES (6, 'DJ Developers', '1-111-111-1111'); + +CREATE TABLE roads.contractors ( + contractor_id smallint NOT NULL, + company_name character varying(40) NOT NULL, + contact_name character varying(30), + contact_title character varying(30), + address character varying(60), + city character varying(15), + state character varying(15), + postal_code character varying(10), + country character varying(15), + phone character varying(24) +); +INSERT INTO roads.contractors VALUES (1, 'You Need Em We Find Em', 'Max Potter', 'Assistant Director', '4 Plumb Branch Lane', 'Goshen', 'IN', '46526', 'USA', '(111) 111-1111'); +INSERT INTO roads.contractors VALUES (2, 'Call Forwarding', 'Sylvester English', 'Administrator', '9650 Mill Lane', 'Raeford', 'NC', '28376', 'USA', '(222) 222-2222'); +INSERT INTO roads.contractors VALUES (3, 'The Connect', 'Paul Raymond', 'Administrator', '7587 Myrtle Ave.', 'Chaska', 'MN', '55318', 'USA', '(333) 333-3333'); + +CREATE TABLE roads.us_region ( + us_region_id smallint NOT NULL, + us_region_description character varying(60) NOT NULL +); +INSERT INTO roads.us_region VALUES (1, 'Eastern'); +INSERT INTO roads.us_region VALUES (2, 'Western'); +INSERT INTO roads.us_region VALUES (3, 'Northern'); +INSERT INTO roads.us_region VALUES (4, 'Southern'); + +CREATE TABLE roads.us_states ( + state_id smallint NOT NULL, + state_name character varying(100), + state_abbr character varying(2), + state_region character varying(50) +); +INSERT INTO roads.us_states VALUES (1, 'Alabama', 'AL', 'Southern'); +INSERT INTO roads.us_states VALUES (2, 'Alaska', 'AK', 'Northern'); +INSERT INTO roads.us_states VALUES (3, 'Arizona', 'AZ', 'Western'); +INSERT INTO roads.us_states VALUES (4, 'Arkansas', 'AR', 'Southern'); +INSERT INTO roads.us_states VALUES (5, 'California', 'CA', 'Western'); +INSERT INTO roads.us_states VALUES (6, 'Colorado', 'CO', 'Western'); +INSERT INTO roads.us_states VALUES (7, 'Connecticut', 'CT', 'Eastern'); +INSERT INTO roads.us_states VALUES (8, 'Delaware', 'DE', 'Eastern'); +INSERT INTO roads.us_states VALUES (9, 'District of Columbia', 'DC', 'Eastern'); +INSERT INTO roads.us_states VALUES (10, 'Florida', 'FL', 'Southern'); +INSERT INTO roads.us_states VALUES (11, 'Georgia', 'GA', 'Southern'); +INSERT INTO roads.us_states VALUES (12, 'Hawaii', 'HI', 'Western'); +INSERT INTO roads.us_states VALUES (13, 'Idaho', 'ID', 'Western'); +INSERT INTO roads.us_states VALUES (14, 'Illinois', 'IL', 'Western'); +INSERT INTO roads.us_states VALUES (15, 'Indiana', 'IN', 'Western'); +INSERT INTO roads.us_states VALUES (16, 'Iowa', 'IO', 'Western'); +INSERT INTO roads.us_states VALUES (17, 'Kansas', 'KS', 'Western'); +INSERT INTO roads.us_states VALUES (18, 'Kentucky', 'KY', 'Southern'); +INSERT INTO roads.us_states VALUES (19, 'Louisiana', 'LA', 'Southern'); +INSERT INTO roads.us_states VALUES (20, 'Maine', 'ME', 'Northern'); +INSERT INTO roads.us_states VALUES (21, 'Maryland', 'MD', 'Eastern'); +INSERT INTO roads.us_states VALUES (22, 'Massachusetts', 'MA', 'Northern'); +INSERT INTO roads.us_states VALUES (23, 'Michigan', 'MI', 'Northern'); +INSERT INTO roads.us_states VALUES (24, 'Minnesota', 'MN', 'Northern'); +INSERT INTO roads.us_states VALUES (25, 'Mississippi', 'MS', 'Southern'); +INSERT INTO roads.us_states VALUES (26, 'Missouri', 'MO', 'Southern'); +INSERT INTO roads.us_states VALUES (27, 'Montana', 'MT', 'Western'); +INSERT INTO roads.us_states VALUES (28, 'Nebraska', 'NE', 'Western'); +INSERT INTO roads.us_states VALUES (29, 'Nevada', 'NV', 'Western'); +INSERT INTO roads.us_states VALUES (30, 'New Hampshire', 'NH', 'Eastern'); +INSERT INTO roads.us_states VALUES (31, 'New Jersey', 'NJ', 'Eastern'); +INSERT INTO roads.us_states VALUES (32, 'New Mexico', 'NM', 'Western'); +INSERT INTO roads.us_states VALUES (33, 'New York', 'NY', 'Eastern'); +INSERT INTO roads.us_states VALUES (34, 'North Carolina', 'NC', 'Eastern'); +INSERT INTO roads.us_states VALUES (35, 'North Dakota', 'ND', 'Western'); +INSERT INTO roads.us_states VALUES (36, 'Ohio', 'OH', 'Western'); +INSERT INTO roads.us_states VALUES (37, 'Oklahoma', 'OK', 'Western'); +INSERT INTO roads.us_states VALUES (38, 'Oregon', 'OR', 'Western'); +INSERT INTO roads.us_states VALUES (39, 'Pennsylvania', 'PA', 'Eastern'); +INSERT INTO roads.us_states VALUES (40, 'Rhode Island', 'RI', 'Eastern'); +INSERT INTO roads.us_states VALUES (41, 'South Carolina', 'SC', 'Eastern'); +INSERT INTO roads.us_states VALUES (42, 'South Dakota', 'SD', 'Western'); +INSERT INTO roads.us_states VALUES (43, 'Tennessee', 'TN', 'Western'); +INSERT INTO roads.us_states VALUES (44, 'Texas', 'TX', 'Western'); +INSERT INTO roads.us_states VALUES (45, 'Utah', 'UT', 'Western'); +INSERT INTO roads.us_states VALUES (46, 'Vermont', 'VT', 'Eastern'); +INSERT INTO roads.us_states VALUES (47, 'Virginia', 'VA', 'Eastern'); +INSERT INTO roads.us_states VALUES (48, 'Washington', 'WA', 'Western'); +INSERT INTO roads.us_states VALUES (49, 'West Virginia', 'WV', 'Southern'); +INSERT INTO roads.us_states VALUES (50, 'Wisconsin', 'WI', 'Western'); +INSERT INTO roads.us_states VALUES (51, 'Wyoming', 'WY', 'Western'); + +CREATE TABLE roads.municipality_municipality_type ( + municipality_id character varying(20) NOT NULL, + municipality_type_id character varying(1) NOT NULL +); +INSERT INTO roads.municipality_municipality_type VALUES ('New York', 'A'); +INSERT INTO roads.municipality_municipality_type VALUES ('Los Angeles', 'B'); +INSERT INTO roads.municipality_municipality_type VALUES ('Chicago', 'B'); +INSERT INTO roads.municipality_municipality_type VALUES ('Houston', 'A'); +INSERT INTO roads.municipality_municipality_type VALUES ('Phoenix', 'A'); +INSERT INTO roads.municipality_municipality_type VALUES ('Philadelphia', 'B'); +INSERT INTO roads.municipality_municipality_type VALUES ('San Antonio', 'A'); +INSERT INTO roads.municipality_municipality_type VALUES ('San Diego', 'B'); +INSERT INTO roads.municipality_municipality_type VALUES ('Dallas', 'A'); +INSERT INTO roads.municipality_municipality_type VALUES ('San Jose', 'B'); + +CREATE TABLE roads.municipality_type ( + municipality_type_id character varying(1) NOT NULL, + municipality_type_desc text +); +INSERT INTO roads.municipality_type VALUES ('A', 'Primary'); +INSERT INTO roads.municipality_type VALUES ('A', 'Secondary'); diff --git a/datajunction-query/docker/postgres_init.sql b/datajunction-query/docker/postgres_init.sql new file mode 100644 index 000000000..f582f68b5 --- /dev/null +++ b/datajunction-query/docker/postgres_init.sql @@ -0,0 +1,63 @@ +-- +-- Basic example +-- +CREATE SCHEMA IF NOT EXISTS basic; + +-- +-- basic.dim_users +-- +CREATE TABLE IF NOT EXISTS basic.dim_users ( + id integer PRIMARY KEY, + full_name text, + age integer, + country text, + gender text, + preferred_language text +); + +INSERT INTO basic.dim_users (id, full_name, age, country, gender, preferred_language) + VALUES + (1, 'Alice One', 10, 'Argentina', 'female', 'Spanish'), + (2, 'Bob Two', 15, 'Brazil', 'male', 'Portuguese'), + (3, 'Charlie Three', 20, 'Chile', 'non-binary', 'Spanish'), + (4, 'Denise Four', 25, 'Denmark', 'female', 'Danish'), + (5, 'Ernie Five', 27, 'Equator', 'male', 'Spanish'), + (6, 'Fabian Six', 29, 'France', 'non-binary', 'French') +; + +-- +-- basic.comments +-- +CREATE TABLE IF NOT EXISTS basic.comments ( + id integer PRIMARY KEY, + user_id integer, + "timestamp" timestamp with time zone, + "text" text, + CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES basic.dim_users (id) +); + +INSERT INTO basic.comments (id, user_id, "timestamp", "text") + VALUES + (1, 1, '2021-01-01 01:00:00', 'Hola!'), + (2, 2, '2021-01-01 02:00:00', 'Oi, tudo bom?'), + (3, 3, '2021-01-01 03:00:00', 'Que pasa?'), + (4, 4, '2021-01-01 04:00:00', 'Også mig'), + (5, 5, '2021-01-01 05:00:00', 'Bueno'), + (6, 6, '2021-01-01 06:00:00', 'Bonjour!'), + (7, 2, '2021-01-01 07:00:00', 'Prazer em conhecer'), + (8, 3, '2021-01-01 08:00:00', 'Si, si'), + (9, 4, '2021-01-01 09:00:00', 'Hej'), + (10, 5, '2021-01-01 10:00:00', 'Por supuesto'), + (11, 6, '2021-01-01 11:00:00', 'Oui, oui'), + (12, 3, '2021-01-01 12:00:00', 'Como no?'), + (13, 4, '2021-01-01 13:00:00', 'Farvel'), + (14, 5, '2021-01-01 14:00:00', 'Hola, amigo!'), + (15, 6, '2021-01-01 15:00:00', 'Très bien'), + (16, 4, '2021-01-01 16:00:00', 'Dejligt at møde dig'), + (17, 5, '2021-01-01 17:00:00', 'Dale!'), + (18, 6, '2021-01-01 18:00:00', 'Bien sûr!'), + (19, 5, '2021-01-01 19:00:00', 'Hasta luego!'), + (20, 6, '2021-01-01 20:00:00', 'À toute à l'' heure ! '), + (21, 6, '2021-01-01 21:00:00', 'Peut être'), + (22, 6, '2021-01-01 00:00:00', 'Cześć!') +; diff --git a/datajunction-query/docker/spark.roads.sql b/datajunction-query/docker/spark.roads.sql new file mode 100644 index 000000000..9f6e6ad06 --- /dev/null +++ b/datajunction-query/docker/spark.roads.sql @@ -0,0 +1,267 @@ +CREATE DATABASE roads; + +CREATE TABLE roads.repair_type ( + repair_type_id int, + repair_type_name string, + contractor_id string +); +INSERT INTO roads.repair_type VALUES +(1, 'Asphalt Overlay', 'Asphalt overlays restore roads to a smooth condition. This resurfacing uses the deteriorating asphalt as a base for which the new layer is added on top of, instead of tearing up the worsening one.'), +(2, 'Patching', 'Patching is the process of filling potholes or excavated areas in the asphalt pavement. Quick repair of potholes or other pavement disintegration helps control further deterioration and expensive repair of the pavement. Without timely patching, water can enter the sub-grade and cause larger and more serious pavement failures.'), +(3, 'Reshaping', 'This is necessary when a road surface it too damaged to be smoothed. Using a grader blade and scarifying if necessary, you rework the gravel sub-base to eliminate large potholes and rebuild a flattened crown.'), +(4, 'Slab Replacement', 'This refers to replacing sections of paved roads. It is a good option for when slabs are chipped, cracked, or uneven, and mitigates the need to replace the entire road when just a small section is damaged.'), +(5, 'Smoothing', 'This is when you lightly rework the gravel of a road without digging in too far to the sub-base. Typically, a motor grader is used in this operation with an attached blade. Smoothing is done when the road has minor damage or is just worn down a bit from use.'), +(6, 'Reconstruction', 'When roads have deteriorated to a point that it is no longer cost-effective to maintain, the entire street or road needs to be rebuilt. Typically, this work is done in phases to limit traffic restrictions. As part of reconstruction, the street may be realigned to improve safety or operations, grading may be changed to improve storm water flow, underground utilities may be added, upgraded or relocated, traffic signals and street lights may be relocated, and street trees and pedestrian ramps may be added.'); + +CREATE TABLE roads.municipality ( + municipality_id string, + contact_name string, + contact_title string, + local_region string, + state_id int +); +INSERT INTO roads.municipality VALUES +('New York', 'Alexander Wilkinson', 'Assistant City Clerk', 'Manhattan', 33), +('Los Angeles', 'Hugh Moser', 'Administrative Assistant', 'Santa Monica',5 ), +('Chicago', 'Phillip Bradshaw', 'Director of Community Engagement', 'West Ridge', 14), +('Houston', 'Leo Ackerman', 'Municipal Roads Specialist', 'The Woodlands', 44), +('Phoenix', 'Jessie Paul', 'Director of Finance and Administration', 'Old Town Scottsdale', 3), +('Philadelphia', 'Willie Chaney', 'Municipal Manager', 'Center City', 39), +('San Antonio', 'Chester Lyon', 'Treasurer', 'Alamo Heights', 44), +('San Diego', 'Ralph Helms', 'Senior Electrical Project Manager', 'Del Mar', 5), +('Dallas', 'Virgil Craft', 'Assistant Assessor (Town/Municipality)', 'Deep Ellum', 44), +('San Jose', 'Charles Carney', 'Municipal Accounting Manager', 'Santana Row', 5); + +CREATE TABLE roads.hard_hats ( + hard_hat_id int, + last_name string, + first_name string, + title string, + birth_date date, + hire_date date, + address string, + city string, + state string, + postal_code string, + country string, + manager int, + contractor_id int +); +INSERT INTO roads.hard_hats VALUES +(1, 'Brian', 'Perkins', 'Construction Laborer', cast('1978-11-28' as date), cast('2009-02-06' as date), '4 Jennings Ave.', 'Jersey City', 'NJ', '37421', 'USA', 9, 1), +(2, 'Nicholas', 'Massey', 'Carpenter', cast('1993-02-19' as date), cast('2003-04-14' as date), '9373 Southampton Street', 'Middletown', 'CT', '27292', 'USA', 9, 1), +(3, 'Cathy', 'Best', 'Framer', cast('1994-08-30' as date), cast('1990-07-02' as date), '4 Hillside Street', 'Billerica', 'MA', '13440', 'USA', 9, 2), +(4, 'Melanie', 'Stafford', 'Construction Manager', cast('1966-03-19' as date), cast('2003-02-02' as date), '77 Studebaker Lane', 'Southampton', 'PA', '71730', 'USA', 9, 2), +(5, 'Donna', 'Riley', 'Pre-construction Manager', cast('1983-03-14' as date), cast('2012-01-13' as date), '82 Taylor Drive', 'Southgate', 'MI', '33125', 'USA', 9, 4), +(6, 'Alfred', 'Clarke', 'Construction Superintendent', cast('1979-01-12' as date), cast('2013-10-17' as date), '7729 Catherine Street', 'Powder Springs', 'GA', '42001', 'USA', 9, 2), +(7, 'William', 'Boone', 'Construction Laborer', cast('1970-02-28' as date), cast('2013-01-02' as date), '1 Border St.', 'Niagara Falls', 'NY', '14304', 'USA', 9, 4), +(8, 'Luka', 'Henderson', 'Construction Laborer', cast('1988-12-09' as date), cast('2013-03-05' as date), '794 S. Chapel Ave.', 'Phoenix', 'AZ', '85021', 'USA', 9, 1), +(9, 'Patrick', 'Ziegler', 'Construction Laborer', cast('1976-11-27' as date), cast('2020-11-15' as date), '321 Gainsway Circle', 'Muskogee', 'OK', '74403', 'USA', 9, 3); + +CREATE TABLE roads.hard_hat_state ( + hard_hat_id int, + state_id int +); +INSERT INTO roads.hard_hat_state VALUES +(1, 2), +(2, 32), +(3, 28), +(4, 12), +(5, 5), +(6, 3), +(7, 16), +(8, 32), +(9, 41); + +CREATE TABLE roads.repair_order_details ( + repair_order_id int, + repair_type_id int, + price real NOT NULL, + quantity int, + discount real NOT NULL +); +INSERT INTO roads.repair_order_details VALUES +(10001, 1, 63708, 1, 0.05), +(10002, 4, 67253, 1, 0.05), +(10003, 2, 66808, 1, 0.05), +(10004, 4, 18497, 1, 0.05), +(10005, 7, 76463, 1, 0.05), +(10006, 4, 87858, 1, 0.05), +(10007, 1, 63918, 1, 0.05), +(10008, 6, 21083, 1, 0.05), +(10009, 3, 74555, 1, 0.05), +(10010, 5, 27222, 1, 0.05), +(10011, 5, 73600, 1, 0.05), +(10012, 3, 54901, 1, 0.01), +(10013, 5, 51594, 1, 0.01), +(10014, 1, 65114, 1, 0.01), +(10015, 1, 48919, 1, 0.01), +(10016, 3, 70418, 1, 0.01), +(10017, 1, 29684, 1, 0.01), +(10018, 2, 62928, 1, 0.01), +(10019, 2, 97916, 1, 0.01), +(10020, 5, 44120, 1, 0.01), +(10021, 1, 53374, 1, 0.01), +(10022, 2, 87289, 1, 0.01), +(10023, 2, 92366, 1, 0.01), +(10024, 2, 47857, 1, 0.01), +(10025, 1, 68745, 1, 0.01); + +CREATE TABLE roads.repair_orders ( + repair_order_id int, + municipality_id string, + hard_hat_id int, + order_date date, + required_date date, + dispatched_date date, + dispatcher_id int +); +INSERT INTO roads.repair_orders VALUES +(10001, 'New York', 1, cast('2007-07-04' as date), cast('2009-07-18' as date), cast('2007-12-01' as date), 3), +(10002, 'New York', 3, cast('2007-07-05' as date), cast('2009-08-28' as date), cast('2007-12-01' as date), 1), +(10003, 'New York', 5, cast('2007-07-08' as date), cast('2009-08-12' as date), cast('2007-12-01' as date), 2), +(10004, 'Dallas', 1, cast('2007-07-08' as date), cast('2009-08-01' as date), cast('2007-12-01' as date), 1), +(10005, 'San Antonio', 8, cast('2007-07-09' as date), cast('2009-08-01' as date), cast('2007-12-01' as date), 2), +(10006, 'New York', 3, cast('2007-07-10' as date), cast('2009-08-01' as date), cast('2007-12-01' as date), 2), +(10007, 'Philadelphia', 4, cast('2007-04-21' as date), cast('2009-08-08' as date), cast('2007-12-01' as date), 2), +(10008, 'Philadelphia', 5, cast('2007-04-22' as date), cast('2009-08-09' as date), cast('2007-12-01' as date), 3), +(10009, 'Philadelphia', 3, cast('2007-04-25' as date), cast('2009-08-12' as date), cast('2007-12-01' as date), 2), +(10010, 'Philadelphia', 4, cast('2007-04-26' as date), cast('2009-08-13' as date), cast('2007-12-01' as date), 3), +(10011, 'Philadelphia', 4, cast('2007-04-27' as date), cast('2009-08-14' as date), cast('2007-12-01' as date), 1), +(10012, 'Philadelphia', 8, cast('2007-04-28' as date), cast('2009-08-15' as date), cast('2007-12-01' as date), 3), +(10013, 'Philadelphia', 4, cast('2007-04-29' as date), cast('2009-08-16' as date), cast('2007-12-01' as date), 1), +(10014, 'Philadelphia', 6, cast('2007-04-29' as date), cast('2009-08-16' as date), cast('2007-12-01' as date), 2), +(10015, 'Philadelphia', 2, cast('2007-04-12' as date), cast('2009-08-19' as date), cast('2007-12-01' as date), 3), +(10016, 'Philadelphia', 9, cast('2007-04-13' as date), cast('2009-08-20' as date), cast('2007-12-01' as date), 3), +(10017, 'Philadelphia', 2, cast('2007-04-14' as date), cast('2009-08-21' as date), cast('2007-12-01' as date), 3), +(10018, 'Philadelphia', 6, cast('2007-04-15' as date), cast('2009-08-22' as date), cast('2007-12-01' as date), 1), +(10019, 'Philadelphia', 5, cast('2007-05-16' as date), cast('2009-09-06' as date), cast('2007-12-01' as date), 3), +(10020, 'Philadelphia', 1, cast('2007-05-19' as date), cast('2009-08-26' as date), cast('2007-12-01' as date), 1), +(10021, 'Philadelphia', 7, cast('2007-05-10' as date), cast('2009-08-27' as date), cast('2007-12-01' as date), 3), +(10022, 'Philadelphia', 5, cast('2007-05-11' as date), cast('2009-08-14' as date), cast('2007-12-01' as date), 1), +(10023, 'Philadelphia', 1, cast('2007-05-11' as date), cast('2009-08-29' as date), cast('2007-12-01' as date), 1), +(10024, 'Philadelphia', 5, cast('2007-05-11' as date), cast('2009-08-29' as date), cast('2007-12-01' as date), 2), +(10025, 'Philadelphia', 6, cast('2007-05-12' as date), cast('2009-08-30' as date), cast('2007-12-01' as date), 2); + +CREATE TABLE roads.dispatchers ( + dispatcher_id int, + company_name string, + phone string +); +INSERT INTO roads.dispatchers VALUES +(1, 'Pothole Pete', '(111) 111-1111'), +(2, 'Asphalts R Us', '(222) 222-2222'), +(3, 'Federal Roads Group', '(333) 333-3333'), +(4, 'Local Patchers', '1-800-888-8888'), +(5, 'Gravel INC', '1-800-000-0000'), +(6, 'DJ Developers', '1-111-111-1111'); + +CREATE TABLE roads.contractors ( + contractor_id int, + company_name string, + contact_name string, + contact_title string, + address string, + city string, + state string, + postal_code string, + country string, + phone string +); +INSERT INTO roads.contractors VALUES +(1, 'You Need Em We Find Em', 'Max Potter', 'Assistant Director', '4 Plumb Branch Lane', 'Goshen', 'IN', '46526', 'USA', '(111) 111-1111'), +(2, 'Call Forwarding', 'Sylvester English', 'Administrator', '9650 Mill Lane', 'Raeford', 'NC', '28376', 'USA', '(222) 222-2222'), +(3, 'The Connect', 'Paul Raymond', 'Administrator', '7587 Myrtle Ave.', 'Chaska', 'MN', '55318', 'USA', '(333) 333-3333'); + +CREATE TABLE roads.us_region ( + us_region_id int, + us_region_description string +); +INSERT INTO roads.us_region VALUES +(1, 'Eastern'), +(2, 'Western'), +(3, 'Northern'), +(4, 'Southern'); + +CREATE TABLE roads.us_states ( + state_id int, + state_name string, + state_abbr string, + state_region string +); +INSERT INTO roads.us_states VALUES +(1, 'Alabama', 'AL', 'Southern'), +(2, 'Alaska', 'AK', 'Northern'), +(3, 'Arizona', 'AZ', 'Western'), +(4, 'Arkansas', 'AR', 'Southern'), +(5, 'California', 'CA', 'Western'), +(6, 'Colorado', 'CO', 'Western'), +(7, 'Connecticut', 'CT', 'Eastern'), +(8, 'Delaware', 'DE', 'Eastern'), +(9, 'District of Columbia', 'DC', 'Eastern'), +(10, 'Florida', 'FL', 'Southern'), +(11, 'Georgia', 'GA', 'Southern'), +(12, 'Hawaii', 'HI', 'Western'), +(13, 'Idaho', 'ID', 'Western'), +(14, 'Illinois', 'IL', 'Western'), +(15, 'Indiana', 'IN', 'Western'), +(16, 'Iowa', 'IO', 'Western'), +(17, 'Kansas', 'KS', 'Western'), +(18, 'Kentucky', 'KY', 'Southern'), +(19, 'Louisiana', 'LA', 'Southern'), +(20, 'Maine', 'ME', 'Northern'), +(21, 'Maryland', 'MD', 'Eastern'), +(22, 'Massachusetts', 'MA', 'Northern'), +(23, 'Michigan', 'MI', 'Northern'), +(24, 'Minnesota', 'MN', 'Northern'), +(25, 'Mississippi', 'MS', 'Southern'), +(26, 'Missouri', 'MO', 'Southern'), +(27, 'Montana', 'MT', 'Western'), +(28, 'Nebraska', 'NE', 'Western'), +(29, 'Nevada', 'NV', 'Western'), +(30, 'New Hampshire', 'NH', 'Eastern'), +(31, 'New Jersey', 'NJ', 'Eastern'), +(32, 'New Mexico', 'NM', 'Western'), +(33, 'New York', 'NY', 'Eastern'), +(34, 'North Carolina', 'NC', 'Eastern'), +(35, 'North Dakota', 'ND', 'Western'), +(36, 'Ohio', 'OH', 'Western'), +(37, 'Oklahoma', 'OK', 'Western'), +(38, 'Oregon', 'OR', 'Western'), +(39, 'Pennsylvania', 'PA', 'Eastern'), +(40, 'Rhode Island', 'RI', 'Eastern'), +(41, 'South Carolina', 'SC', 'Eastern'), +(42, 'South Dakota', 'SD', 'Western'), +(43, 'Tennessee', 'TN', 'Western'), +(44, 'Texas', 'TX', 'Western'), +(45, 'Utah', 'UT', 'Western'), +(46, 'Vermont', 'VT', 'Eastern'), +(47, 'Virginia', 'VA', 'Eastern'), +(48, 'Washington', 'WA', 'Western'), +(49, 'West Virginia', 'WV', 'Southern'), +(50, 'Wisconsin', 'WI', 'Western'), +(51, 'Wyoming', 'WY', 'Western'); + +CREATE TABLE roads.municipality_municipality_type ( + municipality_id string, + municipality_type_id string +); +INSERT INTO roads.municipality_municipality_type VALUES +('New York', 'A'), +('Los Angeles', 'B'), +('Chicago', 'B'), +('Houston', 'A'), +('Phoenix', 'A'), +('Philadelphia', 'B'), +('San Antonio', 'A'), +('San Diego', 'B'), +('Dallas', 'A'), +('San Jose', 'B'); + +CREATE TABLE roads.municipality_type ( + municipality_type_id string, + municipality_type_desc string +); +INSERT INTO roads.municipality_type VALUES +('A', 'Primary'), +('A', 'Secondary'); diff --git a/datajunction-query/docker/spark_load_roads.py b/datajunction-query/docker/spark_load_roads.py new file mode 100644 index 000000000..0e1fa1be8 --- /dev/null +++ b/datajunction-query/docker/spark_load_roads.py @@ -0,0 +1,19 @@ +""" +Load the roads database into a local spark warehouse +""" +from pyspark.sql import SparkSession # pylint: disable=import-error + +print("Starting spark session...") +spark = ( + SparkSession.builder.master("local[*]") + .appName("djqs-load-roads") + .enableHiveSupport() + .getOrCreate() +) +with open("/code/docker/spark.roads.sql", encoding="UTF-8") as sql_file: + queries = sql_file.read() + for query in queries.split(";"): + if query.strip(): + print("Submitting query...") + spark.sql(query) + print("Query completed...") diff --git a/examples/docker/wait-for b/datajunction-query/docker/wait-for similarity index 100% rename from examples/docker/wait-for rename to datajunction-query/docker/wait-for diff --git a/datajunction-query/openapi.json b/datajunction-query/openapi.json new file mode 100644 index 000000000..d86e92266 --- /dev/null +++ b/datajunction-query/openapi.json @@ -0,0 +1,912 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "DJ server", + "description": "A DataJunction metrics repository", + "license": { + "name": "MIT License", + "url": "https://mit-license.org/" + }, + "version": "0.0.post1.dev1+g2c5d4fa" + }, + "paths": { + "/databases/": { + "get": { + "summary": "Read Databases", + "description": "List the available databases.", + "operationId": "read_databases_databases__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Databases Databases Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Database" + } + } + } + } + } + } + } + }, + "/queries/": { + "post": { + "summary": "Submit Query", + "description": "Run or schedule a query.\n\nThis endpoint is different from others in that it accepts both JSON and msgpack, and\ncan also return JSON or msgpack, depending on HTTP headers.", + "operationId": "submit_query_queries__post", + "parameters": [ + { + "required": false, + "schema": { + "title": "Accept", + "type": "string" + }, + "name": "accept", + "in": "header" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "QueryCreate", + "required": [ + "database_id", + "submitted_query" + ], + "type": "object", + "properties": { + "database_id": { + "title": "Database Id", + "type": "integer" + }, + "catalog": { + "title": "Catalog", + "type": "string" + }, + "schema": { + "title": "Schema", + "type": "string" + }, + "submitted_query": { + "title": "Submitted Query", + "type": "string" + } + }, + "description": "Model for submitted queries." + } + }, + "application/msgpack": { + "schema": { + "title": "QueryCreate", + "required": [ + "database_id", + "submitted_query" + ], + "type": "object", + "properties": { + "database_id": { + "title": "Database Id", + "type": "integer" + }, + "catalog": { + "title": "Catalog", + "type": "string" + }, + "schema": { + "title": "Schema", + "type": "string" + }, + "submitted_query": { + "title": "Submitted Query", + "type": "string" + } + }, + "description": "Model for submitted queries." + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Return results as JSON or msgpack", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryWithResults" + } + }, + "application/msgpack": {} + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/queries/{query_id}/": { + "get": { + "summary": "Read Query", + "description": "Fetch information about a query.\n\nFor paginated queries we move the data from the results backend to the cache for a\nshort period, anticipating additional requests.", + "operationId": "read_query_queries__query_id___get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Query Id", + "type": "string", + "format": "uuid" + }, + "name": "query_id", + "in": "path" + }, + { + "required": false, + "schema": { + "title": "Limit", + "type": "integer", + "default": 0 + }, + "name": "limit", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0 + }, + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryWithResults" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/metrics/": { + "get": { + "summary": "Read Metrics", + "description": "List all available metrics.", + "operationId": "read_metrics_metrics__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Metrics Metrics Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Metric" + } + } + } + } + } + } + } + }, + "/metrics/{node_id}/": { + "get": { + "summary": "Read Metric", + "description": "Return a metric by ID.", + "operationId": "read_metric_metrics__node_id___get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Node Id", + "type": "integer" + }, + "name": "node_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Metric" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/metrics/{node_id}/data/": { + "get": { + "summary": "Read Metrics Data", + "description": "Return data for a metric.", + "operationId": "read_metrics_data_metrics__node_id__data__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Node Id", + "type": "integer" + }, + "name": "node_id", + "in": "path" + }, + { + "required": false, + "schema": { + "title": "Database Id", + "type": "integer" + }, + "name": "database_id", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "D", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "name": "d", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "F", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "name": "f", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryWithResults" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/metrics/{node_id}/sql/": { + "get": { + "summary": "Read Metrics Sql", + "description": "Return SQL for a metric.\n\nA database can be optionally specified. If no database is specified the optimal one\nwill be used.", + "operationId": "read_metrics_sql_metrics__node_id__sql__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Node Id", + "type": "integer" + }, + "name": "node_id", + "in": "path" + }, + { + "required": false, + "schema": { + "title": "Database Id", + "type": "integer" + }, + "name": "database_id", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "D", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "name": "d", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "F", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "name": "f", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TranslatedSQL" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/nodes/": { + "get": { + "summary": "Read Nodes", + "description": "List the available nodes.", + "operationId": "read_nodes_nodes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Nodes Nodes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/NodeMetadata" + } + } + } + } + } + } + } + }, + "/graphql": { + "get": { + "summary": "Handle Http Get", + "operationId": "handle_http_get_graphql_get", + "responses": { + "200": { + "description": "The GraphiQL integrated development environment.", + "content": { + "application/json": { + "schema": {} + } + } + }, + "404": { + "description": "Not found if GraphiQL is not enabled." + } + } + }, + "post": { + "summary": "Handle Http Post", + "operationId": "handle_http_post_graphql_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ColumnMetadata": { + "title": "ColumnMetadata", + "required": [ + "name", + "type" + ], + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/ColumnType" + } + }, + "description": "A simple model for column metadata." + }, + "ColumnType": { + "title": "ColumnType", + "enum": [ + "BYTES", + "STR", + "FLOAT", + "INT", + "DECIMAL", + "BOOL", + "DATETIME", + "DATE", + "TIME", + "TIMEDELTA", + "LIST", + "DICT" + ], + "type": "string", + "description": "\n Types for columns.\n\n These represent the values from the ``python_type`` attribute in SQLAlchemy columns.\n " + }, + "Database": { + "title": "Database", + "required": [ + "name", + "URI" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer" + }, + "uuid": { + "title": "Uuid", + "type": "string", + "format": "uuid" + }, + "name": { + "title": "Name", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string", + "default": "" + }, + "URI": { + "title": "Uri", + "type": "string" + }, + "extra_params": { + "title": "Extra Params", + "type": "object", + "default": {} + }, + "read_only": { + "title": "Read Only", + "type": "boolean", + "default": true + }, + "async_": { + "title": "Async ", + "type": "boolean", + "default": false + }, + "cost": { + "title": "Cost", + "type": "number", + "default": 1.0 + }, + "created_at": { + "title": "Created At", + "type": "string", + "format": "date-time" + }, + "updated_at": { + "title": "Updated At", + "type": "string", + "format": "date-time" + } + }, + "description": "A database.\n\nA simple example:\n\n name: druid\n description: An Apache Druid database\n URI: druid://localhost:8082/druid/v2/sql/\n read-only: true\n async_: false\n cost: 1.0" + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + } + } + } + }, + "Metric": { + "title": "Metric", + "required": [ + "id", + "name", + "created_at", + "updated_at", + "query", + "dimensions" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string", + "default": "" + }, + "created_at": { + "title": "Created At", + "type": "string", + "format": "date-time" + }, + "updated_at": { + "title": "Updated At", + "type": "string", + "format": "date-time" + }, + "query": { + "title": "Query", + "type": "string" + }, + "dimensions": { + "title": "Dimensions", + "type": "array", + "items": { + "type": "string" + } + } + }, + "description": "Class for a metric." + }, + "NodeMetadata": { + "title": "NodeMetadata", + "required": [ + "id", + "name", + "created_at", + "updated_at", + "type", + "columns" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string", + "default": "" + }, + "created_at": { + "title": "Created At", + "type": "string", + "format": "date-time" + }, + "updated_at": { + "title": "Updated At", + "type": "string", + "format": "date-time" + }, + "type": { + "$ref": "#/components/schemas/NodeType" + }, + "query": { + "title": "Query", + "type": "string" + }, + "columns": { + "title": "Columns", + "type": "array", + "items": { + "$ref": "#/components/schemas/SimpleColumn" + } + } + }, + "description": "A node with information about columns and if it is a metric." + }, + "NodeType": { + "title": "NodeType", + "enum": [ + "source", + "transform", + "metric", + "dimension" + ], + "type": "string", + "description": "\n Node type.\n\n A node can have 4 types, currently:\n\n 1. SOURCE nodes are root nodes in the DAG, and point to tables or views in a DB.\n 2. TRANSFORM nodes are SQL transformations, reading from SOURCE/TRANSFORM nodes.\n 3. METRIC nodes are leaves in the DAG, and have a single aggregation query.\n 4. DIMENSION nodes are special SOURCE nodes that can be auto-joined with METRICS.\n " + }, + "QueryResults": { + "title": "QueryResults", + "type": "array", + "items": { + "$ref": "#/components/schemas/StatementResults" + }, + "description": "Results for a given query." + }, + "QueryState": { + "title": "QueryState", + "enum": [ + "UNKNOWN", + "ACCEPTED", + "SCHEDULED", + "RUNNING", + "FINISHED", + "CANCELED", + "FAILED" + ], + "type": "string", + "description": "\n Different states of a query.\n " + }, + "QueryWithResults": { + "title": "QueryWithResults", + "required": [ + "database_id", + "id", + "submitted_query", + "results", + "errors" + ], + "type": "object", + "properties": { + "database_id": { + "title": "Database Id", + "type": "integer" + }, + "catalog": { + "title": "Catalog", + "type": "string" + }, + "schema": { + "title": "Schema", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string", + "format": "uuid" + }, + "submitted_query": { + "title": "Submitted Query", + "type": "string" + }, + "executed_query": { + "title": "Executed Query", + "type": "string" + }, + "scheduled": { + "title": "Scheduled", + "type": "string", + "format": "date-time" + }, + "started": { + "title": "Started", + "type": "string", + "format": "date-time" + }, + "finished": { + "title": "Finished", + "type": "string", + "format": "date-time" + }, + "state": { + "allOf": [ + { + "$ref": "#/components/schemas/QueryState" + } + ], + "default": "UNKNOWN" + }, + "progress": { + "title": "Progress", + "type": "number", + "default": 0.0 + }, + "results": { + "$ref": "#/components/schemas/QueryResults" + }, + "next": { + "title": "Next", + "maxLength": 65536, + "minLength": 1, + "type": "string", + "format": "uri" + }, + "previous": { + "title": "Previous", + "maxLength": 65536, + "minLength": 1, + "type": "string", + "format": "uri" + }, + "errors": { + "title": "Errors", + "type": "array", + "items": { + "type": "string" + } + } + }, + "description": "Model for query with results." + }, + "SimpleColumn": { + "title": "SimpleColumn", + "required": [ + "name", + "type" + ], + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/ColumnType" + } + }, + "description": "A simplified column schema, without ID or dimensions." + }, + "StatementResults": { + "title": "StatementResults", + "required": [ + "sql", + "columns", + "rows" + ], + "type": "object", + "properties": { + "sql": { + "title": "Sql", + "type": "string" + }, + "columns": { + "title": "Columns", + "type": "array", + "items": { + "$ref": "#/components/schemas/ColumnMetadata" + } + }, + "rows": { + "title": "Rows", + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "row_count": { + "title": "Row Count", + "type": "integer", + "default": 0 + } + }, + "description": "Results for a given statement.\n\nThis contains the SQL, column names and types, and rows" + }, + "TranslatedSQL": { + "title": "TranslatedSQL", + "required": [ + "database_id", + "sql" + ], + "type": "object", + "properties": { + "database_id": { + "title": "Database Id", + "type": "integer" + }, + "sql": { + "title": "Sql", + "type": "string" + } + }, + "description": "Class for SQL generated from a given metric." + }, + "ValidationError": { + "title": "ValidationError", + "required": [ + "loc", + "msg", + "type" + ], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "type": "string" + } + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/datajunction-query/pdm.lock b/datajunction-query/pdm.lock new file mode 100644 index 000000000..95f2b083b --- /dev/null +++ b/datajunction-query/pdm.lock @@ -0,0 +1,1807 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "test", "testing"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:ce7d6c05c689b0078acbbcf68a09f792a86df4085996fa19e6e7ad1c0e315349" + +[[metadata.targets]] +requires_python = "~=3.10" + +[[package]] +name = "accept-types" +version = "0.4.1" +summary = "Determine the best content to send in an HTTP response" +groups = ["default"] +files = [ + {file = "accept-types-0.4.1.tar.gz", hash = "sha256:fb27099716d8f0360408c8ca86d69dbfed44455834b70d1506250abe521b535a"}, + {file = "accept_types-0.4.1-py3-none-any.whl", hash = "sha256:c87feccdffb66b02f9343ff387d7fd5c451ccb2e1221fbd37ea0cedef5cf290f"}, +] + +[[package]] +name = "alembic" +version = "1.13.2" +requires_python = ">=3.8" +summary = "A database migration tool for SQLAlchemy." +groups = ["test"] +dependencies = [ + "Mako", + "SQLAlchemy>=1.3.0", + "importlib-metadata; python_version < \"3.9\"", + "importlib-resources; python_version < \"3.9\"", + "typing-extensions>=4", +] +files = [ + {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"}, + {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"}, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +requires_python = ">=3.8" +summary = "Reusable constraint types to use with typing.Annotated" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.9\"", +] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.4.0" +requires_python = ">=3.8" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +groups = ["default", "test"] +dependencies = [ + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions>=4.1; python_version < \"3.11\"", +] +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] + +[[package]] +name = "asn1crypto" +version = "1.5.1" +summary = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" +groups = ["default"] +files = [ + {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, + {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, +] + +[[package]] +name = "astroid" +version = "3.2.4" +requires_python = ">=3.8.0" +summary = "An abstract syntax tree for Python with inference support." +groups = ["test"] +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.11\"", +] +files = [ + {file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"}, + {file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"}, +] + +[[package]] +name = "build" +version = "1.2.2" +requires_python = ">=3.8" +summary = "A simple, correct Python build frontend" +groups = ["test"] +dependencies = [ + "colorama; os_name == \"nt\"", + "importlib-metadata>=4.6; python_full_version < \"3.10.2\"", + "packaging>=19.1", + "pyproject-hooks", + "tomli>=1.1.0; python_version < \"3.11\"", +] +files = [ + {file = "build-1.2.2-py3-none-any.whl", hash = "sha256:277ccc71619d98afdd841a0e96ac9fe1593b823af481d3b0cea748e8894e0613"}, + {file = "build-1.2.2.tar.gz", hash = "sha256:119b2fb462adef986483438377a13b2f42064a2a3a4161f24a0cca698a07ac8c"}, +] + +[[package]] +name = "cachelib" +version = "0.13.0" +requires_python = ">=3.8" +summary = "A collection of cache libraries in the same API interface." +groups = ["default"] +files = [ + {file = "cachelib-0.13.0-py3-none-any.whl", hash = "sha256:8c8019e53b6302967d4e8329a504acf75e7bc46130291d30188a6e4e58162516"}, + {file = "cachelib-0.13.0.tar.gz", hash = "sha256:209d8996e3c57595bee274ff97116d1d73c4980b2fd9a34c7846cd07fd2e1a48"}, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default", "test"] +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +groups = ["default"] +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +requires_python = ">=3.8" +summary = "Validate configuration and produce human readable error messages." +groups = ["test"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["default", "test"] +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["test"] +dependencies = [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[[package]] +name = "codespell" +version = "2.3.0" +requires_python = ">=3.8" +summary = "Codespell" +groups = ["test"] +files = [ + {file = "codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1"}, + {file = "codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["default", "test"] +marker = "sys_platform == \"win32\" or os_name == \"nt\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.1" +requires_python = ">=3.8" +summary = "Code coverage measurement for Python" +groups = ["test"] +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[[package]] +name = "coverage" +version = "7.6.1" +extras = ["toml"] +requires_python = ">=3.8" +summary = "Code coverage measurement for Python" +groups = ["test"] +dependencies = [ + "coverage==7.6.1", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[[package]] +name = "cryptography" +version = "43.0.1" +requires_python = ">=3.7" +summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +groups = ["default"] +dependencies = [ + "cffi>=1.12; platform_python_implementation != \"PyPy\"", +] +files = [ + {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, + {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, + {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, + {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, + {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, + {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, + {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, +] + +[[package]] +name = "dill" +version = "0.3.8" +requires_python = ">=3.8" +summary = "serialize all of Python" +groups = ["test"] +files = [ + {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, + {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +summary = "Distribution utilities" +groups = ["test"] +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "docker" +version = "7.1.0" +requires_python = ">=3.8" +summary = "A Python library for the Docker Engine API." +groups = ["test"] +dependencies = [ + "pywin32>=304; sys_platform == \"win32\"", + "requests>=2.26.0", + "urllib3>=1.26.0", +] +files = [ + {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, + {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, +] + +[[package]] +name = "duckdb" +version = "0.8.1" +summary = "DuckDB embedded database" +groups = ["default"] +files = [ + {file = "duckdb-0.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:14781d21580ee72aba1f5dcae7734674c9b6c078dd60470a08b2b420d15b996d"}, + {file = "duckdb-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f13bf7ab0e56ddd2014ef762ae4ee5ea4df5a69545ce1191b8d7df8118ba3167"}, + {file = "duckdb-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4032042d8363e55365bbca3faafc6dc336ed2aad088f10ae1a534ebc5bcc181"}, + {file = "duckdb-0.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a71bd8f0b0ca77c27fa89b99349ef22599ffefe1e7684ae2e1aa2904a08684"}, + {file = "duckdb-0.8.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24568d6e48f3dbbf4a933109e323507a46b9399ed24c5d4388c4987ddc694fd0"}, + {file = "duckdb-0.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297226c0dadaa07f7c5ae7cbdb9adba9567db7b16693dbd1b406b739ce0d7924"}, + {file = "duckdb-0.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5792cf777ece2c0591194006b4d3e531f720186102492872cb32ddb9363919cf"}, + {file = "duckdb-0.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:12803f9f41582b68921d6b21f95ba7a51e1d8f36832b7d8006186f58c3d1b344"}, + {file = "duckdb-0.8.1-cp310-cp310-win32.whl", hash = "sha256:d0953d5a2355ddc49095e7aef1392b7f59c5be5cec8cdc98b9d9dc1f01e7ce2b"}, + {file = "duckdb-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e6583c98a7d6637e83bcadfbd86e1f183917ea539f23b6b41178f32f813a5eb"}, + {file = "duckdb-0.8.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fad7ed0d4415f633d955ac24717fa13a500012b600751d4edb050b75fb940c25"}, + {file = "duckdb-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81ae602f34d38d9c48dd60f94b89f28df3ef346830978441b83c5b4eae131d08"}, + {file = "duckdb-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d75cfe563aaa058d3b4ccaaa371c6271e00e3070df5de72361fd161b2fe6780"}, + {file = "duckdb-0.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbb55e7a3336f2462e5e916fc128c47fe1c03b6208d6bd413ac11ed95132aa0"}, + {file = "duckdb-0.8.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6df53efd63b6fdf04657385a791a4e3c4fb94bfd5db181c4843e2c46b04fef5"}, + {file = "duckdb-0.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b188b80b70d1159b17c9baaf541c1799c1ce8b2af4add179a9eed8e2616be96"}, + {file = "duckdb-0.8.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ad481ee353f31250b45d64b4a104e53b21415577943aa8f84d0af266dc9af85"}, + {file = "duckdb-0.8.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1d1b1729993611b1892509d21c21628917625cdbe824a61ce891baadf684b32"}, + {file = "duckdb-0.8.1-cp311-cp311-win32.whl", hash = "sha256:2d8f9cc301e8455a4f89aa1088b8a2d628f0c1f158d4cf9bc78971ed88d82eea"}, + {file = "duckdb-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:07457a43605223f62d93d2a5a66b3f97731f79bbbe81fdd5b79954306122f612"}, + {file = "duckdb-0.8.1.tar.gz", hash = "sha256:a54d37f4abc2afc4f92314aaa56ecf215a411f40af4bffe1e86bd25e62aceee9"}, +] + +[[package]] +name = "duckdb-engine" +version = "0.13.2" +requires_python = "<4,>=3.8" +summary = "SQLAlchemy driver for duckdb" +groups = ["default"] +dependencies = [ + "duckdb>=0.5.0", + "packaging>=21", + "sqlalchemy>=1.3.22", +] +files = [ + {file = "duckdb_engine-0.13.2-py3-none-any.whl", hash = "sha256:a1a0ad9d16cdcbbe0f9a0844745b1e971f789b60cb46da990bb19a946c251128"}, + {file = "duckdb_engine-0.13.2.tar.gz", hash = "sha256:84cc4ad424345d9e6cf2df58f979caff66d755b0bc5b03043b918de5c2b05925"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["default", "test"] +marker = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[[package]] +name = "fastapi" +version = "0.114.1" +requires_python = ">=3.8" +summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +groups = ["default"] +dependencies = [ + "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", + "starlette<0.39.0,>=0.37.2", + "typing-extensions>=4.8.0", +] +files = [ + {file = "fastapi-0.114.1-py3-none-any.whl", hash = "sha256:5d4746f6e4b7dff0b4f6b6c6d5445645285f662fe75886e99af7ee2d6b58bb3e"}, + {file = "fastapi-0.114.1.tar.gz", hash = "sha256:1d7bbbeabbaae0acb0c22f0ab0b040f642d3093ca3645f8c876b6f91391861d8"}, +] + +[[package]] +name = "filelock" +version = "3.16.0" +requires_python = ">=3.8" +summary = "A platform independent file lock." +groups = ["default", "test"] +files = [ + {file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"}, + {file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"}, +] + +[[package]] +name = "freezegun" +version = "1.5.1" +requires_python = ">=3.7" +summary = "Let your Python tests travel through time" +groups = ["test"] +dependencies = [ + "python-dateutil>=2.7", +] +files = [ + {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, + {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, +] + +[[package]] +name = "greenlet" +version = "3.1.0" +requires_python = ">=3.7" +summary = "Lightweight in-process concurrent programming" +groups = ["default", "test"] +marker = "(platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version < \"3.13\"" +files = [ + {file = "greenlet-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a814dc3100e8a046ff48faeaa909e80cdb358411a3d6dd5293158425c684eda8"}, + {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a771dc64fa44ebe58d65768d869fcfb9060169d203446c1d446e844b62bdfdca"}, + {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e49a65d25d7350cca2da15aac31b6f67a43d867448babf997fe83c7505f57bc"}, + {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cd8518eade968bc52262d8c46727cfc0826ff4d552cf0430b8d65aaf50bb91d"}, + {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76dc19e660baea5c38e949455c1181bc018893f25372d10ffe24b3ed7341fb25"}, + {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0a5b1c22c82831f56f2f7ad9bbe4948879762fe0d59833a4a71f16e5fa0f682"}, + {file = "greenlet-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2651dfb006f391bcb240635079a68a261b227a10a08af6349cba834a2141efa1"}, + {file = "greenlet-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3e7e6ef1737a819819b1163116ad4b48d06cfdd40352d813bb14436024fcda99"}, + {file = "greenlet-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:ffb08f2a1e59d38c7b8b9ac8083c9c8b9875f0955b1e9b9b9a965607a51f8e54"}, + {file = "greenlet-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9730929375021ec90f6447bff4f7f5508faef1c02f399a1953870cdb78e0c345"}, + {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:713d450cf8e61854de9420fb7eea8ad228df4e27e7d4ed465de98c955d2b3fa6"}, + {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c3446937be153718250fe421da548f973124189f18fe4575a0510b5c928f0cc"}, + {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ddc7bcedeb47187be74208bc652d63d6b20cb24f4e596bd356092d8000da6d6"}, + {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44151d7b81b9391ed759a2f2865bbe623ef00d648fed59363be2bbbd5154656f"}, + {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cea1cca3be76c9483282dc7760ea1cc08a6ecec1f0b6ca0a94ea0d17432da19"}, + {file = "greenlet-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:619935a44f414274a2c08c9e74611965650b730eb4efe4b2270f91df5e4adf9a"}, + {file = "greenlet-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:221169d31cada333a0c7fd087b957c8f431c1dba202c3a58cf5a3583ed973e9b"}, + {file = "greenlet-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:01059afb9b178606b4b6e92c3e710ea1635597c3537e44da69f4531e111dd5e9"}, + {file = "greenlet-3.1.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:24fc216ec7c8be9becba8b64a98a78f9cd057fd2dc75ae952ca94ed8a893bf27"}, + {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d07c28b85b350564bdff9f51c1c5007dfb2f389385d1bc23288de51134ca303"}, + {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:243a223c96a4246f8a30ea470c440fe9db1f5e444941ee3c3cd79df119b8eebf"}, + {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26811df4dc81271033a7836bc20d12cd30938e6bd2e9437f56fa03da81b0f8fc"}, + {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9d86401550b09a55410f32ceb5fe7efcd998bd2dad9e82521713cb148a4a15f"}, + {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9c1c4f1748ccac0bae1dbb465fb1a795a75aba8af8ca871503019f4285e2a"}, + {file = "greenlet-3.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:cd468ec62257bb4544989402b19d795d2305eccb06cde5da0eb739b63dc04665"}, + {file = "greenlet-3.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a53dfe8f82b715319e9953330fa5c8708b610d48b5c59f1316337302af5c0811"}, + {file = "greenlet-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:28fe80a3eb673b2d5cc3b12eea468a5e5f4603c26aa34d88bf61bba82ceb2f9b"}, + {file = "greenlet-3.1.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:76b3e3976d2a452cba7aa9e453498ac72240d43030fdc6d538a72b87eaff52fd"}, + {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655b21ffd37a96b1e78cc48bf254f5ea4b5b85efaf9e9e2a526b3c9309d660ca"}, + {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6f4c2027689093775fd58ca2388d58789009116844432d920e9147f91acbe64"}, + {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76e5064fd8e94c3f74d9fd69b02d99e3cdb8fc286ed49a1f10b256e59d0d3a0b"}, + {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4bf607f690f7987ab3291406e012cd8591a4f77aa54f29b890f9c331e84989"}, + {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037d9ac99540ace9424cb9ea89f0accfaff4316f149520b4ae293eebc5bded17"}, + {file = "greenlet-3.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:90b5bbf05fe3d3ef697103850c2ce3374558f6fe40fd57c9fac1bf14903f50a5"}, + {file = "greenlet-3.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:726377bd60081172685c0ff46afbc600d064f01053190e4450857483c4d44484"}, + {file = "greenlet-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:d46d5069e2eeda111d6f71970e341f4bd9aeeee92074e649ae263b834286ecc0"}, + {file = "greenlet-3.1.0.tar.gz", hash = "sha256:b395121e9bbe8d02a750886f108d540abe66075e61e22f7353d9acb0b81be0f0"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["test"] +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +requires_python = ">=3.8" +summary = "A minimal low-level HTTP client." +groups = ["test"] +dependencies = [ + "certifi", + "h11<0.15,>=0.13", +] +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[[package]] +name = "httpx" +version = "0.27.2" +requires_python = ">=3.8" +summary = "The next generation HTTP client." +groups = ["test"] +dependencies = [ + "anyio", + "certifi", + "httpcore==1.*", + "idna", + "sniffio", +] +files = [ + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, +] + +[[package]] +name = "identify" +version = "2.6.0" +requires_python = ">=3.8" +summary = "File identification library for Python" +groups = ["test"] +files = [ + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, +] + +[[package]] +name = "idna" +version = "3.8" +requires_python = ">=3.6" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default", "test"] +files = [ + {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, + {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, +] + +[[package]] +name = "importlib-metadata" +version = "8.4.0" +requires_python = ">=3.8" +summary = "Read metadata from Python packages" +groups = ["default", "test"] +dependencies = [ + "typing-extensions>=3.6.4; python_version < \"3.8\"", + "zipp>=0.5", +] +files = [ + {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, + {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +groups = ["default", "test"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.13.2" +requires_python = ">=3.8.0" +summary = "A Python utility / library to sort Python imports." +groups = ["test"] +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[[package]] +name = "mako" +version = "1.3.5" +requires_python = ">=3.8" +summary = "A super-fast templating language that borrows the best ideas from the existing templating languages." +groups = ["test"] +dependencies = [ + "MarkupSafe>=0.9.2", +] +files = [ + {file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"}, + {file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +requires_python = ">=3.7" +summary = "Safely add untrusted strings to HTML/XML markup." +groups = ["test"] +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +requires_python = ">=3.6" +summary = "McCabe checker, plugin for flake8" +groups = ["test"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["default"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "msgpack" +version = "1.1.0" +requires_python = ">=3.8" +summary = "MessagePack serializer" +groups = ["default"] +files = [ + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, + {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, + {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, + {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, + {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, + {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, + {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, + {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, + {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, + {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Node.js virtual environment builder" +groups = ["test"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "packaging" +version = "24.1" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["default", "test"] +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pip" +version = "24.2" +requires_python = ">=3.8" +summary = "The PyPA recommended tool for installing Python packages." +groups = ["test"] +files = [ + {file = "pip-24.2-py3-none-any.whl", hash = "sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2"}, + {file = "pip-24.2.tar.gz", hash = "sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8"}, +] + +[[package]] +name = "pip-tools" +version = "7.4.1" +requires_python = ">=3.8" +summary = "pip-tools keeps your pinned dependencies fresh." +groups = ["test"] +dependencies = [ + "build>=1.0.0", + "click>=8", + "pip>=22.2", + "pyproject-hooks", + "setuptools", + "tomli; python_version < \"3.11\"", + "wheel", +] +files = [ + {file = "pip-tools-7.4.1.tar.gz", hash = "sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9"}, + {file = "pip_tools-7.4.1-py3-none-any.whl", hash = "sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.2" +requires_python = ">=3.8" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["default", "test"] +files = [ + {file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"}, + {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +requires_python = ">=3.8" +summary = "plugin and hook calling mechanisms for python" +groups = ["default", "test"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[[package]] +name = "pre-commit" +version = "3.8.0" +requires_python = ">=3.9" +summary = "A framework for managing and maintaining multi-language pre-commit hooks." +groups = ["test"] +dependencies = [ + "cfgv>=2.0.0", + "identify>=1.0.0", + "nodeenv>=0.11.1", + "pyyaml>=5.1", + "virtualenv>=20.10.0", +] +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] + +[[package]] +name = "psycopg" +version = "3.2.1" +requires_python = ">=3.8" +summary = "PostgreSQL database adapter for Python" +groups = ["default", "test"] +dependencies = [ + "backports-zoneinfo>=0.2.0; python_version < \"3.9\"", + "typing-extensions>=4.4", + "tzdata; sys_platform == \"win32\"", +] +files = [ + {file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"}, + {file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"}, +] + +[[package]] +name = "psycopg-pool" +version = "3.2.2" +requires_python = ">=3.8" +summary = "Connection Pool for Psycopg" +groups = ["default", "test"] +dependencies = [ + "typing-extensions>=4.4", +] +files = [ + {file = "psycopg_pool-3.2.2-py3-none-any.whl", hash = "sha256:273081d0fbfaced4f35e69200c89cb8fbddfe277c38cc86c235b90a2ec2c8153"}, + {file = "psycopg_pool-3.2.2.tar.gz", hash = "sha256:9e22c370045f6d7f2666a5ad1b0caf345f9f1912195b0b25d0d3bcc4f3a7389c"}, +] + +[[package]] +name = "psycopg" +version = "3.2.1" +extras = ["async", "pool"] +requires_python = ">=3.8" +summary = "PostgreSQL database adapter for Python" +groups = ["default", "test"] +dependencies = [ + "psycopg-pool", + "psycopg==3.2.1", +] +files = [ + {file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"}, + {file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +requires_python = ">=3.8" +summary = "C parser in Python" +groups = ["default"] +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pydantic" +version = "2.9.1" +requires_python = ">=3.8" +summary = "Data validation using Python type hints" +groups = ["default"] +dependencies = [ + "annotated-types>=0.6.0", + "pydantic-core==2.23.3", + "typing-extensions>=4.12.2; python_version >= \"3.13\"", + "typing-extensions>=4.6.1; python_version < \"3.13\"", +] +files = [ + {file = "pydantic-2.9.1-py3-none-any.whl", hash = "sha256:7aff4db5fdf3cf573d4b3c30926a510a10e19a0774d38fc4967f78beb6deb612"}, + {file = "pydantic-2.9.1.tar.gz", hash = "sha256:1363c7d975c7036df0db2b4a61f2e062fbc0aa5ab5f2772e0ffc7191a4f4bce2"}, +] + +[[package]] +name = "pydantic-core" +version = "2.23.3" +requires_python = ">=3.8" +summary = "Core functionality for Pydantic validation and serialization" +groups = ["default"] +dependencies = [ + "typing-extensions!=4.7.0,>=4.6.0", +] +files = [ + {file = "pydantic_core-2.23.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7f10a5d1b9281392f1bf507d16ac720e78285dfd635b05737c3911637601bae6"}, + {file = "pydantic_core-2.23.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c09a7885dd33ee8c65266e5aa7fb7e2f23d49d8043f089989726391dd7350c5"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6470b5a1ec4d1c2e9afe928c6cb37eb33381cab99292a708b8cb9aa89e62429b"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9172d2088e27d9a185ea0a6c8cebe227a9139fd90295221d7d495944d2367700"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86fc6c762ca7ac8fbbdff80d61b2c59fb6b7d144aa46e2d54d9e1b7b0e780e01"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0cb80fd5c2df4898693aa841425ea1727b1b6d2167448253077d2a49003e0ed"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03667cec5daf43ac4995cefa8aaf58f99de036204a37b889c24a80927b629cec"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:047531242f8e9c2db733599f1c612925de095e93c9cc0e599e96cf536aaf56ba"}, + {file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5499798317fff7f25dbef9347f4451b91ac2a4330c6669821c8202fd354c7bee"}, + {file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbb5e45eab7624440516ee3722a3044b83fff4c0372efe183fd6ba678ff681fe"}, + {file = "pydantic_core-2.23.3-cp310-none-win32.whl", hash = "sha256:8b5b3ed73abb147704a6e9f556d8c5cb078f8c095be4588e669d315e0d11893b"}, + {file = "pydantic_core-2.23.3-cp310-none-win_amd64.whl", hash = "sha256:2b603cde285322758a0279995b5796d64b63060bfbe214b50a3ca23b5cee3e83"}, + {file = "pydantic_core-2.23.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c889fd87e1f1bbeb877c2ee56b63bb297de4636661cc9bbfcf4b34e5e925bc27"}, + {file = "pydantic_core-2.23.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea85bda3189fb27503af4c45273735bcde3dd31c1ab17d11f37b04877859ef45"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7f7f72f721223f33d3dc98a791666ebc6a91fa023ce63733709f4894a7dc611"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b2b55b0448e9da68f56b696f313949cda1039e8ec7b5d294285335b53104b61"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c24574c7e92e2c56379706b9a3f07c1e0c7f2f87a41b6ee86653100c4ce343e5"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2b05e6ccbee333a8f4b8f4d7c244fdb7a979e90977ad9c51ea31261e2085ce0"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2c409ce1c219c091e47cb03feb3c4ed8c2b8e004efc940da0166aaee8f9d6c8"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d965e8b325f443ed3196db890d85dfebbb09f7384486a77461347f4adb1fa7f8"}, + {file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f56af3a420fb1ffaf43ece3ea09c2d27c444e7c40dcb7c6e7cf57aae764f2b48"}, + {file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b01a078dd4f9a52494370af21aa52964e0a96d4862ac64ff7cea06e0f12d2c5"}, + {file = "pydantic_core-2.23.3-cp311-none-win32.whl", hash = "sha256:560e32f0df04ac69b3dd818f71339983f6d1f70eb99d4d1f8e9705fb6c34a5c1"}, + {file = "pydantic_core-2.23.3-cp311-none-win_amd64.whl", hash = "sha256:c744fa100fdea0d000d8bcddee95213d2de2e95b9c12be083370b2072333a0fa"}, + {file = "pydantic_core-2.23.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e0ec50663feedf64d21bad0809f5857bac1ce91deded203efc4a84b31b2e4305"}, + {file = "pydantic_core-2.23.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db6e6afcb95edbe6b357786684b71008499836e91f2a4a1e55b840955b341dbb"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ccd69edcf49f0875d86942f4418a4e83eb3047f20eb897bffa62a5d419c8fa"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a678c1ac5c5ec5685af0133262103defb427114e62eafeda12f1357a12140162"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01491d8b4d8db9f3391d93b0df60701e644ff0894352947f31fff3e52bd5c801"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fcf31facf2796a2d3b7fe338fe8640aa0166e4e55b4cb108dbfd1058049bf4cb"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7200fd561fb3be06827340da066df4311d0b6b8eb0c2116a110be5245dceb326"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc1636770a809dee2bd44dd74b89cc80eb41172bcad8af75dd0bc182c2666d4c"}, + {file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:67a5def279309f2e23014b608c4150b0c2d323bd7bccd27ff07b001c12c2415c"}, + {file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:748bdf985014c6dd3e1e4cc3db90f1c3ecc7246ff5a3cd4ddab20c768b2f1dab"}, + {file = "pydantic_core-2.23.3-cp312-none-win32.whl", hash = "sha256:255ec6dcb899c115f1e2a64bc9ebc24cc0e3ab097775755244f77360d1f3c06c"}, + {file = "pydantic_core-2.23.3-cp312-none-win_amd64.whl", hash = "sha256:40b8441be16c1e940abebed83cd006ddb9e3737a279e339dbd6d31578b802f7b"}, + {file = "pydantic_core-2.23.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6daaf5b1ba1369a22c8b050b643250e3e5efc6a78366d323294aee54953a4d5f"}, + {file = "pydantic_core-2.23.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015e63b985a78a3d4ccffd3bdf22b7c20b3bbd4b8227809b3e8e75bc37f9cb2"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3fc572d9b5b5cfe13f8e8a6e26271d5d13f80173724b738557a8c7f3a8a3791"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6bd91345b5163ee7448bee201ed7dd601ca24f43f439109b0212e296eb5b423"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc379c73fd66606628b866f661e8785088afe2adaba78e6bbe80796baf708a63"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdce4b47592f9e296e19ac31667daed8753c8367ebb34b9a9bd89dacaa299c9"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3cf31edf405a161a0adad83246568647c54404739b614b1ff43dad2b02e6d5"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e22b477bf90db71c156f89a55bfe4d25177b81fce4aa09294d9e805eec13855"}, + {file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0a0137ddf462575d9bce863c4c95bac3493ba8e22f8c28ca94634b4a1d3e2bb4"}, + {file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:203171e48946c3164fe7691fc349c79241ff8f28306abd4cad5f4f75ed80bc8d"}, + {file = "pydantic_core-2.23.3-cp313-none-win32.whl", hash = "sha256:76bdab0de4acb3f119c2a4bff740e0c7dc2e6de7692774620f7452ce11ca76c8"}, + {file = "pydantic_core-2.23.3-cp313-none-win_amd64.whl", hash = "sha256:37ba321ac2a46100c578a92e9a6aa33afe9ec99ffa084424291d84e456f490c1"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f399e8657c67313476a121a6944311fab377085ca7f490648c9af97fc732732d"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b5547d098c76e1694ba85f05b595720d7c60d342f24d5aad32c3049131fa5c4"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dda0290a6f608504882d9f7650975b4651ff91c85673341789a476b1159f211"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b6e5da855e9c55a0c67f4db8a492bf13d8d3316a59999cfbaf98cc6e401961"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09e926397f392059ce0afdcac920df29d9c833256354d0c55f1584b0b70cf07e"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:87cfa0ed6b8c5bd6ae8b66de941cece179281239d482f363814d2b986b79cedc"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e61328920154b6a44d98cabcb709f10e8b74276bc709c9a513a8c37a18786cc4"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce3317d155628301d649fe5e16a99528d5680af4ec7aa70b90b8dacd2d725c9b"}, + {file = "pydantic_core-2.23.3.tar.gz", hash = "sha256:3cb0f65d8b4121c1b015c60104a685feb929a29d7cf204387c7f2688c7974690"}, +] + +[[package]] +name = "pydruid" +version = "0.6.9" +summary = "A Python connector for Druid." +groups = ["test"] +dependencies = [ + "requests", +] +files = [ + {file = "pydruid-0.6.9.tar.gz", hash = "sha256:63c41b33ab47fbb71cc25d3f3316cad78f18bfe947fa108862dd841d1f44fe49"}, +] + +[[package]] +name = "pyfakefs" +version = "5.6.0" +requires_python = ">=3.7" +summary = "pyfakefs implements a fake file system that mocks the Python file system modules." +groups = ["test"] +files = [ + {file = "pyfakefs-5.6.0-py3-none-any.whl", hash = "sha256:1a45bba8615323ec29d65929d32dc66d7b59a1e60a02109950440edb0486c539"}, + {file = "pyfakefs-5.6.0.tar.gz", hash = "sha256:7a549b32865aa97d8ba6538285a93816941d9b7359be2954ac60ec36b277e879"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +requires_python = ">=3.8" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["default"] +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[[package]] +name = "pyjwt" +version = "2.9.0" +requires_python = ">=3.8" +summary = "JSON Web Token implementation in Python" +groups = ["default"] +files = [ + {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, + {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, +] + +[[package]] +name = "pylint" +version = "3.2.7" +requires_python = ">=3.8.0" +summary = "python code static checker" +groups = ["test"] +dependencies = [ + "astroid<=3.3.0-dev0,>=3.2.4", + "colorama>=0.4.5; sys_platform == \"win32\"", + "dill>=0.2; python_version < \"3.11\"", + "dill>=0.3.6; python_version >= \"3.11\"", + "dill>=0.3.7; python_version >= \"3.12\"", + "isort!=5.13.0,<6,>=4.2.5", + "mccabe<0.8,>=0.6", + "platformdirs>=2.2.0", + "tomli>=1.1.0; python_version < \"3.11\"", + "tomlkit>=0.10.1", + "typing-extensions>=3.10.0; python_version < \"3.10\"", +] +files = [ + {file = "pylint-3.2.7-py3-none-any.whl", hash = "sha256:02f4aedeac91be69fb3b4bea997ce580a4ac68ce58b89eaefeaf06749df73f4b"}, + {file = "pylint-3.2.7.tar.gz", hash = "sha256:1b7a721b575eaeaa7d39db076b6e7743c993ea44f57979127c517c6c572c803e"}, +] + +[[package]] +name = "pyopenssl" +version = "24.2.1" +requires_python = ">=3.7" +summary = "Python wrapper module around the OpenSSL library" +groups = ["default"] +dependencies = [ + "cryptography<44,>=41.0.5", +] +files = [ + {file = "pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"}, + {file = "pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95"}, +] + +[[package]] +name = "pyproject-hooks" +version = "1.1.0" +requires_python = ">=3.7" +summary = "Wrappers to call pyproject.toml-based build backend hooks." +groups = ["test"] +files = [ + {file = "pyproject_hooks-1.1.0-py3-none-any.whl", hash = "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2"}, + {file = "pyproject_hooks-1.1.0.tar.gz", hash = "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965"}, +] + +[[package]] +name = "pytest" +version = "8.3.3" +requires_python = ">=3.8" +summary = "pytest: simple powerful testing with Python" +groups = ["default", "test"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2,>=1.5", + "tomli>=1; python_version < \"3.11\"", +] +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +requires_python = ">=3.8" +summary = "Pytest support for asyncio" +groups = ["default"] +dependencies = [ + "pytest<9,>=8.2", +] +files = [ + {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, + {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +requires_python = ">=3.8" +summary = "Pytest plugin for measuring coverage." +groups = ["test"] +dependencies = [ + "coverage[toml]>=5.2.1", + "pytest>=4.6", +] +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[[package]] +name = "pytest-integration" +version = "0.2.3" +requires_python = ">=3.6" +summary = "Organizing pytests by integration or not" +groups = ["default"] +files = [ + {file = "pytest_integration-0.2.3-py3-none-any.whl", hash = "sha256:7f59ed1fa1cc8cb240f9495b68bc02c0421cce48589f78e49b7b842231604b12"}, + {file = "pytest_integration-0.2.3.tar.gz", hash = "sha256:b00988a5de8a6826af82d4c7a3485b43fbf32c11235e9f4a8b7225eef5fbcf65"}, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +requires_python = ">=3.8" +summary = "Thin-wrapper around the mock package for easier use with pytest" +groups = ["test"] +dependencies = [ + "pytest>=6.2.5", +] +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +groups = ["default", "test"] +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[[package]] +name = "python-dotenv" +version = "0.19.2" +requires_python = ">=3.5" +summary = "Read key-value pairs from a .env file and set them as environment variables" +groups = ["default"] +files = [ + {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, + {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, +] + +[[package]] +name = "pytz" +version = "2024.2" +summary = "World timezone definitions, modern and historical" +groups = ["default"] +files = [ + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] + +[[package]] +name = "pywin32" +version = "306" +summary = "Python for Window Extensions" +groups = ["test"] +marker = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +requires_python = ">=3.8" +summary = "YAML parser and emitter for Python" +groups = ["default", "test"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "requests" +version = "2.29.0" +requires_python = ">=3.7" +summary = "Python HTTP for Humans." +groups = ["default", "test"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<1.27,>=1.21.1", +] +files = [ + {file = "requests-2.29.0-py3-none-any.whl", hash = "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b"}, + {file = "requests-2.29.0.tar.gz", hash = "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059"}, +] + +[[package]] +name = "requests-mock" +version = "1.12.1" +requires_python = ">=3.5" +summary = "Mock out responses from the requests package" +groups = ["test"] +dependencies = [ + "requests<3,>=2.22", +] +files = [ + {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, + {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, +] + +[[package]] +name = "rich" +version = "13.8.1" +requires_python = ">=3.7.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +groups = ["default"] +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", + "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", +] +files = [ + {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, + {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, +] + +[[package]] +name = "setuptools" +version = "74.1.2" +requires_python = ">=3.8" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +groups = ["test"] +files = [ + {file = "setuptools-74.1.2-py3-none-any.whl", hash = "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308"}, + {file = "setuptools-74.1.2.tar.gz", hash = "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6"}, +] + +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" +groups = ["default", "test"] +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +groups = ["default", "test"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "snowflake-connector-python" +version = "3.12.1" +requires_python = ">=3.8" +summary = "Snowflake Connector for Python" +groups = ["default"] +dependencies = [ + "asn1crypto<2.0.0,>0.24.0", + "certifi>=2017.4.17", + "cffi<2.0.0,>=1.9", + "charset-normalizer<4,>=2", + "cryptography>=3.1.0", + "filelock<4,>=3.5", + "idna<4,>=2.5", + "importlib-metadata; python_version < \"3.8\"", + "packaging", + "platformdirs<5.0.0,>=2.6.0", + "pyOpenSSL<25.0.0,>=16.2.0", + "pyjwt<3.0.0", + "pytz", + "requests<3.0.0", + "sortedcontainers>=2.4.0", + "tomlkit", + "typing-extensions<5,>=4.3", + "urllib3<2.0.0,>=1.21.1; python_version < \"3.10\"", +] +files = [ + {file = "snowflake_connector_python-3.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0979324bd96019f500f6c987d4720c9e4d7176df54b1b5aa96875be8c8ff57b"}, + {file = "snowflake_connector_python-3.12.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:c889a85966ec6a3384799e594e97301a4be0705d7763a5177104866b75383d8c"}, + {file = "snowflake_connector_python-3.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bfb5fe8db051771480059ffddd5127653f4ac1168c76293655da33c2a2904d7"}, + {file = "snowflake_connector_python-3.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1061af4a3a3e66b0c99ab0f8bae5eda28e6324618143b3f5b2d81d1649b8557"}, + {file = "snowflake_connector_python-3.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3edcf3591b6071ddb02413a0000dea42ee6fe811693d176915edb8687b03ce89"}, + {file = "snowflake_connector_python-3.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:226a714eb68bbae328fe49b705ecb304fbd44ea6a7afbb329ba3c389ac9111bc"}, + {file = "snowflake_connector_python-3.12.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:7319f63c09efed853d7652cbb38ecc23068e86dbce8340444056787993a854d9"}, + {file = "snowflake_connector_python-3.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f86b42a076e14900dc6af2f096343ccf4314d324e7e1153b667d6ee53c60334b"}, + {file = "snowflake_connector_python-3.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d231f0d5fb8d7a96b9ab5e9500035bd9f259c80d4b3c482163d156928fb0e546"}, + {file = "snowflake_connector_python-3.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:d9f1bc6b35344b170e2fb30314aa64709b28539084be88e95aacf094e13259eb"}, + {file = "snowflake_connector_python-3.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0114370c274ed64fe4aee2333b01e9ff88272837bdaa65fb3a3ee4820dca61b4"}, + {file = "snowflake_connector_python-3.12.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:dadd262196cce0132ca7e766f055e00c00497a88fdf83fd48143eb4a469a4527"}, + {file = "snowflake_connector_python-3.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473642c0e628b8b9f264cbf31c7f4de44974373db43052b6542a66e751159caf"}, + {file = "snowflake_connector_python-3.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bddc4cdcd991f9538726a7c293d2637bb5aed43db68246e06c92c49a6df2b692"}, + {file = "snowflake_connector_python-3.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:b06c63ec0381df1f4da6c4326330a1a40c8fc21fd3dcc2f58df4de395d676893"}, + {file = "snowflake_connector_python-3.12.1.tar.gz", hash = "sha256:e43b7d4b4488ecd97b5bf62539cc502d7e84d8215c547eaeb4dd928c0b7212b9"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +summary = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +groups = ["default"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.34" +requires_python = ">=3.7" +summary = "Database Abstraction Library" +groups = ["default", "test"] +dependencies = [ + "greenlet!=0.4.17; (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version < \"3.13\"", + "importlib-metadata; python_version < \"3.8\"", + "typing-extensions>=4.6.0", +] +files = [ + {file = "SQLAlchemy-2.0.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:95d0b2cf8791ab5fb9e3aa3d9a79a0d5d51f55b6357eecf532a120ba3b5524db"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:243f92596f4fd4c8bd30ab8e8dd5965afe226363d75cab2468f2c707f64cd83b"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ea54f7300553af0a2a7235e9b85f4204e1fc21848f917a3213b0e0818de9a24"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173f5f122d2e1bff8fbd9f7811b7942bead1f5e9f371cdf9e670b327e6703ebd"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:196958cde924a00488e3e83ff917be3b73cd4ed8352bbc0f2989333176d1c54d"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd90c221ed4e60ac9d476db967f436cfcecbd4ef744537c0f2d5291439848768"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-win32.whl", hash = "sha256:3166dfff2d16fe9be3241ee60ece6fcb01cf8e74dd7c5e0b64f8e19fab44911b"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-win_amd64.whl", hash = "sha256:6831a78bbd3c40f909b3e5233f87341f12d0b34a58f14115c9e94b4cdaf726d3"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7db3db284a0edaebe87f8f6642c2b2c27ed85c3e70064b84d1c9e4ec06d5d84"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:430093fce0efc7941d911d34f75a70084f12f6ca5c15d19595c18753edb7c33b"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79cb400c360c7c210097b147c16a9e4c14688a6402445ac848f296ade6283bbc"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb1b30f31a36c7f3fee848391ff77eebdd3af5750bf95fbf9b8b5323edfdb4ec"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fddde2368e777ea2a4891a3fb4341e910a056be0bb15303bf1b92f073b80c02"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80bd73ea335203b125cf1d8e50fef06be709619eb6ab9e7b891ea34b5baa2287"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-win32.whl", hash = "sha256:6daeb8382d0df526372abd9cb795c992e18eed25ef2c43afe518c73f8cccb721"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-win_amd64.whl", hash = "sha256:5bc08e75ed11693ecb648b7a0a4ed80da6d10845e44be0c98c03f2f880b68ff4"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:53e68b091492c8ed2bd0141e00ad3089bcc6bf0e6ec4142ad6505b4afe64163e"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bcd18441a49499bf5528deaa9dee1f5c01ca491fc2791b13604e8f972877f812"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:165bbe0b376541092bf49542bd9827b048357f4623486096fc9aaa6d4e7c59a2"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3330415cd387d2b88600e8e26b510d0370db9b7eaf984354a43e19c40df2e2b"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97b850f73f8abbffb66ccbab6e55a195a0eb655e5dc74624d15cff4bfb35bd74"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee4c6917857fd6121ed84f56d1dc78eb1d0e87f845ab5a568aba73e78adf83"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-win32.whl", hash = "sha256:fbb034f565ecbe6c530dff948239377ba859420d146d5f62f0271407ffb8c580"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-win_amd64.whl", hash = "sha256:707c8f44931a4facd4149b52b75b80544a8d824162602b8cd2fe788207307f9a"}, + {file = "SQLAlchemy-2.0.34-py3-none-any.whl", hash = "sha256:7286c353ee6475613d8beff83167374006c6b3e3f0e6491bfe8ca610eb1dec0f"}, + {file = "sqlalchemy-2.0.34.tar.gz", hash = "sha256:10d8f36990dd929690666679b0f42235c159a7051534adb135728ee52828dd22"}, +] + +[[package]] +name = "starlette" +version = "0.38.5" +requires_python = ">=3.8" +summary = "The little ASGI library that shines." +groups = ["default"] +dependencies = [ + "anyio<5,>=3.4.0", + "typing-extensions>=3.10.0; python_version < \"3.10\"", +] +files = [ + {file = "starlette-0.38.5-py3-none-any.whl", hash = "sha256:632f420a9d13e3ee2a6f18f437b0a9f1faecb0bc42e1942aa2ea0e379a4c4206"}, + {file = "starlette-0.38.5.tar.gz", hash = "sha256:04a92830a9b6eb1442c766199d62260c3d4dc9c4f9188360626b1e0273cb7077"}, +] + +[[package]] +name = "tenacity" +version = "9.0.0" +requires_python = ">=3.8" +summary = "Retry code until it succeeds" +groups = ["default"] +files = [ + {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, + {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, +] + +[[package]] +name = "testcontainers" +version = "4.8.1" +requires_python = "<4.0,>=3.9" +summary = "Python library for throwaway instances of anything that can run in a Docker container" +groups = ["test"] +dependencies = [ + "docker", + "typing-extensions", + "urllib3", + "wrapt", +] +files = [ + {file = "testcontainers-4.8.1-py3-none-any.whl", hash = "sha256:d8ae43e8fe34060fcd5c3f494e0b7652b7774beabe94568a2283d0881e94d489"}, + {file = "testcontainers-4.8.1.tar.gz", hash = "sha256:5ded4820b7227ad526857eb3caaafcabce1bbac05d22ad194849b136ffae3cb0"}, +] + +[[package]] +name = "testcontainers" +version = "4.8.1" +extras = ["postgres"] +requires_python = "<4.0,>=3.9" +summary = "Python library for throwaway instances of anything that can run in a Docker container" +groups = ["test"] +dependencies = [ + "testcontainers==4.8.1", +] +files = [ + {file = "testcontainers-4.8.1-py3-none-any.whl", hash = "sha256:d8ae43e8fe34060fcd5c3f494e0b7652b7774beabe94568a2283d0881e94d489"}, + {file = "testcontainers-4.8.1.tar.gz", hash = "sha256:5ded4820b7227ad526857eb3caaafcabce1bbac05d22ad194849b136ffae3cb0"}, +] + +[[package]] +name = "toml" +version = "0.10.2" +requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python Library for Tom's Obvious, Minimal Language" +groups = ["default"] +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +groups = ["default", "test"] +marker = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +requires_python = ">=3.8" +summary = "Style preserving TOML library" +groups = ["default", "test"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "trino" +version = "0.324.0" +requires_python = ">=3.7" +summary = "Client for the Trino distributed SQL Engine" +groups = ["default"] +dependencies = [ + "backports-zoneinfo; python_version < \"3.9\"", + "python-dateutil", + "pytz", + "requests", + "tzlocal", +] +files = [ + {file = "trino-0.324.0-py3-none-any.whl", hash = "sha256:19390848b8a88dcd7691e5a8a2d722bca96ec1cfa509e8b4d904977202599147"}, + {file = "trino-0.324.0.tar.gz", hash = "sha256:aa4d25376529dbfc5b5d0968e808438f923eb7e296b1bc091ed0e3fb5a6957ad"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default", "test"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2024.1" +requires_python = ">=2" +summary = "Provider of IANA time zone data" +groups = ["default", "test"] +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[[package]] +name = "tzlocal" +version = "5.2" +requires_python = ">=3.8" +summary = "tzinfo object for the local timezone" +groups = ["default"] +dependencies = [ + "backports-zoneinfo; python_version < \"3.9\"", + "tzdata; platform_system == \"Windows\"", +] +files = [ + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["default", "test"] +files = [ + {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, + {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, +] + +[[package]] +name = "virtualenv" +version = "20.26.4" +requires_python = ">=3.7" +summary = "Virtual Python Environment builder" +groups = ["test"] +dependencies = [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.12.2", + "importlib-metadata>=6.6; python_version < \"3.8\"", + "platformdirs<5,>=3.9.1", +] +files = [ + {file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"}, + {file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"}, +] + +[[package]] +name = "wheel" +version = "0.44.0" +requires_python = ">=3.8" +summary = "A built-package format for Python" +groups = ["test"] +files = [ + {file = "wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f"}, + {file = "wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49"}, +] + +[[package]] +name = "wrapt" +version = "1.16.0" +requires_python = ">=3.6" +summary = "Module for decorators, wrappers and monkey patching." +groups = ["test"] +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + +[[package]] +name = "zipp" +version = "3.20.1" +requires_python = ">=3.8" +summary = "Backport of pathlib-compatible object wrapper for zip files" +groups = ["default", "test"] +files = [ + {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, + {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, +] diff --git a/datajunction-query/pyproject.toml b/datajunction-query/pyproject.toml new file mode 100644 index 000000000..ef692d6d8 --- /dev/null +++ b/datajunction-query/pyproject.toml @@ -0,0 +1,108 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +requires-python = ">=3.10,<4.0" +name = "datajunction-query" +dynamic = ["version"] +description = "OSS Implementation of a DataJunction Query Service" +authors = [ + {name = "DataJunction Authors", email = "roberto@dealmeida.net"}, +] +dependencies = [ + "importlib-metadata", + "accept-types==0.4.1", + "cachelib>=0.4.0", + "duckdb==0.8.1", + "duckdb-engine", + "fastapi>=0.79.0", + "msgpack>=1.0.3", + "python-dotenv==0.19.2", + "requests<=2.29.0,>=2.28.2", + "rich>=10.16.2", + "toml>=0.10.2", + "snowflake-connector-python>=3.3.1", + "pyyaml>=6.0.1", + "trino>=0.324.0", + "psycopg[async,pool]>=3.2.1", + "sqlalchemy>=2.0.34", + "pytest-asyncio>=0.24.0", + "pytest-integration>=0.2.3", + "tenacity>=9.0.0", +] +readme = "README.rst" +license = {text = "MIT"} +classifiers = [ + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[tool.hatch.build.targets.wheel] +packages = ["djqs"] + + + + + +[tool.pdm] +[tool.pdm.build] +includes = ["djqs"] + +[tool.pdm.dev-dependencies] +test = [ + "alembic>=1.7.7", + "codespell>=2.1.0", + "freezegun>=1.1.0", + "pre-commit>=3.2.2", + "pyfakefs>=4.5.1", + "pylint>=2.15.3", + "pytest-cov>=2.12.1", + "pytest-mock>=3.6.1", + "pytest>=6.2.5", + "requests-mock>=1.9.3", + "setuptools>=49.6.0", + "pip-tools>=6.4.0", + "pydruid>=0.6.4", + "typing-extensions>=4.3.0", + "httpx>=0.24.1", + "psycopg[async,pool]>=3.2.1", + "testcontainers[postgres]>=4.8.1", +] + +[tool.pdm.scripts] +pytest = { cmd = "pytest", env = { "CONFIGURATION_FILE" = "./tests/config.djqs.yml", "INDEX" = "postgresql://dj:dj@localhost:4321/dj", "DEFAULT_ENGINE" = "duckdb_inmemory", "DEFAULT_ENGINE_VERSION" = "0.7.1" } } + +[project.optional-dependencies] +uvicorn = [ + "uvicorn[standard]>=0.21.1", +] + + + + + +[tool.hatch.version] +path = "djqs/__about__.py" + +[project.urls] +repository = "https://github.com/DataJunction/dj" + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.coverage.run] +source = ['djqs/'] +omit = [ + "djqs/config.py", + "djqs/exceptions.py" +] + +[tool.isort] +src_paths = ["djqs/", "tests/"] +profile = 'black' diff --git a/datajunction-query/scripts/generate-openapi.py b/datajunction-query/scripts/generate-openapi.py new file mode 100755 index 000000000..ef420e609 --- /dev/null +++ b/datajunction-query/scripts/generate-openapi.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# pylint: skip-file + +import argparse +import json + +from djqs.api.main import app + + +def save_openapi_spec(f: str): + spec = app.openapi() + with open(f, "w") as outfile: + outfile.write(json.dumps(spec, indent=4)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Generate a file containing the OpenAPI spec for a DJ server", + ) + parser.add_argument( + "-o", + "--output-file", + dest="filename", + required=True, + metavar="FILE", + ) + args = vars(parser.parse_args()) + save_openapi_spec(f=args["filename"]) diff --git a/datajunction-query/setup.cfg b/datajunction-query/setup.cfg new file mode 100644 index 000000000..4e278c7e1 --- /dev/null +++ b/datajunction-query/setup.cfg @@ -0,0 +1,19 @@ +# This file is used to configure your project. +# Read more about the various options under: +# https://setuptools.pypa.io/en/latest/userguide/declarative_config.html +# https://setuptools.pypa.io/en/latest/references/keywords.html + +[tool:pytest] +# Specify command line options as you would do when invoking pytest directly. +# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml +# in order to write a coverage file that can be read by Jenkins. +# CAUTION: --cov flags may prohibit setting breakpoints while debugging. +# Comment those flags to avoid this pytest issue. +addopts = + --cov djqs --cov-report term-missing + --verbose +norecursedirs = + dist + build + .tox +testpaths = tests diff --git a/datajunction-query/tests/__init__.py b/datajunction-query/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-query/tests/api/__init__.py b/datajunction-query/tests/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-query/tests/api/queries_test.py b/datajunction-query/tests/api/queries_test.py new file mode 100644 index 000000000..806a2c33e --- /dev/null +++ b/datajunction-query/tests/api/queries_test.py @@ -0,0 +1,651 @@ +""" +Tests for the queries API. +""" +import datetime +import json +from dataclasses import asdict +from http import HTTPStatus +from unittest import mock + +import msgpack +from fastapi.testclient import TestClient +from freezegun import freeze_time +from pytest_mock import MockerFixture + +from djqs.config import Settings +from djqs.engine import process_query +from djqs.models.query import ( + Query, + QueryCreate, + QueryState, + StatementResults, + decode_results, + encode_results, +) +from djqs.utils import get_settings + + +def test_submit_query_default_engine(client: TestClient) -> None: + """ + Test ``POST /queries/``. + """ + query_create = QueryCreate( + catalog_name="warehouse_inmemory", + submitted_query="SELECT 1 AS col", + ) + payload = json.dumps(asdict(query_create)) + assert payload == json.dumps( + { + "catalog_name": "warehouse_inmemory", + "engine_name": None, + "engine_version": None, + "submitted_query": "SELECT 1 AS col", + "async_": False, + }, + ) + + with freeze_time("2021-01-01T00:00:00Z"): + response = client.post( + "/queries/", + data=payload, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + ) + data = response.json() + + assert response.status_code == 200 + assert data["catalog_name"] == "warehouse_inmemory" + assert data["engine_name"] == "duckdb_inmemory" + assert data["engine_version"] == "0.7.1" + assert data["submitted_query"] == "SELECT 1 AS col" + assert data["executed_query"] == "SELECT 1 AS col" + assert data["scheduled"] == "2021-01-01 00:00:00+00:00" + assert data["started"] == "2021-01-01 00:00:00+00:00" + assert data["finished"] == "2021-01-01 00:00:00+00:00" + assert data["state"] == QueryState.FINISHED.value + assert data["progress"] == 1.0 + assert len(data["results"]) == 1 + assert data["results"][0]["sql"] == "SELECT 1 AS col" + assert data["results"][0]["rows"] == [[1]] + assert data["errors"] == [] + + +def test_submit_query(client: TestClient) -> None: + """ + Test ``POST /queries/``. + """ + query_create = QueryCreate( + catalog_name="warehouse_inmemory", + engine_name="duckdb_inmemory", + engine_version="0.7.1", + submitted_query="SELECT 1 AS col", + ) + payload = json.dumps(asdict(query_create)) + assert payload == json.dumps( + { + "catalog_name": "warehouse_inmemory", + "engine_name": "duckdb_inmemory", + "engine_version": "0.7.1", + "submitted_query": "SELECT 1 AS col", + "async_": False, + }, + ) + + with freeze_time("2021-01-01T00:00:00Z"): + response = client.post( + "/queries/", + data=payload, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + ) + data = response.json() + + assert response.status_code == 200 + assert data["catalog_name"] == "warehouse_inmemory" + assert data["engine_name"] == "duckdb_inmemory" + assert data["engine_version"] == "0.7.1" + assert data["submitted_query"] == "SELECT 1 AS col" + assert data["executed_query"] == "SELECT 1 AS col" + assert data["scheduled"] == "2021-01-01 00:00:00+00:00" + assert data["started"] == "2021-01-01 00:00:00+00:00" + assert data["finished"] == "2021-01-01 00:00:00+00:00" + assert data["state"] == QueryState.FINISHED.value + assert data["progress"] == 1.0 + assert len(data["results"]) == 1 + assert data["results"][0]["sql"] == "SELECT 1 AS col" + assert data["results"][0]["rows"] == [[1]] + assert data["errors"] == [] + + +def test_submit_query_generic_sqlalchemy(client: TestClient) -> None: + """ + Test ``POST /queries/``. + """ + query_create = QueryCreate( + catalog_name="sqlite_warehouse", + engine_name="sqlite_inmemory", + engine_version="1.0", + submitted_query="SELECT 1 AS col", + ) + payload = json.dumps(asdict(query_create)) + assert payload == json.dumps( + { + "catalog_name": "sqlite_warehouse", + "engine_name": "sqlite_inmemory", + "engine_version": "1.0", + "submitted_query": "SELECT 1 AS col", + "async_": False, + }, + ) + + with freeze_time("2021-01-01T00:00:00Z"): + response = client.post( + "/queries/", + data=payload, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + ) + data = response.json() + + assert response.status_code == 200 + assert data["catalog_name"] == "sqlite_warehouse" + assert data["engine_name"] == "sqlite_inmemory" + assert data["engine_version"] == "1.0" + assert data["submitted_query"] == "SELECT 1 AS col" + assert data["executed_query"] == "SELECT 1 AS col" + assert data["scheduled"] == "2021-01-01 00:00:00+00:00" + assert data["started"] == "2021-01-01 00:00:00+00:00" + assert data["finished"] == "2021-01-01 00:00:00+00:00" + assert data["state"] == QueryState.FINISHED.value + assert data["progress"] == 1.0 + assert len(data["results"]) == 1 + assert data["results"][0]["sql"] == "SELECT 1 AS col" + assert data["results"][0]["rows"] == [[1]] + assert data["errors"] == [] + + +def test_submit_query_with_sqlalchemy_uri_header( + client: TestClient, +) -> None: + """ + Test ``POST /queries/`` with the SQLALCHEMY_URI defined in the header. + """ + query_create = QueryCreate( + catalog_name="warehouse_inmemory", + engine_name="duckdb_inmemory", + engine_version="0.7.1", + submitted_query="SELECT 1 AS col", + ) + payload = json.dumps(asdict(query_create)) + assert payload == json.dumps( + { + "catalog_name": "warehouse_inmemory", + "engine_name": "duckdb_inmemory", + "engine_version": "0.7.1", + "submitted_query": "SELECT 1 AS col", + "async_": False, + }, + ) + + with freeze_time("2021-01-01T00:00:00Z"): + response = client.post( + "/queries/", + data=payload, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + "SQLALCHEMY_URI": "trino://example@foo.bar/catalog/schema", + }, + ) + data = response.json() + + assert response.status_code == 200 + assert data["catalog_name"] == "warehouse_inmemory" + assert data["engine_name"] == "duckdb_inmemory" + assert data["engine_version"] == "0.7.1" + assert data["submitted_query"] == "SELECT 1 AS col" + assert data["executed_query"] == "SELECT 1 AS col" + assert data["scheduled"] == "2021-01-01 00:00:00+00:00" + assert data["started"] == "2021-01-01 00:00:00+00:00" + assert data["finished"] == "2021-01-01 00:00:00+00:00" + assert data["state"] == QueryState.FINISHED.value + assert data["progress"] == 1.0 + assert len(data["results"]) == 1 + assert data["results"][0]["sql"] == "SELECT 1 AS col" + assert data["results"][0]["rows"] == [[1]] + assert data["errors"] == [] + + +def test_submit_query_msgpack(client: TestClient) -> None: + """ + Test ``POST /queries/`` using msgpack. + """ + query_create = QueryCreate( + catalog_name="warehouse_inmemory", + engine_name="duckdb_inmemory", + engine_version="0.7.1", + submitted_query="SELECT 1 AS col", + ) + payload = json.dumps(asdict(query_create)) + data = msgpack.packb(payload, default=encode_results) + + with freeze_time("2021-01-01T00:00:00Z"): + response = client.post( + "/queries/", + data=data, + headers={ + "Content-Type": "application/msgpack", + "Accept": "application/msgpack; q=1.0, application/json; q=0.5", + }, + ) + data = msgpack.unpackb(response.content, ext_hook=decode_results) + + assert response.headers.get("content-type") == "application/msgpack" + assert response.status_code == 200 + assert data["catalog_name"] == "warehouse_inmemory" + assert data["engine_name"] == "duckdb_inmemory" + assert data["engine_version"] == "0.7.1" + assert data["submitted_query"] == "SELECT 1 AS col" + assert data["executed_query"] == "SELECT 1 AS col" + assert data["scheduled"] == datetime.datetime( + 2021, + 1, + 1, + tzinfo=datetime.timezone.utc, + ) + assert data["started"] == datetime.datetime( + 2021, + 1, + 1, + tzinfo=datetime.timezone.utc, + ) + assert data["finished"] == datetime.datetime( + 2021, + 1, + 1, + tzinfo=datetime.timezone.utc, + ) + assert data["state"] == QueryState.FINISHED.value + assert data["progress"] == 1.0 + assert len(data["results"]) == 1 + assert data["results"][0]["sql"] == "SELECT 1 AS col" + assert data["results"][0]["rows"] == [[1]] + assert data["errors"] == [] + + +def test_submit_query_errors( + client: TestClient, +) -> None: + """ + Test ``POST /queries/`` with missing/invalid content type. + """ + query_create = QueryCreate( + catalog_name="warehouse_inmemory", + engine_name="duckdb_inmemory", + engine_version="0.7.1", + submitted_query="SELECT 1 AS col", + ) + payload = json.dumps(asdict(query_create)) + + response = client.post( + "/queries/", + data=payload, + headers={"Accept": "application/json"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Content type must be specified"} + + response = client.post( + "/queries/", + data=payload, + headers={ + "Content-Type": "application/protobuf", + "Accept": "application/json", + }, + ) + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + assert response.json() == { + "detail": "Content type not accepted: application/protobuf", + } + + response = client.post( + "/queries/", + data=payload, + headers={"Content-Type": "application/json", "Accept": "application/protobuf"}, + ) + assert response.status_code == 406 + assert response.json() == { + "detail": "Client MUST accept: application/json, application/msgpack", + } + + +def test_submit_query_multiple_statements( + client: TestClient, +) -> None: + """ + Test ``POST /queries/``. + """ + query_create = QueryCreate( + catalog_name="warehouse_inmemory", + engine_name="duckdb_inmemory", + engine_version="0.7.1", + submitted_query="SELECT 1 AS col; SELECT 2 AS another_col", + ) + + payload = json.dumps(asdict(query_create)) + with freeze_time("2021-01-01T00:00:00Z"): + response = client.post( + "/queries/", + data=payload, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + ) + data = response.json() + + assert response.status_code == 200 + assert data["catalog_name"] == "warehouse_inmemory" + assert data["engine_name"] == "duckdb_inmemory" + assert data["engine_version"] == "0.7.1" + assert data["submitted_query"] == "SELECT 1 AS col; SELECT 2 AS another_col" + assert data["executed_query"] == "SELECT 1 AS col; SELECT 2 AS another_col" + assert data["scheduled"] == "2021-01-01 00:00:00+00:00" + assert data["started"] == "2021-01-01 00:00:00+00:00" + assert data["finished"] == "2021-01-01 00:00:00+00:00" + assert data["state"] == QueryState.FINISHED.value + assert data["progress"] == 1.0 + assert len(data["results"]) == 1 + assert data["results"][0]["sql"] == "SELECT 1 AS col; SELECT 2 AS another_col" + assert data["results"][0]["rows"] == [[2]] + assert data["errors"] == [] + + +def test_submit_query_results_backend( + client: TestClient, +) -> None: + """ + Test that ``POST /queries/`` stores results. + """ + query_create = QueryCreate( + catalog_name="warehouse_inmemory", + engine_name="duckdb_inmemory", + engine_version="0.7.1", + submitted_query="SELECT 1 AS col", + ) + payload = json.dumps(asdict(query_create)) + with freeze_time("2021-01-01T00:00:00Z"): + response = client.post( + "/queries/", + data=payload, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + ) + data = response.json() + assert data == { + "catalog_name": "warehouse_inmemory", + "engine_name": "duckdb_inmemory", + "engine_version": "0.7.1", + "id": mock.ANY, + "submitted_query": "SELECT 1 AS col", + "executed_query": "SELECT 1 AS col", + "scheduled": "2021-01-01 00:00:00+00:00", + "started": "2021-01-01 00:00:00+00:00", + "finished": "2021-01-01 00:00:00+00:00", + "state": QueryState.FINISHED.value, + "progress": 1.0, + "results": [ + { + "sql": "SELECT 1 AS col", + "columns": mock.ANY, + "rows": [[1]], + "row_count": 1, + }, + ], + "next": None, + "previous": None, + "errors": [], + "async_": False, + } + settings = get_settings() + cached = settings.results_backend.get(data["id"]) + assert json.loads(cached) == [ + { + "sql": "SELECT 1 AS col", + "columns": mock.ANY, + "rows": [[1]], + "row_count": 1, + }, + ] + + +def test_submit_query_async( + mocker: MockerFixture, + client: TestClient, +) -> None: + """ + Test ``POST /queries/`` on an async database. + """ + add_task = mocker.patch("fastapi.BackgroundTasks.add_task") + query_create = QueryCreate( + catalog_name="warehouse_inmemory", + engine_name="duckdb_inmemory", + engine_version="0.7.1", + submitted_query="SELECT 1 AS col", + async_=True, + ) + payload = json.dumps(asdict(query_create)) + response = client.post( + "/queries/", + data=payload, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + ) + data = response.json() + + assert response.status_code == 201 + assert data["catalog_name"] == "warehouse_inmemory" + assert data["engine_name"] == "duckdb_inmemory" + assert data["engine_version"] == "0.7.1" + assert data["submitted_query"] == "SELECT 1 AS col" + assert data["executed_query"] is None + assert data["scheduled"] is None + assert data["started"] is None + assert data["finished"] is None + assert data["state"] == QueryState.SCHEDULED.value + assert data["progress"] == 0.0 + assert data["results"] == [] + assert data["errors"] == [] + + # check that ``BackgroundTasks.add_task`` was called + add_task.assert_called() + arguments = add_task.mock_calls[0].args + assert arguments[0] == process_query # pylint: disable=comparison-with-callable + assert isinstance(arguments[1], Settings) + assert isinstance(arguments[3], Query) + + +def test_submit_query_error(client: TestClient) -> None: + """ + Test submitting invalid query to ``POST /queries/``. + """ + query_create = QueryCreate( + catalog_name="warehouse_inmemory", + engine_name="duckdb_inmemory", + engine_version="0.7.1", + submitted_query="SELECT FROM", + async_=False, + ) + + payload = json.dumps(asdict(query_create)) + response = client.post( + "/queries/", + data=payload, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + ) + data = response.json() + + assert response.status_code == 200 + assert data["catalog_name"] == "warehouse_inmemory" + assert data["engine_name"] == "duckdb_inmemory" + assert data["engine_version"] == "0.7.1" + assert data["submitted_query"] == "SELECT FROM" + assert data["executed_query"] == "SELECT FROM" + assert data["state"] == QueryState.FAILED.value + assert data["progress"] == 0.0 + assert data["results"] == [] + assert "Parser Error: syntax error at end of input" in data["errors"][0] + + +def test_read_query(client: TestClient) -> None: + """ + Test ``GET /queries/{query_id}``. + """ + query_create = QueryCreate( + catalog_name="warehouse_inmemory", + engine_name="duckdb_inmemory", + engine_version="0.7.1", + submitted_query="SELECT 1 AS col", + ) + payload = json.dumps(asdict(query_create)) + assert payload == json.dumps( + { + "catalog_name": "warehouse_inmemory", + "engine_name": "duckdb_inmemory", + "engine_version": "0.7.1", + "submitted_query": "SELECT 1 AS col", + "async_": False, + }, + ) + + with freeze_time("2021-01-01T00:00:00Z"): + response = client.post( + "/queries/", + data=payload, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + ) + data = response.json() + + results = [ + StatementResults( + sql="SELECT 2 as foo", + columns=[{"name": "foo", "type": "STR"}], # type: ignore + rows=[[2]], # type: ignore + ), + ] + settings = get_settings() + settings.results_backend.set( + str(data["id"]), + json.dumps([asdict(result) for result in results]), + ) + response = client.get(f"/queries/{data['id']}") + data = response.json() + + assert response.status_code == 200 + + # Make sure the results are pulling from the cache, evidenced by the fact that it pulled + # the data that was manually set in the cache for this query ID + assert len(data["results"]) == 1 + assert data["results"][0]["sql"] == "SELECT 2 as foo" + assert data["results"][0]["columns"] == [{"name": "foo", "type": "STR"}] + assert data["results"][0]["rows"] == [[2]] + assert data["errors"] == [] + + response = client.get("/queries/27289db6-a75c-47fc-b451-da59a743a168") + assert response.status_code == 404 + + response = client.get("/queries/123") + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + + +def test_submit_duckdb_query(client: TestClient) -> None: + """ + Test submitting a duckdb query + """ + query_create = QueryCreate( + catalog_name="warehouse_inmemory", + engine_name="duckdb_inmemory", + engine_version="0.7.1", + submitted_query="SELECT 1 AS col", + ) + payload = json.dumps(asdict(query_create)) + assert payload == json.dumps( + { + "catalog_name": "warehouse_inmemory", + "engine_name": "duckdb_inmemory", + "engine_version": "0.7.1", + "submitted_query": "SELECT 1 AS col", + "async_": False, + }, + ) + + with freeze_time("2021-01-01T00:00:00Z"): + response = client.post( + "/queries/", + data=payload, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + ) + data = response.json() + + assert response.status_code == 200 + assert data["catalog_name"] == "warehouse_inmemory" + assert data["engine_name"] == "duckdb_inmemory" + assert data["engine_version"] == "0.7.1" + assert data["submitted_query"] == "SELECT 1 AS col" + assert data["executed_query"] == "SELECT 1 AS col" + assert data["scheduled"] == "2021-01-01 00:00:00+00:00" + assert data["started"] == "2021-01-01 00:00:00+00:00" + assert data["finished"] == "2021-01-01 00:00:00+00:00" + assert data["state"] == QueryState.FINISHED.value + assert data["progress"] == 1.0 + assert len(data["results"]) == 1 + assert data["results"][0]["sql"] == "SELECT 1 AS col" + assert data["results"][0]["rows"] == [[1]] + assert data["errors"] == [] + + +@mock.patch("djqs.engine.snowflake.connector") +def test_submit_snowflake_query( + mock_snowflake_connect, + client: TestClient, +) -> None: + """ + Test submitting a Snowflake query + """ + mock_exec = mock.MagicMock() + mock_exec.fetchall.return_value = [[1, "a"]] + mock_cur = mock.MagicMock() + mock_cur.execute.return_value = mock_exec + mock_conn = mock.MagicMock() + mock_conn.cursor.return_value = mock_cur + mock_snowflake_connect.connect.return_value = mock_conn + + query_create = QueryCreate( + catalog_name="snowflake_warehouse", + engine_name="snowflake_test", + engine_version="7.37", + submitted_query="SELECT 1 AS int_col, 'a' as str_col", + ) + payload = json.dumps(asdict(query_create)) + assert payload == json.dumps( + { + "catalog_name": "snowflake_warehouse", + "engine_name": "snowflake_test", + "engine_version": "7.37", + "submitted_query": "SELECT 1 AS int_col, 'a' as str_col", + "async_": False, + }, + ) + + with freeze_time("2021-01-01T00:00:00Z"): + response = client.post( + "/queries/", + data=payload, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + ) + data = response.json() + + assert response.status_code == 200 + assert data["catalog_name"] == "snowflake_warehouse" + assert data["engine_name"] == "snowflake_test" + assert data["engine_version"] == "7.37" + assert data["submitted_query"] == "SELECT 1 AS int_col, 'a' as str_col" + assert data["executed_query"] == "SELECT 1 AS int_col, 'a' as str_col" + assert data["scheduled"] == "2021-01-01 00:00:00+00:00" + assert data["started"] == "2021-01-01 00:00:00+00:00" + assert data["finished"] == "2021-01-01 00:00:00+00:00" + assert data["state"] == QueryState.FINISHED.value + assert data["progress"] == 1.0 + assert data["errors"] == [] diff --git a/datajunction-query/tests/api/table_test.py b/datajunction-query/tests/api/table_test.py new file mode 100644 index 000000000..65f0ff739 --- /dev/null +++ b/datajunction-query/tests/api/table_test.py @@ -0,0 +1,85 @@ +""" +Tests for the catalog API. +""" +from fastapi.testclient import TestClient + + +def test_table_columns(client: TestClient, mocker): + """ + Test getting table columns + """ + columns = [ + {"name": "col_a", "type": "STR"}, + {"name": "col_b", "type": "INT"}, + {"name": "col_c", "type": "MAP"}, + {"name": "col_d", "type": "STR"}, + {"name": "col_e", "type": "DECIMAL"}, + ] + mocker.patch("djqs.api.tables.get_columns", return_value=columns) + response = client.get( + "/table/foo.bar.baz/columns/?engine=duckdb_inmemory&engine_version=0.7.1", + ) + assert response.json() == { + "name": "foo.bar.baz", + "columns": [ + {"name": "col_a", "type": "STR"}, + {"name": "col_b", "type": "INT"}, + {"name": "col_c", "type": "MAP"}, + {"name": "col_d", "type": "STR"}, + {"name": "col_e", "type": "DECIMAL"}, + ], + } + + +def test_table_columns_w_default_engine(client: TestClient, mocker): + """ + Test getting table columns using the default engine + """ + columns = [ + {"name": "col_a", "type": "STR"}, + {"name": "col_b", "type": "INT"}, + {"name": "col_c", "type": "MAP"}, + {"name": "col_d", "type": "STR"}, + {"name": "col_e", "type": "DECIMAL"}, + ] + mocker.patch("djqs.api.tables.get_columns", return_value=columns) + response = client.get( + "/table/foo.bar.baz/columns/", + ) + assert response.json() == { + "name": "foo.bar.baz", + "columns": [ + {"name": "col_a", "type": "STR"}, + {"name": "col_b", "type": "INT"}, + {"name": "col_c", "type": "MAP"}, + {"name": "col_d", "type": "STR"}, + {"name": "col_e", "type": "DECIMAL"}, + ], + } + + +def test_raise_on_invalid_table_name(client: TestClient): + """ + Test raising on invalid table names + """ + response = client.get("/table/foo.bar.baz.qux/columns/") + assert response.json() == { + "message": ( + "The provided table value `foo.bar.baz.qux` is invalid. " + "A valid value for `table` must be in the format " + "`..
`" + ), + "errors": [], + "warnings": [], + } + + response = client.get("/table/foo/columns/") + assert response.json() == { + "message": ( + "The provided table value `foo` is invalid. " + "A valid value for `table` must be in the format " + "`..
`" + ), + "errors": [], + "warnings": [], + } diff --git a/datajunction-query/tests/config.djqs.yml b/datajunction-query/tests/config.djqs.yml new file mode 100644 index 000000000..4e41d764a --- /dev/null +++ b/datajunction-query/tests/config.djqs.yml @@ -0,0 +1,32 @@ +engines: + - name: duckdb + version: 0.7.1 + type: duckdb + uri: duckdb:////code/docker/default.duckdb + extra_params: + location: /code/docker/default.duckdb + - name: duckdb_inmemory + version: 0.7.1 + type: duckdb + uri: "duckdb:///:memory:" + - name: snowflake_test + version: 7.37 + type: snowflake + uri: "snowflake:///" + - name: sqlite_inmemory + version: 1.0 + type: sqlalchemy + uri: "sqlite:///" +catalogs: + - name: warehouse + engines: + - duckdb + - name: warehouse_inmemory + engines: + - duckdb_inmemory + - name: snowflake_warehouse + engines: + - snowflake_test + - name: sqlite_warehouse + engines: + - sqlite_inmemory diff --git a/datajunction-query/tests/conftest.py b/datajunction-query/tests/conftest.py new file mode 100644 index 000000000..c2e771f45 --- /dev/null +++ b/datajunction-query/tests/conftest.py @@ -0,0 +1,85 @@ +""" +Fixtures for testing. +""" +# pylint: disable=redefined-outer-name, invalid-name +import logging +from typing import Iterator + +import psycopg +import pytest +from fastapi.testclient import TestClient +from testcontainers.core.waiting_utils import wait_for_logs +from testcontainers.postgres import PostgresContainer + +from djqs.api.main import app + +_logger = logging.getLogger(__name__) + + +@pytest.fixture(scope="session") +def postgres_container() -> PostgresContainer: + """ + Setup postgres container + """ + localhost_port = 4321 # The test container will be bound to localhost port 4321 + postgres = PostgresContainer( + image="postgres:latest", + username="dj", + password="dj", + dbname="dj", + port=5432, + driver="psycopg", + ).with_bind_ports(5432, localhost_port) + with postgres: + wait_for_logs( + postgres, + r"UTC \[1\] LOG: database system is ready to accept connections", + 10, + ) + + # Manually build the connection string + username = postgres.username + password = postgres.password + host = postgres.get_container_host_ip() + port = postgres.get_exposed_port(postgres.port) + dbname = postgres.dbname + + connection_url = f"postgresql://{username}:{password}@{host}:{port}/{dbname}" + + with psycopg.connect( # pylint: disable=not-context-manager + connection_url, + ) as conn: + with conn.cursor() as cur: + _logger.info("Creating query table") + cur.execute( + """ + CREATE TABLE query ( + id UUID PRIMARY KEY, + catalog_name VARCHAR NOT NULL, + engine_name VARCHAR NOT NULL, + engine_version VARCHAR NOT NULL, + submitted_query VARCHAR NOT NULL, + async_ BOOLEAN NOT NULL, + executed_query VARCHAR, + scheduled TIMESTAMP, + started TIMESTAMP, + finished TIMESTAMP, + state VARCHAR NOT NULL, + progress FLOAT NOT NULL + ) + """, + ) + conn.commit() + _logger.info("Creating table created") + yield postgres + + +@pytest.fixture(scope="session") +def client( # pylint: disable=unused-argument + postgres_container, +) -> Iterator[TestClient]: + """ + Create a client for testing APIs. + """ + with TestClient(app) as client: + yield client diff --git a/datajunction-query/tests/exceptions_test.py b/datajunction-query/tests/exceptions_test.py new file mode 100644 index 000000000..097d97c4e --- /dev/null +++ b/datajunction-query/tests/exceptions_test.py @@ -0,0 +1,44 @@ +""" +Tests errors. +""" + +from http import HTTPStatus + +from djqs.exceptions import DJError, DJException, ErrorCode + + +def test_dj_exception() -> None: + """ + Test the base ``DJException``. + """ + exc = DJException() + assert exc.dbapi_exception == "Error" + assert exc.http_status_code == 500 + + exc = DJException(dbapi_exception="InternalError") + assert exc.dbapi_exception == "InternalError" + assert exc.http_status_code == 500 + + exc = DJException( + dbapi_exception="ProgrammingError", + http_status_code=HTTPStatus.BAD_REQUEST, + ) + assert exc.dbapi_exception == "ProgrammingError" + assert exc.http_status_code == HTTPStatus.BAD_REQUEST + + exc = DJException("Message") + assert str(exc) == "Message" + exc = DJException( + "Message", + errors=[ + DJError(message="Error 1", code=ErrorCode.UNKWNON_ERROR), + DJError(message="Error 2", code=ErrorCode.UNKWNON_ERROR), + ], + ) + assert ( + str(exc) + == """Message +The following errors happened: +- Error 1 (error code: 0) +- Error 2 (error code: 0)""" + ) diff --git a/datajunction-query/tests/resources/contractors.parquet b/datajunction-query/tests/resources/contractors.parquet new file mode 100644 index 000000000..436668e6c Binary files /dev/null and b/datajunction-query/tests/resources/contractors.parquet differ diff --git a/datajunction-query/tests/resources/dispatchers.parquet b/datajunction-query/tests/resources/dispatchers.parquet new file mode 100644 index 000000000..356f719ec Binary files /dev/null and b/datajunction-query/tests/resources/dispatchers.parquet differ diff --git a/datajunction-query/tests/resources/hard_hat_state.parquet b/datajunction-query/tests/resources/hard_hat_state.parquet new file mode 100644 index 000000000..59a5b654b Binary files /dev/null and b/datajunction-query/tests/resources/hard_hat_state.parquet differ diff --git a/datajunction-query/tests/resources/hard_hats.parquet b/datajunction-query/tests/resources/hard_hats.parquet new file mode 100644 index 000000000..d6bea8e90 Binary files /dev/null and b/datajunction-query/tests/resources/hard_hats.parquet differ diff --git a/datajunction-query/tests/resources/municipality.parquet b/datajunction-query/tests/resources/municipality.parquet new file mode 100644 index 000000000..08eed043b Binary files /dev/null and b/datajunction-query/tests/resources/municipality.parquet differ diff --git a/datajunction-query/tests/resources/municipality_municipality_type.parquet b/datajunction-query/tests/resources/municipality_municipality_type.parquet new file mode 100644 index 000000000..13c513e25 Binary files /dev/null and b/datajunction-query/tests/resources/municipality_municipality_type.parquet differ diff --git a/datajunction-query/tests/resources/municipality_type.parquet b/datajunction-query/tests/resources/municipality_type.parquet new file mode 100644 index 000000000..297cc25f4 Binary files /dev/null and b/datajunction-query/tests/resources/municipality_type.parquet differ diff --git a/datajunction-query/tests/resources/repair_order_details.parquet b/datajunction-query/tests/resources/repair_order_details.parquet new file mode 100644 index 000000000..265f2a8bb Binary files /dev/null and b/datajunction-query/tests/resources/repair_order_details.parquet differ diff --git a/datajunction-query/tests/resources/repair_orders.parquet b/datajunction-query/tests/resources/repair_orders.parquet new file mode 100644 index 000000000..703362916 Binary files /dev/null and b/datajunction-query/tests/resources/repair_orders.parquet differ diff --git a/datajunction-query/tests/resources/repair_type.parquet b/datajunction-query/tests/resources/repair_type.parquet new file mode 100644 index 000000000..c065d0d45 Binary files /dev/null and b/datajunction-query/tests/resources/repair_type.parquet differ diff --git a/datajunction-query/tests/resources/us_region.parquet b/datajunction-query/tests/resources/us_region.parquet new file mode 100644 index 000000000..08e31fa29 Binary files /dev/null and b/datajunction-query/tests/resources/us_region.parquet differ diff --git a/datajunction-query/tests/resources/us_states.parquet b/datajunction-query/tests/resources/us_states.parquet new file mode 100644 index 000000000..a1f4dd92a Binary files /dev/null and b/datajunction-query/tests/resources/us_states.parquet differ diff --git a/datajunction-query/tests/utils_test.py b/datajunction-query/tests/utils_test.py new file mode 100644 index 000000000..329cfe3cd --- /dev/null +++ b/datajunction-query/tests/utils_test.py @@ -0,0 +1,36 @@ +""" +Tests for ``djqs.utils``. +""" + +import logging + +import pytest +from pytest_mock import MockerFixture + +from djqs.utils import get_settings, setup_logging + + +def test_setup_logging() -> None: + """ + Test ``setup_logging``. + """ + setup_logging("debug") + assert logging.root.level == logging.DEBUG + + with pytest.raises(ValueError) as excinfo: + setup_logging("invalid") + assert str(excinfo.value) == "Invalid log level: invalid" + + +def test_get_settings(mocker: MockerFixture) -> None: + """ + Test ``get_settings``. + """ + mocker.patch("djqs.utils.load_dotenv") + Settings = mocker.patch( # pylint: disable=invalid-name, redefined-outer-name + "djqs.utils.Settings", + ) + + # should be already cached, since it's called by the Celery app + get_settings() + Settings.assert_not_called() diff --git a/datajunction-query/tox.ini b/datajunction-query/tox.ini new file mode 100644 index 000000000..f6c338a21 --- /dev/null +++ b/datajunction-query/tox.ini @@ -0,0 +1,14 @@ +[tox] +envlist = py3 + +[testenv] +pip_pre = true +deps = + -rrequirements/test.txt + pytest + testfixtures + coverage +commands = + pip install -e .[testing] + coverage run --source djqs --parallel-mode -m pytest {posargs} --without-integration --without-slow-integration + coverage html --fail-under 100 -d test-reports/{envname}/coverage-html diff --git a/datajunction-reflection/.coveragerc b/datajunction-reflection/.coveragerc new file mode 100644 index 000000000..d5c993481 --- /dev/null +++ b/datajunction-reflection/.coveragerc @@ -0,0 +1,31 @@ +# .coveragerc to control coverage.py +[run] +branch = True +source = djrs +# omit = bad_file.py + +[paths] +source = + djrs/ + */site-packages/ + +[report] +sort = -Cover +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + if TYPE_CHECKING: diff --git a/datajunction-reflection/.flake8 b/datajunction-reflection/.flake8 new file mode 100644 index 000000000..d9ad0b409 --- /dev/null +++ b/datajunction-reflection/.flake8 @@ -0,0 +1,5 @@ +[flake8] +ignore = E203, E266, E501, W503, F403, F401 +max-line-length = 79 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 diff --git a/datajunction-reflection/.gitignore b/datajunction-reflection/.gitignore new file mode 100644 index 000000000..b6e47617d --- /dev/null +++ b/datajunction-reflection/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/datajunction-reflection/.isort.cfg b/datajunction-reflection/.isort.cfg new file mode 100644 index 000000000..c00191213 --- /dev/null +++ b/datajunction-reflection/.isort.cfg @@ -0,0 +1,3 @@ +[settings] +profile = black +known_first_party = djrs diff --git a/datajunction-reflection/.pre-commit-config.yaml b/datajunction-reflection/.pre-commit-config.yaml new file mode 100644 index 000000000..a7e1051f6 --- /dev/null +++ b/datajunction-reflection/.pre-commit-config.yaml @@ -0,0 +1,101 @@ +files: ^datajunction-reflection/ + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + - id: check-ast + exclude: ^templates/ + - id: check-json + - id: check-merge-conflict + - id: check-xml + - id: check-yaml + - id: debug-statements + exclude: ^templates/ + - id: end-of-file-fixer + exclude: openapi.json + - id: requirements-txt-fixer + exclude: ^templates/ + - id: mixed-line-ending + args: ['--fix=auto'] # replace 'auto' with 'lf' to enforce Linux/Mac line endings or 'crlf' for Windows + +## If you want to avoid flake8 errors due to unused vars or imports: +# - repo: https://github.com/myint/autoflake.git +# rev: v1.4 +# hooks: +# - id: autoflake +# args: [ +# --in-place, +# --remove-all-unused-imports, +# --remove-unused-variables, +# ] + +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + +- repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black + language_version: python3 + exclude: ^templates/ + +## If like to embrace black styles even in the docs: +# - repo: https://github.com/asottile/blacken-docs +# rev: v1.9.1 +# hooks: +# - id: blacken-docs +# additional_dependencies: [black] + +- repo: https://github.com/PyCQA/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + exclude: ^templates/ + ## You can add flake8 plugins via `additional_dependencies`: + # additional_dependencies: [flake8-bugbear] + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v0.981' # Use the sha / tag you want to point at + hooks: + - id: mypy + exclude: ^templates/ + additional_dependencies: + - types-requests + - types-freezegun + - types-python-dateutil + - types-setuptools + - types-PyYAML + - types-tabulate +- repo: https://github.com/asottile/add-trailing-comma + rev: v2.2.1 + hooks: + - id: add-trailing-comma +#- repo: https://github.com/asottile/reorder_python_imports +# rev: v2.5.0 +# hooks: +# - id: reorder-python-imports +# args: [--application-directories=.:src] +## Removing this for now due to this bug: https://github.com/hadialqattan/pycln/issues/249 +# - repo: https://github.com/hadialqattan/pycln +# rev: v2.4.0 # Possible releases: https://github.com/hadialqattan/pycln/tags +# hooks: +# - id: pycln +# args: [--config=pyproject.toml] +# exclude: ^templates/ +- repo: local + hooks: + - id: pylint + name: pylint + entry: pylint --disable=duplicate-code,use-implicit-booleaness-not-comparison + language: system + types: [python] + exclude: ^templates/ +- repo: https://github.com/pdm-project/pdm + rev: 2.6.1 + hooks: + - id: pdm-lock-check diff --git a/datajunction-reflection/.pylintrc b/datajunction-reflection/.pylintrc new file mode 100644 index 000000000..5b6fccaa9 --- /dev/null +++ b/datajunction-reflection/.pylintrc @@ -0,0 +1,6 @@ +[MESSAGES CONTROL] + +[MASTER] +# https://github.com/samuelcolvin/pydantic/issues/1961#issuecomment-759522422 +extension-pkg-whitelist=pydantic +ignore=templates,docs diff --git a/datajunction-reflection/CODE_OF_CONDUCT.md b/datajunction-reflection/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..18c914718 --- /dev/null +++ b/datajunction-reflection/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/datajunction-reflection/Dockerfile b/datajunction-reflection/Dockerfile new file mode 100644 index 000000000..549a63b54 --- /dev/null +++ b/datajunction-reflection/Dockerfile @@ -0,0 +1,4 @@ +FROM python:3.10 +WORKDIR /code +COPY . /code +RUN pip install -e . diff --git a/datajunction-reflection/LICENSE b/datajunction-reflection/LICENSE new file mode 100644 index 000000000..208e34b3d --- /dev/null +++ b/datajunction-reflection/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 DJ + +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. diff --git a/datajunction-reflection/Makefile b/datajunction-reflection/Makefile new file mode 100644 index 000000000..7e708fe04 --- /dev/null +++ b/datajunction-reflection/Makefile @@ -0,0 +1,37 @@ +pyenv: .python-version + +.python-version: setup.cfg + if [ -z "`pyenv virtualenvs | grep djrs`" ]; then\ + pyenv virtualenv 3.10 djrs;\ + fi + if [ ! -f .python-version ]; then\ + pyenv local djrs;\ + fi + pdm install + touch .python-version + +docker-build: + docker build . + +test: pyenv + pdm run pytest --cov=datajunction_reflection -vv tests/ --doctest-modules datajunction_reflection --without-integration --without-slow-integration ${PYTEST_ARGS} + +integration: pyenv + pdm run pytest --cov=datajunction_reflection -vv tests/ --doctest-modules datajunction_reflection --with-integration --with-slow-integration + +clean: + pyenv virtualenv-delete djrs + +spellcheck: + codespell -L froms -S "*.json" datajunction_reflection docs/*rst tests templates + +check: + pdm run pre-commit run --all-files + +lint: + make check + +dev-release: + hatch version dev + hatch build + hatch publish diff --git a/datajunction-reflection/README.md b/datajunction-reflection/README.md new file mode 100644 index 000000000..d94696f3a --- /dev/null +++ b/datajunction-reflection/README.md @@ -0,0 +1,9 @@ +# DJ Reflection Service + +The reflection service polls the DJ core service for all nodes with associated tables, whether source +tables or materialized tables. For each node, it refreshes the node's schema based on the associated +table's schema that it retrieves from the query service. It also retrieves the available partitions and +the valid through timestamp of these tables and reflects them accordingly to DJ core. + +This service uses a celery beat scheduler, with a configurable polling interval that defaults to once per +hour and async tasks for each node's reflection. diff --git a/datajunction-reflection/datajunction_reflection/__about__.py b/datajunction-reflection/datajunction_reflection/__about__.py new file mode 100644 index 000000000..2788edf4b --- /dev/null +++ b/datajunction-reflection/datajunction_reflection/__about__.py @@ -0,0 +1,4 @@ +""" +Version for Hatch +""" +__version__ = "0.0.1a87" diff --git a/datajunction-reflection/datajunction_reflection/__init__.py b/datajunction-reflection/datajunction_reflection/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-reflection/datajunction_reflection/config.py b/datajunction-reflection/datajunction_reflection/config.py new file mode 100644 index 000000000..f291a753f --- /dev/null +++ b/datajunction-reflection/datajunction_reflection/config.py @@ -0,0 +1,26 @@ +"""Reflection service settings.""" +from functools import lru_cache + +from pydantic import BaseSettings + + +class Settings(BaseSettings): + """ + Default settings for the reflection service. + """ + + core_service: str = "http://dj:8000" + query_service: str = "http://djqs:8001" + celery_broker: str = "redis://djrs-redis:6379/1" + celery_results_backend: str = "redis://djrs-redis:6379/2" + + # Set the number of seconds to wait in between polling + polling_interval: int = 3600 + + +@lru_cache +def get_settings() -> Settings: + """ + Return a cached settings object. + """ + return Settings() diff --git a/datajunction-reflection/datajunction_reflection/worker/__init__.py b/datajunction-reflection/datajunction_reflection/worker/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-reflection/datajunction_reflection/worker/app.py b/datajunction-reflection/datajunction_reflection/worker/app.py new file mode 100644 index 000000000..ddfc523b5 --- /dev/null +++ b/datajunction-reflection/datajunction_reflection/worker/app.py @@ -0,0 +1,7 @@ +""" +Celery app that does the polling of nodes in DJ and then subsequent +queueing of reflection tasks. +""" +from datajunction_reflection.worker.utils import get_celery + +celery_app = get_celery() diff --git a/datajunction-reflection/datajunction_reflection/worker/tasks.py b/datajunction-reflection/datajunction_reflection/worker/tasks.py new file mode 100644 index 000000000..8b1339b3e --- /dev/null +++ b/datajunction-reflection/datajunction_reflection/worker/tasks.py @@ -0,0 +1,81 @@ +"""Reflection service celery tasks.""" +from abc import ABC + +import celery +import requests +from celery import shared_task +from celery.utils.log import get_task_logger + +from datajunction_reflection.worker.app import celery_app +from datajunction_reflection.worker.utils import get_settings + +logger = get_task_logger(__name__) + + +class ReflectionServiceTask(celery.Task, ABC): + """ + Base reflection service task. + """ + + abstract = True + + def on_failure(self, exc, task_id, *args, **kwargs): + logger.exception("%s failed: %s", task_id, exc) # pragma: no cover + + +@shared_task( + queue="celery", + name="datajunction_reflection.worker.app.refresh", + base=ReflectionServiceTask, +) +def refresh(): + """ + Find available DJ nodes and kick off reflection tasks for + nodes with associated tables. + """ + settings = get_settings() + response = requests.get( + f"{settings.core_service}/nodes/?node_type=source", + timeout=30, + ) + response.raise_for_status() + source_nodes = response.json() + + tasks = [] + for node_name in source_nodes: + task = celery_app.send_task( + "datajunction_reflection.worker.tasks.reflect_source", + (node_name,), + ) + tasks.append(task) + + +@shared_task( + queue="celery", + name="datajunction_reflection.worker.tasks.reflect_source", + base=ReflectionServiceTask, +) +def reflect_source( + node_name: str, +): + """ + This reflects the state of the node's associated table, whether + external or materialized, back to the DJ core service. + """ + logger.info(f"Refreshing source node={node_name} in DJ core") + settings = get_settings() + + # Call the source node's refresh endpoint + response = requests.post( + f"{settings.core_service}/nodes/{node_name}/refresh/", + timeout=30, + ) + + logger.info( + "Finished refreshing source node `%s`. Response: %s", + node_name, + response.reason, + ) + + # pylint: disable=fixme + # TODO: Post actual availability state when information available diff --git a/datajunction-reflection/datajunction_reflection/worker/utils.py b/datajunction-reflection/datajunction_reflection/worker/utils.py new file mode 100644 index 000000000..16555ffcc --- /dev/null +++ b/datajunction-reflection/datajunction_reflection/worker/utils.py @@ -0,0 +1,41 @@ +"""Utility functions for retrieving API clients.""" +import os + +from celery import Celery + +from datajunction_reflection.config import get_settings + + +def get_celery() -> Celery: + """ + core celery app + """ + + settings = get_settings() + + celery_app = Celery( + __name__, + include=[ + "datajunction_reflection.worker.app", + "datajunction_reflection.worker.tasks", + ], + ) + celery_app.conf.broker_url = os.environ.get( + "CELERY_BROKER_URL", + settings.celery_broker, + ) + celery_app.conf.result_backend = os.environ.get( + "CELERY_RESULT_BACKEND", + settings.celery_results_backend, + ) + celery_app.conf.imports = [ + "datajunction_reflection.worker.app", + "datajunction_reflection.worker.tasks", + ] + celery_app.conf.beat_schedule = { + "refresh": { + "task": "datajunction_reflection.worker.app.refresh", + "schedule": settings.polling_interval, + }, + } + return celery_app diff --git a/datajunction-reflection/pdm.lock b/datajunction-reflection/pdm.lock new file mode 100644 index 000000000..dc66a8cce --- /dev/null +++ b/datajunction-reflection/pdm.lock @@ -0,0 +1,1276 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "test"] +cross_platform = true +static_urls = false +lock_version = "4.2" +content_hash = "sha256:b691c0cb73bc65dcb812d3e361a3c78c0b4d9e20406c4ed47b04f8edb9e271b9" + +[[package]] +name = "amqp" +version = "5.1.1" +requires_python = ">=3.6" +summary = "Low-level AMQP client for Python (fork of amqplib)." +dependencies = [ + "vine>=5.0.0", +] +files = [ + {file = "amqp-5.1.1-py3-none-any.whl", hash = "sha256:6f0956d2c23d8fa6e7691934d8c3930eadb44972cbbd1a7ae3a520f735d43359"}, + {file = "amqp-5.1.1.tar.gz", hash = "sha256:2c1b13fecc0893e946c65cbd5f36427861cffa4ea2201d8f6fca22e2a373b5e2"}, +] + +[[package]] +name = "astroid" +version = "2.15.6" +requires_python = ">=3.7.2" +summary = "An abstract syntax tree for Python with inference support." +dependencies = [ + "lazy-object-proxy>=1.4.0", + "typing-extensions>=4.0.0; python_version < \"3.11\"", + "wrapt<2,>=1.11; python_version < \"3.11\"", + "wrapt<2,>=1.14; python_version >= \"3.11\"", +] +files = [ + {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"}, + {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"}, +] + +[[package]] +name = "async-timeout" +version = "4.0.2" +requires_python = ">=3.6" +summary = "Timeout context manager for asyncio programs" +files = [ + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, +] + +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +requires_python = ">=3.6" +summary = "Backport of the standard library zoneinfo module" +files = [ + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +extras = ["tzdata"] +requires_python = ">=3.6" +summary = "Backport of the standard library zoneinfo module" +dependencies = [ + "backports-zoneinfo==0.2.1", + "tzdata", +] +files = [ + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[[package]] +name = "billiard" +version = "4.1.0" +requires_python = ">=3.7" +summary = "Python multiprocessing fork with improvements and bugfixes" +files = [ + {file = "billiard-4.1.0-py3-none-any.whl", hash = "sha256:0f50d6be051c6b2b75bfbc8bfd85af195c5739c281d3f5b86a5640c65563614a"}, + {file = "billiard-4.1.0.tar.gz", hash = "sha256:1ad2eeae8e28053d729ba3373d34d9d6e210f6e4d8bf0a9c64f92bd053f1edf5"}, +] + +[[package]] +name = "build" +version = "0.10.0" +requires_python = ">= 3.7" +summary = "A simple, correct Python build frontend" +dependencies = [ + "colorama; os_name == \"nt\"", + "packaging>=19.0", + "pyproject-hooks", + "tomli>=1.1.0; python_version < \"3.11\"", +] +files = [ + {file = "build-0.10.0-py3-none-any.whl", hash = "sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171"}, + {file = "build-0.10.0.tar.gz", hash = "sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269"}, +] + +[[package]] +name = "celery" +version = "5.3.1" +requires_python = ">=3.8" +summary = "Distributed Task Queue." +dependencies = [ + "backports-zoneinfo>=0.2.1; python_version < \"3.9\"", + "billiard<5.0,>=4.1.0", + "click-didyoumean>=0.3.0", + "click-plugins>=1.1.1", + "click-repl>=0.2.0", + "click<9.0,>=8.1.2", + "kombu<6.0,>=5.3.1", + "python-dateutil>=2.8.2", + "tzdata>=2022.7", + "vine<6.0,>=5.0.0", +] +files = [ + {file = "celery-5.3.1-py3-none-any.whl", hash = "sha256:27f8f3f3b58de6e0ab4f174791383bbd7445aff0471a43e99cfd77727940753f"}, + {file = "celery-5.3.1.tar.gz", hash = "sha256:f84d1c21a1520c116c2b7d26593926581191435a03aa74b77c941b93ca1c6210"}, +] + +[[package]] +name = "celery" +version = "5.3.1" +extras = ["pytest"] +requires_python = ">=3.8" +summary = "Distributed Task Queue." +dependencies = [ + "celery==5.3.1", + "pytest-celery==0.0.0", +] +files = [ + {file = "celery-5.3.1-py3-none-any.whl", hash = "sha256:27f8f3f3b58de6e0ab4f174791383bbd7445aff0471a43e99cfd77727940753f"}, + {file = "celery-5.3.1.tar.gz", hash = "sha256:f84d1c21a1520c116c2b7d26593926581191435a03aa74b77c941b93ca1c6210"}, +] + +[[package]] +name = "celery" +version = "5.3.1" +extras = ["redis"] +requires_python = ">=3.8" +summary = "Distributed Task Queue." +dependencies = [ + "celery==5.3.1", + "redis!=4.5.5,>=4.5.2", +] +files = [ + {file = "celery-5.3.1-py3-none-any.whl", hash = "sha256:27f8f3f3b58de6e0ab4f174791383bbd7445aff0471a43e99cfd77727940753f"}, + {file = "celery-5.3.1.tar.gz", hash = "sha256:f84d1c21a1520c116c2b7d26593926581191435a03aa74b77c941b93ca1c6210"}, +] + +[[package]] +name = "certifi" +version = "2023.7.22" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "cfgv" +version = "3.3.1" +requires_python = ">=3.6.1" +summary = "Validate configuration and produce human readable error messages." +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.2.0" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +files = [ + {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, + {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, +] + +[[package]] +name = "click" +version = "8.1.6" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.0" +requires_python = ">=3.6.2,<4.0.0" +summary = "Enables git-like *did-you-mean* feature in click" +dependencies = [ + "click>=7", +] +files = [ + {file = "click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"}, + {file = "click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667"}, +] + +[[package]] +name = "click-plugins" +version = "1.1.1" +summary = "An extension module for click to enable registering CLI commands via setuptools entry-points." +dependencies = [ + "click>=4.0", +] +files = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +requires_python = ">=3.6" +summary = "REPL plugin for Click" +dependencies = [ + "click>=7.0", + "prompt-toolkit>=3.0.36", +] +files = [ + {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, + {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, +] + +[[package]] +name = "codespell" +version = "2.2.5" +requires_python = ">=3.7" +summary = "Codespell" +files = [ + {file = "codespell-2.2.5-py3-none-any.whl", hash = "sha256:efa037f54b73c84f7bd14ce8e853d5f822cdd6386ef0ff32e957a3919435b9ec"}, + {file = "codespell-2.2.5.tar.gz", hash = "sha256:6d9faddf6eedb692bf80c9a94ec13ab4f5fb585aabae5f3750727148d7b5be56"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.2.7" +requires_python = ">=3.7" +summary = "Code coverage measurement for Python" +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[[package]] +name = "coverage" +version = "7.2.7" +extras = ["toml"] +requires_python = ">=3.7" +summary = "Code coverage measurement for Python" +dependencies = [ + "coverage==7.2.7", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[[package]] +name = "dill" +version = "0.3.7" +requires_python = ">=3.7" +summary = "serialize all of Python" +files = [ + {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, + {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, +] + +[[package]] +name = "distlib" +version = "0.3.7" +summary = "Distribution utilities" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.2" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +files = [ + {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, + {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, +] + +[[package]] +name = "filelock" +version = "3.12.2" +requires_python = ">=3.7" +summary = "A platform independent file lock." +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] + +[[package]] +name = "freezegun" +version = "1.2.2" +requires_python = ">=3.6" +summary = "Let your Python tests travel through time" +dependencies = [ + "python-dateutil>=2.7", +] +files = [ + {file = "freezegun-1.2.2-py3-none-any.whl", hash = "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f"}, + {file = "freezegun-1.2.2.tar.gz", hash = "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446"}, +] + +[[package]] +name = "identify" +version = "2.5.26" +requires_python = ">=3.8" +summary = "File identification library for Python" +files = [ + {file = "identify-2.5.26-py2.py3-none-any.whl", hash = "sha256:c22a8ead0d4ca11f1edd6c9418c3220669b3b7533ada0a0ffa6cc0ef85cf9b54"}, + {file = "identify-2.5.26.tar.gz", hash = "sha256:7243800bce2f58404ed41b7c002e53d4d22bcf3ae1b7900c2d7aefd95394bf7f"}, +] + +[[package]] +name = "idna" +version = "3.4" +requires_python = ">=3.5" +summary = "Internationalized Domain Names in Applications (IDNA)" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.8.0" +requires_python = ">=3.8" +summary = "Read metadata from Python packages" +dependencies = [ + "zipp>=0.5", +] +files = [ + {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, + {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.12.0" +requires_python = ">=3.8.0" +summary = "A Python utility / library to sort Python imports." +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[[package]] +name = "kombu" +version = "5.3.1" +requires_python = ">=3.8" +summary = "Messaging library for Python." +dependencies = [ + "amqp<6.0.0,>=5.1.1", + "backports-zoneinfo[tzdata]>=0.2.1; python_version < \"3.9\"", + "typing-extensions; python_version < \"3.10\"", + "vine", +] +files = [ + {file = "kombu-5.3.1-py3-none-any.whl", hash = "sha256:48ee589e8833126fd01ceaa08f8a2041334e9f5894e5763c8486a550454551e9"}, + {file = "kombu-5.3.1.tar.gz", hash = "sha256:fbd7572d92c0bf71c112a6b45163153dea5a7b6a701ec16b568c27d0fd2370f2"}, +] + +[[package]] +name = "lazy-object-proxy" +version = "1.9.0" +requires_python = ">=3.7" +summary = "A fast and thorough lazy object proxy." +files = [ + {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +requires_python = ">=3.6" +summary = "McCabe checker, plugin for flake8" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +summary = "Node.js virtual environment builder" +dependencies = [ + "setuptools", +] +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[[package]] +name = "packaging" +version = "23.1" +requires_python = ">=3.7" +summary = "Core utilities for Python packages" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pip" +version = "23.2.1" +requires_python = ">=3.7" +summary = "The PyPA recommended tool for installing Python packages." +files = [ + {file = "pip-23.2.1-py3-none-any.whl", hash = "sha256:7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be"}, + {file = "pip-23.2.1.tar.gz", hash = "sha256:fb0bd5435b3200c602b5bf61d2d43c2f13c02e29c1707567ae7fbc514eb9faf2"}, +] + +[[package]] +name = "pip-tools" +version = "7.1.0" +requires_python = ">=3.8" +summary = "pip-tools keeps your pinned dependencies fresh." +dependencies = [ + "build", + "click>=8", + "pip>=22.2", + "setuptools", + "tomli; python_version < \"3.11\"", + "wheel", +] +files = [ + {file = "pip-tools-7.1.0.tar.gz", hash = "sha256:f6ead499e726c8cfee04b2dea6282a9faf29663c378d9a4aca2ea6b86c8ec715"}, + {file = "pip_tools-7.1.0-py3-none-any.whl", hash = "sha256:4e60b7d05b046f49ad5bf3c2818df8e78dec5820e9b331cd9898cff5ec19ff2f"}, +] + +[[package]] +name = "platformdirs" +version = "3.9.1" +requires_python = ">=3.7" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +files = [ + {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, + {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +requires_python = ">=3.7" +summary = "plugin and hook calling mechanisms for python" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[[package]] +name = "pre-commit" +version = "3.3.3" +requires_python = ">=3.8" +summary = "A framework for managing and maintaining multi-language pre-commit hooks." +dependencies = [ + "cfgv>=2.0.0", + "identify>=1.0.0", + "nodeenv>=0.11.1", + "pyyaml>=5.1", + "virtualenv>=20.10.0", +] +files = [ + {file = "pre_commit-3.3.3-py2.py3-none-any.whl", hash = "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb"}, + {file = "pre_commit-3.3.3.tar.gz", hash = "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023"}, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.39" +requires_python = ">=3.7.0" +summary = "Library for building powerful interactive command lines in Python" +dependencies = [ + "wcwidth", +] +files = [ + {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, + {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, +] + +[[package]] +name = "pydantic" +version = "1.10.11" +requires_python = ">=3.7" +summary = "Data validation and settings management using python type hints" +dependencies = [ + "typing-extensions>=4.2.0", +] +files = [ + {file = "pydantic-1.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ff44c5e89315b15ff1f7fdaf9853770b810936d6b01a7bcecaa227d2f8fe444f"}, + {file = "pydantic-1.10.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c098d4ab5e2d5b3984d3cb2527e2d6099d3de85630c8934efcfdc348a9760e"}, + {file = "pydantic-1.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16928fdc9cb273c6af00d9d5045434c39afba5f42325fb990add2c241402d151"}, + {file = "pydantic-1.10.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0588788a9a85f3e5e9ebca14211a496409cb3deca5b6971ff37c556d581854e7"}, + {file = "pydantic-1.10.11-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9baf78b31da2dc3d3f346ef18e58ec5f12f5aaa17ac517e2ffd026a92a87588"}, + {file = "pydantic-1.10.11-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:373c0840f5c2b5b1ccadd9286782852b901055998136287828731868027a724f"}, + {file = "pydantic-1.10.11-cp310-cp310-win_amd64.whl", hash = "sha256:c3339a46bbe6013ef7bdd2844679bfe500347ac5742cd4019a88312aa58a9847"}, + {file = "pydantic-1.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:08a6c32e1c3809fbc49debb96bf833164f3438b3696abf0fbeceb417d123e6eb"}, + {file = "pydantic-1.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a451ccab49971af043ec4e0d207cbc8cbe53dbf148ef9f19599024076fe9c25b"}, + {file = "pydantic-1.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b02d24f7b2b365fed586ed73582c20f353a4c50e4be9ba2c57ab96f8091ddae"}, + {file = "pydantic-1.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f34739a89260dfa420aa3cbd069fbcc794b25bbe5c0a214f8fb29e363484b66"}, + {file = "pydantic-1.10.11-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e297897eb4bebde985f72a46a7552a7556a3dd11e7f76acda0c1093e3dbcf216"}, + {file = "pydantic-1.10.11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d185819a7a059550ecb85d5134e7d40f2565f3dd94cfd870132c5f91a89cf58c"}, + {file = "pydantic-1.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:4400015f15c9b464c9db2d5d951b6a780102cfa5870f2c036d37c23b56f7fc1b"}, + {file = "pydantic-1.10.11-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2417de68290434461a266271fc57274a138510dca19982336639484c73a07af6"}, + {file = "pydantic-1.10.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:331c031ba1554b974c98679bd0780d89670d6fd6f53f5d70b10bdc9addee1713"}, + {file = "pydantic-1.10.11-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8268a735a14c308923e8958363e3a3404f6834bb98c11f5ab43251a4e410170c"}, + {file = "pydantic-1.10.11-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:44e51ba599c3ef227e168424e220cd3e544288c57829520dc90ea9cb190c3248"}, + {file = "pydantic-1.10.11-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d7781f1d13b19700b7949c5a639c764a077cbbdd4322ed505b449d3ca8edcb36"}, + {file = "pydantic-1.10.11-cp37-cp37m-win_amd64.whl", hash = "sha256:7522a7666157aa22b812ce14c827574ddccc94f361237ca6ea8bb0d5c38f1629"}, + {file = "pydantic-1.10.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc64eab9b19cd794a380179ac0e6752335e9555d214cfcb755820333c0784cb3"}, + {file = "pydantic-1.10.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8dc77064471780262b6a68fe67e013298d130414d5aaf9b562c33987dbd2cf4f"}, + {file = "pydantic-1.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe429898f2c9dd209bd0632a606bddc06f8bce081bbd03d1c775a45886e2c1cb"}, + {file = "pydantic-1.10.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:192c608ad002a748e4a0bed2ddbcd98f9b56df50a7c24d9a931a8c5dd053bd3d"}, + {file = "pydantic-1.10.11-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ef55392ec4bb5721f4ded1096241e4b7151ba6d50a50a80a2526c854f42e6a2f"}, + {file = "pydantic-1.10.11-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:41e0bb6efe86281623abbeeb0be64eab740c865388ee934cd3e6a358784aca6e"}, + {file = "pydantic-1.10.11-cp38-cp38-win_amd64.whl", hash = "sha256:265a60da42f9f27e0b1014eab8acd3e53bd0bad5c5b4884e98a55f8f596b2c19"}, + {file = "pydantic-1.10.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:469adf96c8e2c2bbfa655fc7735a2a82f4c543d9fee97bd113a7fb509bf5e622"}, + {file = "pydantic-1.10.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6cbfbd010b14c8a905a7b10f9fe090068d1744d46f9e0c021db28daeb8b6de1"}, + {file = "pydantic-1.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abade85268cc92dff86d6effcd917893130f0ff516f3d637f50dadc22ae93999"}, + {file = "pydantic-1.10.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9738b0f2e6c70f44ee0de53f2089d6002b10c33264abee07bdb5c7f03038303"}, + {file = "pydantic-1.10.11-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:787cf23e5a0cde753f2eabac1b2e73ae3844eb873fd1f5bdbff3048d8dbb7604"}, + {file = "pydantic-1.10.11-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:174899023337b9fc685ac8adaa7b047050616136ccd30e9070627c1aaab53a13"}, + {file = "pydantic-1.10.11-cp39-cp39-win_amd64.whl", hash = "sha256:1954f8778489a04b245a1e7b8b22a9d3ea8ef49337285693cf6959e4b757535e"}, + {file = "pydantic-1.10.11-py3-none-any.whl", hash = "sha256:008c5e266c8aada206d0627a011504e14268a62091450210eda7c07fabe6963e"}, + {file = "pydantic-1.10.11.tar.gz", hash = "sha256:f66d479cf7eb331372c470614be6511eae96f1f120344c25f3f9bb59fb1b5528"}, +] + +[[package]] +name = "pyfakefs" +version = "5.2.3" +requires_python = ">=3.7" +summary = "pyfakefs implements a fake file system that mocks the Python file system modules." +files = [ + {file = "pyfakefs-5.2.3-py3-none-any.whl", hash = "sha256:101a91d8e454934fe2435392c38d505beacfc27dad71a0ad2a6215d0b750f2f1"}, + {file = "pyfakefs-5.2.3.tar.gz", hash = "sha256:f4d677645e44c56fd47d579c7586ff0daef1546d3100df2af50969f794368fc6"}, +] + +[[package]] +name = "pylint" +version = "2.17.4" +requires_python = ">=3.7.2" +summary = "python code static checker" +dependencies = [ + "astroid<=2.17.0-dev0,>=2.15.4", + "colorama>=0.4.5; sys_platform == \"win32\"", + "dill>=0.2; python_version < \"3.11\"", + "dill>=0.3.6; python_version >= \"3.11\"", + "isort<6,>=4.2.5", + "mccabe<0.8,>=0.6", + "platformdirs>=2.2.0", + "tomli>=1.1.0; python_version < \"3.11\"", + "tomlkit>=0.10.1", + "typing-extensions>=3.10.0; python_version < \"3.10\"", +] +files = [ + {file = "pylint-2.17.4-py3-none-any.whl", hash = "sha256:7a1145fb08c251bdb5cca11739722ce64a63db479283d10ce718b2460e54123c"}, + {file = "pylint-2.17.4.tar.gz", hash = "sha256:5dcf1d9e19f41f38e4e85d10f511e5b9c35e1aa74251bf95cdd8cb23584e2db1"}, +] + +[[package]] +name = "pyproject-hooks" +version = "1.0.0" +requires_python = ">=3.7" +summary = "Wrappers to call pyproject.toml-based build backend hooks." +dependencies = [ + "tomli>=1.1.0; python_version < \"3.11\"", +] +files = [ + {file = "pyproject_hooks-1.0.0-py3-none-any.whl", hash = "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8"}, + {file = "pyproject_hooks-1.0.0.tar.gz", hash = "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"}, +] + +[[package]] +name = "pytest" +version = "7.4.0" +requires_python = ">=3.7" +summary = "pytest: simple powerful testing with Python" +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2.0,>=0.12", + "tomli>=1.0.0; python_version < \"3.11\"", +] +files = [ + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, +] + +[[package]] +name = "pytest-asyncio" +version = "0.15.1" +requires_python = ">= 3.6" +summary = "Pytest support for asyncio." +dependencies = [ + "pytest>=5.4.0", +] +files = [ + {file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"}, + {file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"}, +] + +[[package]] +name = "pytest-celery" +version = "0.0.0" +summary = "pytest-celery a shim pytest plugin to enable celery.contrib.pytest" +dependencies = [ + "celery>=4.4.0", +] +files = [ + {file = "pytest-celery-0.0.0.tar.gz", hash = "sha256:cfd060fc32676afa1e4f51b2938f903f7f75d952186b8c6cf631628c4088f406"}, + {file = "pytest_celery-0.0.0-py2.py3-none-any.whl", hash = "sha256:63dec132df3a839226ecb003ffdbb0c2cb88dd328550957e979c942766578060"}, +] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +requires_python = ">=3.7" +summary = "Pytest plugin for measuring coverage." +dependencies = [ + "coverage[toml]>=5.2.1", + "pytest>=4.6", +] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[[package]] +name = "pytest-freezegun" +version = "0.4.2" +summary = "Wrap tests with fixtures in freeze_time" +dependencies = [ + "freezegun>0.3", + "pytest>=3.0.0", +] +files = [ + {file = "pytest-freezegun-0.4.2.zip", hash = "sha256:19c82d5633751bf3ec92caa481fb5cffaac1787bd485f0df6436fd6242176949"}, + {file = "pytest_freezegun-0.4.2-py2.py3-none-any.whl", hash = "sha256:5318a6bfb8ba4b709c8471c94d0033113877b3ee02da5bfcd917c1889cde99a7"}, +] + +[[package]] +name = "pytest-integration" +version = "0.2.2" +requires_python = ">=3.6" +summary = "Organizing pytests by integration or not" +files = [ + {file = "pytest_integration-0.2.2-py3-none-any.whl", hash = "sha256:560b18c003cf6a3d6672878e826a823ea5f8d1d289dbe97546495040b2f0bd3d"}, + {file = "pytest_integration-0.2.2.tar.gz", hash = "sha256:7630b2bb1a8d518168bae44d827c20c4f0c1bbc5a1d3e1014dc5624ccadcdbd1"}, +] + +[[package]] +name = "pytest-mock" +version = "3.11.1" +requires_python = ">=3.7" +summary = "Thin-wrapper around the mock package for easier use with pytest" +dependencies = [ + "pytest>=5.0", +] +files = [ + {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, + {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, +] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[[package]] +name = "python-dotenv" +version = "0.19.2" +requires_python = ">=3.5" +summary = "Read key-value pairs from a .env file and set them as environment variables" +files = [ + {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, + {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +requires_python = ">=3.6" +summary = "YAML parser and emitter for Python" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "redis" +version = "4.6.0" +requires_python = ">=3.7" +summary = "Python client for Redis database and key-value store" +dependencies = [ + "async-timeout>=4.0.2; python_full_version <= \"3.11.2\"", +] +files = [ + {file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"}, + {file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +requires_python = ">=3.7" +summary = "Python HTTP for Humans." +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[[package]] +name = "requests-mock" +version = "1.11.0" +summary = "Mock out responses from the requests package" +dependencies = [ + "requests<3,>=2.3", + "six", +] +files = [ + {file = "requests-mock-1.11.0.tar.gz", hash = "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4"}, + {file = "requests_mock-1.11.0-py2.py3-none-any.whl", hash = "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15"}, +] + +[[package]] +name = "setuptools" +version = "68.0.0" +requires_python = ">=3.7" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tomlkit" +version = "0.11.8" +requires_python = ">=3.7" +summary = "Style preserving TOML library" +files = [ + {file = "tomlkit-0.11.8-py3-none-any.whl", hash = "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171"}, + {file = "tomlkit-0.11.8.tar.gz", hash = "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3"}, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +requires_python = ">=3.7" +summary = "Backported and Experimental Type Hints for Python 3.7+" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +requires_python = ">=2" +summary = "Provider of IANA time zone data" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "urllib3" +version = "2.0.4" +requires_python = ">=3.7" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +files = [ + {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, + {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, +] + +[[package]] +name = "vine" +version = "5.0.0" +requires_python = ">=3.6" +summary = "Promises, promises, promises." +files = [ + {file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"}, + {file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"}, +] + +[[package]] +name = "virtualenv" +version = "20.24.1" +requires_python = ">=3.7" +summary = "Virtual Python Environment builder" +dependencies = [ + "distlib<1,>=0.3.6", + "filelock<4,>=3.12", + "platformdirs<4,>=3.5.1", +] +files = [ + {file = "virtualenv-20.24.1-py3-none-any.whl", hash = "sha256:01aacf8decd346cf9a865ae85c0cdc7f64c8caa07ff0d8b1dfc1733d10677442"}, + {file = "virtualenv-20.24.1.tar.gz", hash = "sha256:2ef6a237c31629da6442b0bcaa3999748108c7166318d1f55cc9f8d7294e97bd"}, +] + +[[package]] +name = "wcwidth" +version = "0.2.6" +summary = "Measures the displayed width of unicode strings in a terminal" +files = [ + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, +] + +[[package]] +name = "wheel" +version = "0.41.0" +requires_python = ">=3.7" +summary = "A built-package format for Python" +files = [ + {file = "wheel-0.41.0-py3-none-any.whl", hash = "sha256:7e9be3bbd0078f6147d82ed9ed957e323e7708f57e134743d2edef3a7b7972a9"}, + {file = "wheel-0.41.0.tar.gz", hash = "sha256:55a0f0a5a84869bce5ba775abfd9c462e3a6b1b7b7ec69d72c0b83d673a5114d"}, +] + +[[package]] +name = "wrapt" +version = "1.15.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +summary = "Module for decorators, wrappers and monkey patching." +files = [ + {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, + {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, + {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, + {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, + {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, + {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, + {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, + {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, + {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +] + +[[package]] +name = "zipp" +version = "3.16.2" +requires_python = ">=3.8" +summary = "Backport of pathlib-compatible object wrapper for zip files" +files = [ + {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, + {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, +] diff --git a/datajunction-reflection/pyproject.toml b/datajunction-reflection/pyproject.toml new file mode 100644 index 000000000..d532e9ba9 --- /dev/null +++ b/datajunction-reflection/pyproject.toml @@ -0,0 +1,74 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.pdm.build] +includes = ["datajunction_reflection"] + +[project] +name = "datajunction-reflection" +dynamic = ["version"] +description = "OSS Implementation of a DataJunction Reflection Service" +authors = [ + {name = "DataJunction Authors", email = "roberto@dealmeida.net"}, +] +dependencies = [ + "importlib-metadata", + "celery[redis]>=5.2.3", + "python-dotenv==0.19.2", + "requests>=2.26.0", + "pydantic<2.0", +] +requires-python = ">=3.8,<4.0" +readme = "README.md" +license = {text = "MIT"} +classifiers = [ + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.optional-dependencies] +uvicorn = [ + "uvicorn[standard]>=0.21.1", +] + +[tool.hatch.version] +path = "datajunction_reflection/__about__.py" + +[project.urls] +repository = "https://github.com/DataJunction/dj" + +[tool.pdm.dev-dependencies] +test = [ + "celery[pytest]", + "codespell>=2.1.0", + "freezegun>=1.1.0", + "pre-commit>=2.15.0", + "pyfakefs>=4.5.1", + "pylint>=2.15.3", + "pytest-asyncio==0.15.1", + "pytest-cov>=2.12.1", + "pytest-freezegun", + "pytest-integration==0.2.2", + "pytest-mock>=3.6.1", + "pytest>=6.2.5", + "requests-mock>=1.9.3", + "setuptools>=49.6.0", + "pip-tools>=6.4.0", + "typing-extensions>=4.3.0", +] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.coverage.run] +source = ['datajunction_reflection/'] + +[tool.isort] +src_paths = ["datajunction_reflection/", "tests/"] +profile = 'black' diff --git a/datajunction-reflection/setup.cfg b/datajunction-reflection/setup.cfg new file mode 100644 index 000000000..cacabf684 --- /dev/null +++ b/datajunction-reflection/setup.cfg @@ -0,0 +1,19 @@ +# This file is used to configure your project. +# Read more about the various options under: +# https://setuptools.pypa.io/en/latest/userguide/declarative_config.html +# https://setuptools.pypa.io/en/latest/references/keywords.html + +[tool:pytest] +# Specify command line options as you would do when invoking pytest directly. +# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml +# in order to write a coverage file that can be read by Jenkins. +# CAUTION: --cov flags may prohibit setting breakpoints while debugging. +# Comment those flags to avoid this pytest issue. +addopts = + --cov datajunction_reflection --cov-report term-missing + --verbose +norecursedirs = + dist + build + .tox +testpaths = tests diff --git a/datajunction-reflection/tests/__init__.py b/datajunction-reflection/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-reflection/tests/conftest.py b/datajunction-reflection/tests/conftest.py new file mode 100644 index 000000000..f06791552 --- /dev/null +++ b/datajunction-reflection/tests/conftest.py @@ -0,0 +1,31 @@ +"""Test configuration.""" +import threading + +import pytest + +from datajunction_reflection.worker.app import celery_app as celeryapp + +# pytest_plugins = ("celery.contrib.pytest", ) + + +@pytest.fixture() +def celery_app(): + """ + Configure celery app for unit tests. This uses an in-memory broker + and starts a single worker. + """ + celeryapp.conf.update(CELERY_ALWAYS_EAGER=True) + celeryapp.conf.broker_url = "memory://localhost/" + celeryapp.conf.result_backend = "" + celeryapp.conf.CELERYD_CONCURRENCY = 1 + celeryapp.conf.CELERYD_POOL = "solo" + celeryapp.all_tasks = [] + + def run_worker(): + """Celery worker.""" + celeryapp.worker_main() + + thread = threading.Thread(target=run_worker) + thread.daemon = True + thread.start() + return celeryapp diff --git a/datajunction-reflection/tests/test_tasks.py b/datajunction-reflection/tests/test_tasks.py new file mode 100644 index 000000000..a5c55c0a8 --- /dev/null +++ b/datajunction-reflection/tests/test_tasks.py @@ -0,0 +1,41 @@ +"""Tests the celery app.""" +from unittest.mock import call + +from datajunction_reflection.worker.tasks import reflect_source, refresh + + +def test_refresh(celery_app, mocker): + """ + Tests that the reflection service refreshes DJ source nodes + """ + mock_dj_get_sources = mocker.patch("requests.get") + mock_dj_get_sources.return_value.json = lambda: ["postgres.test.revenue"] + mock_dj_get_sources.return_value.status_code = 200 + + refresh() + + assert { + "datajunction_reflection.worker.app.refresh", + "datajunction_reflection.worker.tasks.reflect_source", + }.intersection( + celery_app.tasks.keys(), + ) + assert mock_dj_get_sources.call_args_list == [ + call("http://dj:8000/nodes/?node_type=source", timeout=30), + ] + + +def test_reflect_source(celery_app, mocker, freezer): # pylint: disable=unused-argument + """ + Tests the reflection task. + """ + mock_dj_refresh = mocker.patch("requests.post") + mock_dj_refresh.return_value.status = 201 + + reflect_source.apply( + args=("postgres.test.revenue",), + ).get() + + assert mock_dj_refresh.call_args_list == [ + call("http://dj:8000/nodes/postgres.test.revenue/refresh/", timeout=30), + ] diff --git a/datajunction-reflection/tox.ini b/datajunction-reflection/tox.ini new file mode 100644 index 000000000..c10db6830 --- /dev/null +++ b/datajunction-reflection/tox.ini @@ -0,0 +1,14 @@ +[tox] +envlist = py3 + +[testenv] +pip_pre = true +deps = + -rrequirements/test.txt + pytest + testfixtures + coverage +commands = + pip install -e .[testing] + coverage run --source datajunction_reflection --parallel-mode -m pytest {posargs} --without-integration --without-slow-integration + coverage html --fail-under 100 -d test-reports/{envname}/coverage-html diff --git a/datajunction-server/.coveragerc b/datajunction-server/.coveragerc new file mode 100644 index 000000000..ee6929192 --- /dev/null +++ b/datajunction-server/.coveragerc @@ -0,0 +1,40 @@ +# .coveragerc to control coverage.py +[run] +concurrency = thread,greenlet +branch = True +source = datajunction_server +omit = + */datajunction_server/sql/parsing/backends/grammar/generated/* + */datajunction_server/sql/parsing/backends/antlr4.py + */datajunction_server/sql/parsing/ast.py + */datajunction_server/internal/access/authentication/google.py + */datajunction_server/api/access/authentication/google.py + */datajunction_server/api/djsql.py + */datajunction_server/construction/dj_query.py + */datajunction_server/alembic/* + +[paths] +source = + datajunction_server/ + */site-packages/ + +[report] +show_missing = True +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + if TYPE_CHECKING: diff --git a/datajunction-server/.env b/datajunction-server/.env new file mode 100644 index 000000000..c199a9a84 --- /dev/null +++ b/datajunction-server/.env @@ -0,0 +1,3 @@ +QUERY_SERVICE=http://djqs:8001 +SECRET=a-fake-secretkey +NODE_LIST_MAX=10000 diff --git a/datajunction-server/.env.integration b/datajunction-server/.env.integration new file mode 100644 index 000000000..1dd0ac039 --- /dev/null +++ b/datajunction-server/.env.integration @@ -0,0 +1 @@ +QUERY_SERVICE=http://djqs:8001 diff --git a/datajunction-server/.flake8 b/datajunction-server/.flake8 new file mode 100644 index 000000000..d9ad0b409 --- /dev/null +++ b/datajunction-server/.flake8 @@ -0,0 +1,5 @@ +[flake8] +ignore = E203, E266, E501, W503, F403, F401 +max-line-length = 79 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 diff --git a/datajunction-server/.isort.cfg b/datajunction-server/.isort.cfg new file mode 100644 index 000000000..8c960bf43 --- /dev/null +++ b/datajunction-server/.isort.cfg @@ -0,0 +1,3 @@ +[settings] +profile = black +known_first_party = dj diff --git a/datajunction-server/.pre-commit-config.yaml b/datajunction-server/.pre-commit-config.yaml new file mode 100644 index 000000000..5cb3db18e --- /dev/null +++ b/datajunction-server/.pre-commit-config.yaml @@ -0,0 +1,54 @@ +files: ^datajunction-server/ +exclude: (^datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated|^README.md) + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: check-ast + exclude: ^templates/ + - id: check-merge-conflict + - id: debug-statements + exclude: ^templates/ + - id: requirements-txt-fixer + exclude: ^templates/ + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.4 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v0.981' # Use the sha / tag you want to point at + hooks: + - id: mypy + exclude: ^templates/ + additional_dependencies: + - types-requests + - types-freezegun + - types-python-dateutil + - types-setuptools + - types-PyYAML + - types-tabulate + +- repo: https://github.com/asottile/add-trailing-comma + rev: v2.2.1 + hooks: + - id: add-trailing-comma + +- repo: https://github.com/kynan/nbstripout + rev: 0.6.1 + hooks: + - id: nbstripout +- repo: https://github.com/tomcatling/black-nb + rev: "0.7" + hooks: + - id: black-nb + files: '\.ipynb$' +- repo: https://github.com/pdm-project/pdm + rev: 2.6.1 + hooks: + - id: pdm-lock-check diff --git a/datajunction-server/Dockerfile b/datajunction-server/Dockerfile new file mode 100644 index 000000000..fdbdd35e6 --- /dev/null +++ b/datajunction-server/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.10 + +ARG RELOAD="--reload" +ENV RELOAD ${RELOAD} + +WORKDIR /code +COPY . /code +RUN pip install --no-cache-dir --upgrade -r /code/requirements/docker.txt +RUN pip install -e . + +CMD ["sh", "-c", "opentelemetry-instrument uvicorn datajunction_server.api.main:app --host 0.0.0.0 --port 8000 $RELOAD"] +EXPOSE 8000 diff --git a/datajunction-server/Makefile b/datajunction-server/Makefile new file mode 100644 index 000000000..5a5b7d342 --- /dev/null +++ b/datajunction-server/Makefile @@ -0,0 +1,41 @@ +pyenv: .python-version + +.python-version: setup.cfg + if [ -z "`pyenv virtualenvs | grep dj`" ]; then\ + pyenv virtualenv dj;\ + fi + if [ ! -f .python-version ]; then\ + pyenv local dj;\ + fi + pip install -r requirements/test.txt + touch .python-version + +docker-build: + docker build . + docker compose build + +docker-run: + docker compose up + +test: + pdm run pytest -n auto --cov-fail-under=100 --cov=datajunction_server --cov-report term-missing -vv tests/ --doctest-modules datajunction_server --without-integration --without-slow-integration --ignore=datajunction_server/alembic/env.py ${PYTEST_ARGS} + +integration: + pdm run pytest --cov=dj -vv tests/ --doctest-modules datajunction_server --with-integration --with-slow-integration --ignore=datajunction_server/alembic/env.py ${PYTEST_ARGS} + +clean: + pyenv virtualenv-delete dj + +spellcheck: + codespell -L froms -S "*.json" dj docs/*rst tests templates + +check: + pdm run pre-commit run --all-files + +lint: + make check + +dev-release: + hatch version dev + hatch build + hatch publish diff --git a/datajunction-server/README.md b/datajunction-server/README.md new file mode 100644 index 000000000..6fe7ee418 --- /dev/null +++ b/datajunction-server/README.md @@ -0,0 +1,42 @@ +# DataJunction + +## Introduction + +DataJunction (DJ) is an open source **metrics platform** that allows users to define +metrics and the data models behind them using **SQL**, serving as a **semantic layer** +on top of a physical data warehouse. By leveraging this metadata, DJ can enable efficient +retrieval of metrics data across different dimensions and filters. + +![DataJunction](docs/static/datajunction-illustration.png) + +## Getting Started + +To launch the DataJunction UI with a minimal DataJunction backend, start the default docker compose environment. + +```sh +docker compose up +``` + +If you'd like to launch the full suite of services, including open-source implementations of the DataJunction query service and +DataJunction reflection service specifications, use the `demo` profile. + +```sh +docker compose --profile demo up +``` + +DJUI: [http://localhost:3000/](http://localhost:3000/) +DJ Swagger Docs: [http://localhost:8000/docs](http://localhost:8000/docs) +DJQS Swagger Docs: [http://localhost:8001/docs](http://localhost:8001/docs) +Jaeger UI: [http://localhost:16686/search](http://localhost:16686/search) +Jupyter Lab: [http://localhost:8888](http://localhost:8888) + +## How does this work? + +At its core, DJ stores metrics and their upstream abstractions as interconnected nodes. +These nodes can represent a variety of elements, such as tables in a data warehouse +(**source nodes**), SQL transformation logic (**transform nodes**), dimensions logic, +metrics logic, and even selections of metrics, dimensions, and filters (**cube nodes**). + +By parsing each node's SQL into an AST and through dimensional links between columns, +DJ can infer a graph of dependencies between nodes, which allows it to find the +appropriate join paths between nodes to generate queries for metrics. diff --git a/datajunction-server/datajunction_server/__about__.py b/datajunction-server/datajunction_server/__about__.py new file mode 100644 index 000000000..4a0fdf2e8 --- /dev/null +++ b/datajunction-server/datajunction_server/__about__.py @@ -0,0 +1,5 @@ +""" +Version for Hatch +""" + +__version__ = "0.0.1a87" diff --git a/datajunction-server/datajunction_server/__init__.py b/datajunction-server/datajunction_server/__init__.py new file mode 100644 index 000000000..2cff295fb --- /dev/null +++ b/datajunction-server/datajunction_server/__init__.py @@ -0,0 +1,14 @@ +""" +Package version and name. +""" + +from importlib.metadata import PackageNotFoundError, version # pragma: no cover + +try: + # Change here if project is renamed and does not equal the package name + DIST_NAME = __name__ + __version__ = version(DIST_NAME) +except PackageNotFoundError: # pragma: no cover + __version__ = "unknown" +finally: + del version, PackageNotFoundError diff --git a/datajunction-server/datajunction_server/alembic.ini b/datajunction-server/datajunction_server/alembic.ini new file mode 100644 index 000000000..5716e4937 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic.ini @@ -0,0 +1,104 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s +file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = +timezone = UTC + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/datajunction-server/datajunction_server/alembic/README b/datajunction-server/datajunction_server/alembic/README new file mode 100644 index 000000000..2500aa1bc --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. diff --git a/datajunction-server/datajunction_server/alembic/env.py b/datajunction-server/datajunction_server/alembic/env.py new file mode 100644 index 000000000..1e28f0bf0 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/env.py @@ -0,0 +1,88 @@ +""" +Environment for Alembic migrations. +""" + +import os +from logging.config import fileConfig + +import alembic +from sqlalchemy import create_engine + +from datajunction_server.database.base import Base + +DEFAULT_URI = os.getenv( + "DATABASE_URI", + "postgresql+psycopg://dj:dj@postgres_metadata:5432/dj", +) + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = alembic.context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + x_args = alembic.context.get_x_argument(as_dictionary=True) + alembic.context.configure( + url=x_args.get("uri") or DEFAULT_URI, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + render_as_batch=True, + ) + + with alembic.context.begin_transaction(): + alembic.context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + x_args = alembic.context.get_x_argument(as_dictionary=True) + connectable = create_engine(x_args.get("uri") or DEFAULT_URI) + + with connectable.connect() as connection: + alembic.context.configure( + connection=connection, + target_metadata=target_metadata, + render_as_batch=True, + ) + + with alembic.context.begin_transaction(): + alembic.context.run_migrations() + + +if alembic.context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/datajunction-server/datajunction_server/alembic/script.py.mako b/datajunction-server/datajunction_server/alembic/script.py.mako new file mode 100644 index 000000000..6b50c8266 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/datajunction-server/datajunction_server/alembic/versions/2023_12_20_1829-724445d2b29d_initial_migration.py b/datajunction-server/datajunction_server/alembic/versions/2023_12_20_1829-724445d2b29d_initial_migration.py new file mode 100644 index 000000000..b141e0981 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2023_12_20_1829-724445d2b29d_initial_migration.py @@ -0,0 +1,708 @@ +"""Initial migration + +Revision ID: 724445d2b29d +Revises: +Create Date: 2023-12-20 18:29:11.681799+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +import sqlalchemy_utils +from alembic import op + +# revision identifiers, used by Alembic. +revision = "724445d2b29d" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "attributetype", + sa.Column("name", sa.String(), nullable=False), + sa.Column("allowed_node_types", sa.JSON(), nullable=True), + sa.Column("uniqueness_scope", sa.JSON(), nullable=True), + sa.Column("namespace", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=False), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_attributetype")), + sa.UniqueConstraint( + "namespace", + "name", + name=op.f("uq_attributetype_namespace"), + ), + ) + op.create_table( + "availabilitystate", + sa.Column("categorical_partitions", sa.JSON(), nullable=True), + sa.Column("temporal_partitions", sa.JSON(), nullable=True), + sa.Column("min_temporal_partition", sa.JSON(), nullable=True), + sa.Column("max_temporal_partition", sa.JSON(), nullable=True), + sa.Column("partitions", sa.JSON(), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("catalog", sa.String(), nullable=False), + sa.Column("schema_", sa.String(), nullable=True), + sa.Column("table", sa.String(), nullable=False), + sa.Column("valid_through_ts", sa.BigInteger(), nullable=False), + sa.Column("url", sa.String(), nullable=True), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_availabilitystate")), + ) + op.create_table( + "catalog", + sa.Column("uuid", sqlalchemy_utils.types.uuid.UUIDType(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("extra_params", sa.JSON(), nullable=True), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("name", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_catalog")), + ) + op.create_table( + "node", + sa.Column("name", sa.String(), nullable=False), + sa.Column( + "type", + sa.Enum( + "SOURCE", + "TRANSFORM", + "METRIC", + "DIMENSION", + "CUBE", + name="nodetype", + ), + nullable=False, + ), + sa.Column("display_name", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("deactivated_at", sa.DateTime(timezone=True), nullable=True), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("namespace", sa.String(), nullable=False), + sa.Column("current_version", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_node")), + sa.UniqueConstraint("name", "namespace", name="unique_node_namespace_name"), + sa.UniqueConstraint("name", name=op.f("uq_node_name")), + ) + op.create_table( + "measures", + sa.Column("display_name", sa.String(), nullable=True), + sa.Column( + "additive", + sa.Enum( + "additive", + "non_additive", + "semi_additive", + name="aggregationrule", + ), + nullable=False, + ), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id", name=op.f("pk_measures")), + sa.UniqueConstraint("name", name=op.f("uq_measures_name")), + ) + op.create_table( + "partition", + sa.Column("type_", sa.String(), nullable=False), + sa.Column("granularity", sa.String(), nullable=True), + sa.Column("format", sa.String(), nullable=True), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("column_id", sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_partition")), + ) + op.create_table( + "column", + sa.Column("display_name", sa.String(), nullable=True), + sa.Column("type", sa.String(), nullable=True), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("name", sa.String(), nullable=False), + sa.Column("dimension_id", sa.BigInteger(), nullable=True), + sa.Column("dimension_column", sa.String(), nullable=True), + sa.Column("measure_id", sa.BigInteger(), nullable=True), + sa.Column("partition_id", sa.BigInteger(), nullable=True), + sa.ForeignKeyConstraint( + ["dimension_id"], + ["node.id"], + name=op.f("fk_column_dimension_id_node"), + ), + sa.ForeignKeyConstraint( + ["measure_id"], + ["measures.id"], + name=op.f("fk_column_measure_id_measures"), + ), + sa.ForeignKeyConstraint( + ["partition_id"], + ["partition.id"], + name=op.f("fk_column_partition_id_partition"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_column")), + ) + with op.batch_alter_table("partition", schema=None) as batch_op: + batch_op.create_foreign_key( + "fk_partition_column_id_column", + "column", + ["column_id"], + ["id"], + ) + op.create_table( + "database", + sa.Column("uuid", sqlalchemy_utils.types.uuid.UUIDType(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("extra_params", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("description", sa.String(), nullable=True), + sa.Column("URI", sa.String(), nullable=False), + sa.Column("read_only", sa.Boolean(), nullable=False), + sa.Column("async", sa.Boolean(), nullable=False), + sa.Column("cost", sa.Float(), nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_database")), + sa.UniqueConstraint("name", name=op.f("uq_database_name")), + ) + op.create_table( + "engine", + sa.Column( + "dialect", + sa.Enum("SPARK", "TRINO", "DRUID", name="dialect"), + nullable=True, + ), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("name", sa.String(), nullable=False), + sa.Column("version", sa.String(), nullable=False), + sa.Column("uri", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id", name=op.f("pk_engine")), + ) + op.create_table( + "history", + sa.Column("pre", sa.JSON(), nullable=True), + sa.Column("post", sa.JSON(), nullable=True), + sa.Column("details", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column( + "entity_type", + sa.Enum( + "attribute", + "availability", + "backfill", + "catalog", + "column_attribute", + "dependency", + "engine", + "link", + "materialization", + "namespace", + "node", + "partition", + "query", + "tag", + name="entitytype", + ), + nullable=True, + ), + sa.Column("entity_name", sa.String(), nullable=True), + sa.Column("node", sa.String(), nullable=True), + sa.Column( + "activity_type", + sa.Enum( + "create", + "delete", + "restore", + "update", + "refresh", + "tag", + "set_attribute", + "status_change", + name="activitytype", + ), + nullable=True, + ), + sa.Column("user", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id", name=op.f("pk_history")), + ) + op.create_table( + "metricmetadata", + sa.Column( + "direction", + sa.Enum( + "HIGHER_IS_BETTER", + "LOWER_IS_BETTER", + "NEUTRAL", + name="metricdirection", + ), + nullable=True, + ), + sa.Column( + "unit", + sa.Enum( + "UNKNOWN", + "UNITLESS", + "PERCENTAGE", + "PROPORTION", + "DOLLAR", + "SECOND", + "MINUTE", + "HOUR", + "DAY", + "WEEK", + "MONTH", + "YEAR", + name="metricunit", + ), + nullable=True, + ), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_metricmetadata")), + ) + op.create_table( + "missingparent", + sa.Column("name", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_missingparent")), + ) + op.create_table( + "nodenamespace", + sa.Column("deactivated_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("namespace", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("namespace", name=op.f("pk_nodenamespace")), + sa.UniqueConstraint("namespace", name=op.f("uq_nodenamespace_namespace")), + ) + op.create_table( + "tag", + sa.Column("display_name", sa.String(), nullable=False), + sa.Column("tag_metadata", sa.JSON(), nullable=True), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("tag_type", sa.String(), nullable=False), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_tag")), + sa.UniqueConstraint("name", name=op.f("uq_tag_name")), + ) + op.create_table( + "users", + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("username", sa.String(), nullable=False), + sa.Column("password", sa.String(), nullable=True), + sa.Column("email", sa.String(), nullable=True), + sa.Column("name", sa.String(), nullable=True), + sa.Column( + "oauth_provider", + sa.Enum("basic", "github", "google", name="oauthprovider"), + nullable=False, + ), + sa.Column("is_admin", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_users")), + ) + op.create_table( + "catalogengines", + sa.Column("catalog_id", sa.BigInteger(), nullable=False), + sa.Column("engine_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["catalog_id"], + ["catalog.id"], + name=op.f("fk_catalogengines_catalog_id_catalog"), + ), + sa.ForeignKeyConstraint( + ["engine_id"], + ["engine.id"], + name=op.f("fk_catalogengines_engine_id_engine"), + ), + sa.PrimaryKeyConstraint( + "catalog_id", + "engine_id", + name=op.f("pk_catalogengines"), + ), + ) + op.create_table( + "columnattribute", + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("attribute_type_id", sa.BigInteger(), nullable=False), + sa.Column("column_id", sa.BigInteger(), nullable=True), + sa.ForeignKeyConstraint( + ["attribute_type_id"], + ["attributetype.id"], + name=op.f("fk_columnattribute_attribute_type_id_attributetype"), + ), + sa.ForeignKeyConstraint( + ["column_id"], + ["column.id"], + name=op.f("fk_columnattribute_column_id_column"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_columnattribute")), + sa.UniqueConstraint( + "attribute_type_id", + "column_id", + name=op.f("uq_columnattribute_attribute_type_id"), + ), + ) + op.create_table( + "noderevision", + sa.Column("name", sa.String(), nullable=False), + sa.Column("display_name", sa.String(), nullable=True), + sa.Column( + "type", + sa.Enum( + "SOURCE", + "TRANSFORM", + "METRIC", + "DIMENSION", + "CUBE", + name="nodetype", + ), + nullable=False, + ), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("lineage", sa.JSON(), nullable=True), + sa.Column("description", sa.String(), nullable=False), + sa.Column("query", sa.String(), nullable=True), + sa.Column("mode", sa.String(), nullable=False), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("version", sa.String(), nullable=True), + sa.Column("node_id", sa.BigInteger(), nullable=False), + sa.Column("catalog_id", sa.BigInteger(), nullable=True), + sa.Column("schema_", sa.String(), nullable=True), + sa.Column("table", sa.String(), nullable=True), + sa.Column("metric_metadata_id", sa.BigInteger(), nullable=True), + sa.Column("status", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["catalog_id"], + ["catalog.id"], + name=op.f("fk_noderevision_catalog_id_catalog"), + ), + sa.ForeignKeyConstraint( + ["metric_metadata_id"], + ["metricmetadata.id"], + name=op.f("fk_noderevision_metric_metadata_id_metricmetadata"), + ), + sa.ForeignKeyConstraint( + ["node_id"], + ["node.id"], + name=op.f("fk_noderevision_node_id_node"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_noderevision")), + sa.UniqueConstraint("version", "node_id", name=op.f("uq_noderevision_version")), + ) + op.create_table( + "table", + sa.Column("schema_", sa.String(), nullable=True), + sa.Column("table", sa.String(), nullable=False), + sa.Column("cost", sa.Float(), nullable=False), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("database_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["database_id"], + ["database.id"], + name=op.f("fk_table_database_id_database"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_table")), + ) + op.create_table( + "tagnoderelationship", + sa.Column("tag_id", sa.BigInteger(), nullable=False), + sa.Column("node_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["node_id"], + ["node.id"], + name=op.f("fk_tagnoderelationship_node_id_node"), + ), + sa.ForeignKeyConstraint( + ["tag_id"], + ["tag.id"], + name=op.f("fk_tagnoderelationship_tag_id_tag"), + ), + sa.PrimaryKeyConstraint( + "tag_id", + "node_id", + name=op.f("pk_tagnoderelationship"), + ), + ) + op.create_table( + "cube", + sa.Column("cube_id", sa.BigInteger(), nullable=False), + sa.Column("cube_element_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["cube_element_id"], + ["column.id"], + name=op.f("fk_cube_cube_element_id_column"), + ), + sa.ForeignKeyConstraint( + ["cube_id"], + ["noderevision.id"], + name=op.f("fk_cube_cube_id_noderevision"), + ), + sa.PrimaryKeyConstraint("cube_id", "cube_element_id", name=op.f("pk_cube")), + ) + op.create_table( + "materialization", + sa.Column( + "strategy", + sa.Enum( + "full", + "snapshot", + "snapshot_partition", + "incremental_time", + "view", + name="materializationstrategy", + ), + nullable=True, + server_default="full", + ), + sa.Column("config", sa.JSON(), nullable=False), + sa.Column("job", sa.String(), nullable=False), + sa.Column("deactivated_at", sa.DateTime(timezone=True), nullable=True), + sa.Column( + "id", + sa.BigInteger(), + autoincrement=True, + nullable=False, + ), + sa.Column("node_revision_id", sa.BigInteger(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("schedule", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["node_revision_id"], + ["noderevision.id"], + name=op.f("fk_materialization_node_revision_id_noderevision"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_materialization")), + sa.UniqueConstraint("name", "node_revision_id", name="name_node_revision_uniq"), + ) + op.create_table( + "metric_required_dimensions", + sa.Column("metric_id", sa.BigInteger(), nullable=False), + sa.Column("bound_dimension_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["bound_dimension_id"], + ["column.id"], + name=op.f("fk_metric_required_dimensions_bound_dimension_id_column"), + ), + sa.ForeignKeyConstraint( + ["metric_id"], + ["noderevision.id"], + name=op.f("fk_metric_required_dimensions_metric_id_noderevision"), + ), + sa.PrimaryKeyConstraint( + "metric_id", + "bound_dimension_id", + name=op.f("pk_metric_required_dimensions"), + ), + ) + op.create_table( + "nodeavailabilitystate", + sa.Column("availability_id", sa.BigInteger(), nullable=False), + sa.Column("node_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["availability_id"], + ["availabilitystate.id"], + name=op.f("fk_nodeavailabilitystate_availability_id_availabilitystate"), + ), + sa.ForeignKeyConstraint( + ["node_id"], + ["noderevision.id"], + name=op.f("fk_nodeavailabilitystate_node_id_noderevision"), + ), + sa.PrimaryKeyConstraint( + "availability_id", + "node_id", + name=op.f("pk_nodeavailabilitystate"), + ), + ) + op.create_table( + "nodecolumns", + sa.Column("node_id", sa.BigInteger(), nullable=False), + sa.Column("column_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["column_id"], + ["column.id"], + name=op.f("fk_nodecolumns_column_id_column"), + ), + sa.ForeignKeyConstraint( + ["node_id"], + ["noderevision.id"], + name=op.f("fk_nodecolumns_node_id_noderevision"), + ), + sa.PrimaryKeyConstraint("node_id", "column_id", name=op.f("pk_nodecolumns")), + ) + op.create_table( + "nodemissingparents", + sa.Column("missing_parent_id", sa.BigInteger(), nullable=False), + sa.Column("referencing_node_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["missing_parent_id"], + ["missingparent.id"], + name=op.f("fk_nodemissingparents_missing_parent_id_missingparent"), + ), + sa.ForeignKeyConstraint( + ["referencing_node_id"], + ["noderevision.id"], + name=op.f("fk_nodemissingparents_referencing_node_id_noderevision"), + ), + sa.PrimaryKeyConstraint( + "missing_parent_id", + "referencing_node_id", + name=op.f("pk_nodemissingparents"), + ), + ) + op.create_table( + "noderelationship", + sa.Column("parent_id", sa.BigInteger(), nullable=False), + sa.Column("parent_version", sa.String(), nullable=True), + sa.Column("child_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["child_id"], + ["noderevision.id"], + name=op.f("fk_noderelationship_child_id_noderevision"), + ), + sa.ForeignKeyConstraint( + ["parent_id"], + ["node.id"], + name=op.f("fk_noderelationship_parent_id_node"), + ), + sa.PrimaryKeyConstraint( + "parent_id", + "child_id", + name=op.f("pk_noderelationship"), + ), + ) + op.create_table( + "tablecolumns", + sa.Column("table_id", sa.BigInteger(), nullable=False), + sa.Column("column_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["column_id"], + ["column.id"], + name=op.f("fk_tablecolumns_column_id_column"), + ), + sa.ForeignKeyConstraint( + ["table_id"], + ["table.id"], + name=op.f("fk_tablecolumns_table_id_table"), + ), + sa.PrimaryKeyConstraint("table_id", "column_id", name=op.f("pk_tablecolumns")), + ) + op.create_table( + "backfill", + sa.Column("spec", sa.JSON(), nullable=True), + sa.Column("urls", sa.JSON(), nullable=True), + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("materialization_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["materialization_id"], + ["materialization.id"], + name=op.f("fk_backfill_materialization_id_materialization"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_backfill")), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("backfill") + op.drop_table("tablecolumns") + op.drop_table("noderelationship") + op.drop_table("nodemissingparents") + op.drop_table("nodecolumns") + op.drop_table("nodeavailabilitystate") + op.drop_table("metric_required_dimensions") + op.drop_table("materialization") + op.drop_table("cube") + op.drop_table("tagnoderelationship") + op.drop_table("table") + op.drop_table("noderevision") + op.drop_table("columnattribute") + op.drop_table("catalogengines") + op.drop_table("users") + op.drop_table("tag") + op.drop_table("partition") + op.drop_table("nodenamespace") + op.drop_table("node") + op.drop_table("missingparent") + op.drop_table("metricmetadata") + op.drop_table("measures") + op.drop_table("history") + op.drop_table("engine") + op.drop_table("database") + op.drop_table("column") + op.drop_table("catalog") + op.drop_table("availabilitystate") + op.drop_table("attributetype") + # ### end Alembic commands ### diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_01_08_2034-945d44abcd32_add_dimension_links.py b/datajunction-server/datajunction_server/alembic/versions/2024_01_08_2034-945d44abcd32_add_dimension_links.py new file mode 100644 index 000000000..bf9d57935 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_01_08_2034-945d44abcd32_add_dimension_links.py @@ -0,0 +1,72 @@ +"""Add dimension links + +Revision ID: 945d44abcd32 +Revises: 724445d2b29d +Create Date: 2024-01-08 20:34:59.505580+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "945d44abcd32" +down_revision = "724445d2b29d" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "dimensionlink", + sa.Column( + "id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("role", sa.String(), nullable=True), + sa.Column( + "node_revision_id", + sa.BigInteger(), + nullable=False, + ), + sa.Column( + "dimension_id", + sa.BigInteger(), + nullable=False, + ), + sa.Column("join_sql", sa.String(), nullable=False), + sa.Column( + "join_type", + sa.Enum("LEFT", "RIGHT", "INNER", "FULL", "CROSS", name="jointype"), + nullable=True, + ), + sa.Column( + "join_cardinality", + sa.Enum( + "ONE_TO_ONE", + "ONE_TO_MANY", + "MANY_TO_ONE", + "MANY_TO_MANY", + name="joincardinality", + ), + nullable=False, + ), + sa.Column("materialization_conf", sa.JSON(), nullable=True), + sa.ForeignKeyConstraint( + ["dimension_id"], + ["node.id"], + name=op.f("fk_dimensionlink_dimension_id_node"), + ), + sa.ForeignKeyConstraint( + ["node_revision_id"], + ["noderevision.id"], + name=op.f("fk_dimensionlink_node_revision_id_noderevision"), + ), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade(): + op.drop_table("dimensionlink") diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_01_11_2032-c74b11566d82_add_column_order.py b/datajunction-server/datajunction_server/alembic/versions/2024_01_11_2032-c74b11566d82_add_column_order.py new file mode 100644 index 000000000..ac0a62965 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_01_11_2032-c74b11566d82_add_column_order.py @@ -0,0 +1,76 @@ +"""Add column order and sets on delete policy for column <-> dimension_id + +Revision ID: c74b11566d82 +Revises: 945d44abcd32 +Create Date: 2024-01-11 20:32:44.086208+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c74b11566d82" +down_revision = "945d44abcd32" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("column", schema=None) as batch_op: + batch_op.add_column(sa.Column("order", sa.Integer(), nullable=True)) + batch_op.drop_constraint("fk_column_dimension_id_node", type_="foreignkey") + batch_op.create_foreign_key( + "fk_column_dimension_id_node", + "node", + ["dimension_id"], + ["id"], + ondelete="SET NULL", + ) + + batch_op.drop_constraint("fk_column_measure_id_measures", type_="foreignkey") + batch_op.create_foreign_key( + "fk_column_measure_id_measures", + "measures", + ["measure_id"], + ["id"], + ondelete="SET NULL", + ) + + batch_op.drop_constraint("fk_column_partition_id_partition", type_="foreignkey") + batch_op.create_foreign_key( + "fk_column_partition_id_partition", + "partition", + ["partition_id"], + ["id"], + ondelete="SET NULL", + ) + + +def downgrade(): + with op.batch_alter_table("column", schema=None) as batch_op: + batch_op.drop_constraint("fk_column_partition_id_partition", type_="foreignkey") + batch_op.create_foreign_key( + "fk_column_partition_id_partition", + "partition", + ["partition_id"], + ["id"], + ) + + batch_op.drop_constraint("fk_column_measure_id_measures", type_="foreignkey") + batch_op.create_foreign_key( + "fk_column_measure_id_measures", + "measures", + ["measure_id"], + ["id"], + ) + + batch_op.drop_constraint("fk_column_dimension_id_node", type_="foreignkey") + batch_op.create_foreign_key( + "fk_column_dimension_id_node", + "node", + ["dimension_id"], + ["id"], + ) + batch_op.drop_column("order") diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_01_18_2011-20f060b02772_switch_enum_values.py b/datajunction-server/datajunction_server/alembic/versions/2024_01_18_2011-20f060b02772_switch_enum_values.py new file mode 100644 index 000000000..e16c97ce0 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_01_18_2011-20f060b02772_switch_enum_values.py @@ -0,0 +1,204 @@ +"""Switch enum values + +Revision ID: 20f060b02772 +Revises: c74b11566d82 +Create Date: 2024-01-18 20:11:08.521879+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "20f060b02772" +down_revision = "c74b11566d82" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("nodenamespace", schema=None) as batch_op: + batch_op.create_unique_constraint("uq_nodenamespace_namespace", ["namespace"]) + + op.execute("ALTER TYPE oauthprovider RENAME VALUE 'basic' TO 'BASIC'") + op.execute("ALTER TYPE oauthprovider RENAME VALUE 'github' TO 'GITHUB'") + op.execute("ALTER TYPE oauthprovider RENAME VALUE 'google' TO 'GOOGLE'") + op.execute( + "UPDATE users set oauth_provider = upper(oauth_provider::text)::oauthprovider", + ) + + op.execute("ALTER TYPE materializationstrategy RENAME VALUE 'full' TO 'FULL'") + op.execute( + "ALTER TYPE materializationstrategy RENAME VALUE 'snapshot' TO 'SNAPSHOT'", + ) + op.execute( + "ALTER TYPE materializationstrategy " + "RENAME VALUE 'snapshot_partition' TO 'SNAPSHOT_PARTITION'", + ) + op.execute( + "ALTER TYPE materializationstrategy RENAME VALUE 'incremental_time' TO 'INCREMENTAL_TIME'", + ) + op.execute("ALTER TYPE materializationstrategy RENAME VALUE 'view' TO 'VIEW'") + op.execute( + "UPDATE materialization set strategy = upper(strategy::text)::materializationstrategy", + ) + + op.execute("ALTER TYPE aggregationrule RENAME VALUE 'additive' TO 'ADDITIVE'") + op.execute( + "ALTER TYPE aggregationrule RENAME VALUE 'non_additive' TO 'NON_ADDITIVE'", + ) + op.execute( + "ALTER TYPE aggregationrule RENAME VALUE 'semi_additive' TO 'SEMI_ADDITIVE'", + ) + op.execute("UPDATE measures set additive = upper(additive::text)::aggregationrule") + + op.execute("ALTER TYPE entitytype RENAME VALUE 'attribute' TO 'ATTRIBUTE'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'availability' TO 'AVAILABILITY'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'backfill' TO 'BACKFILL'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'catalog' TO 'CATALOG'") + op.execute( + "ALTER TYPE entitytype RENAME VALUE 'column_attribute' TO 'COLUMN_ATTRIBUTE'", + ) + op.execute("ALTER TYPE entitytype RENAME VALUE 'dependency' TO 'DEPENDENCY'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'engine' TO 'ENGINE'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'link' TO 'LINK'") + op.execute( + "ALTER TYPE entitytype RENAME VALUE 'materialization' TO 'MATERIALIZATION'", + ) + op.execute("ALTER TYPE entitytype RENAME VALUE 'namespace' TO 'NAMESPACE'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'node' TO 'NODE'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'partition' TO 'PARTITION'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'query' TO 'QUERY'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'tag' TO 'TAG'") + op.execute("UPDATE history set entity_type = upper(entity_type::text)::entitytype") + + op.execute("ALTER TYPE activitytype RENAME VALUE 'create' TO 'CREATE'") + op.execute("ALTER TYPE activitytype RENAME VALUE 'delete' TO 'DELETE'") + op.execute("ALTER TYPE activitytype RENAME VALUE 'restore' TO 'RESTORE'") + op.execute("ALTER TYPE activitytype RENAME VALUE 'update' TO 'UPDATE'") + op.execute("ALTER TYPE activitytype RENAME VALUE 'refresh' TO 'REFRESH'") + op.execute("ALTER TYPE activitytype RENAME VALUE 'tag' TO 'TAG'") + op.execute( + "ALTER TYPE activitytype RENAME VALUE 'set_attribute' TO 'SET_ATTRIBUTE'", + ) + op.execute( + "ALTER TYPE activitytype RENAME VALUE 'status_change' TO 'STATUS_CHANGE'", + ) + op.execute( + "UPDATE history set activity_type = upper(activity_type::text)::activitytype", + ) + + op.execute("CREATE TYPE nodestatus AS ENUM ('VALID', 'INVALID')") + op.execute("CREATE TYPE nodemode AS ENUM ('DRAFT', 'PUBLISHED')") + op.execute( + "ALTER TABLE noderevision ALTER COLUMN status " + "TYPE nodestatus USING upper(status)::nodestatus", + ) + op.execute( + "ALTER TABLE noderevision ALTER COLUMN mode TYPE nodemode USING upper(mode)::nodemode", + ) + + op.execute("CREATE TYPE partitiontype AS ENUM ('TEMPORAL', 'CATEGORICAL')") + op.execute( + "CREATE TYPE granularity AS ENUM " + "('SECOND', 'MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'QUARTER', 'YEAR')", + ) + op.execute( + "ALTER TABLE partition ALTER COLUMN type_ " + "TYPE partitiontype USING upper(type_)::partitiontype", + ) + op.execute( + "ALTER TABLE partition ALTER COLUMN granularity " + "TYPE granularity USING upper(granularity)::granularity", + ) + + +def downgrade(): + op.execute("DROP TYPE partitiontype") + op.execute("DROP TYPE granularity") + op.execute( + "ALTER TABLE partition ALTER COLUMN type_ TYPE text USING lower(type_::text)", + ) + op.execute( + "ALTER TABLE partition ALTER COLUMN granularity TYPE text USING lower(granularity::text)", + ) + + op.execute("DROP TYPE nodestatus") + op.execute("DROP TYPE nodemode") + op.execute( + "ALTER TABLE noderevision ALTER COLUMN status TYPE text USING lower(status::text)", + ) + op.execute( + "ALTER TABLE noderevision ALTER COLUMN mode TYPE text USING lower(mode::text)", + ) + + op.execute("ALTER TYPE activitytype RENAME VALUE 'CREATE' TO 'create'") + op.execute("ALTER TYPE activitytype RENAME VALUE 'DELETE' TO 'delete'") + op.execute("ALTER TYPE activitytype RENAME VALUE 'RESTORE' TO 'restore'") + op.execute("ALTER TYPE activitytype RENAME VALUE 'UPDATE' TO 'update'") + op.execute("ALTER TYPE activitytype RENAME VALUE 'REFRESH' TO 'refresh'") + op.execute("ALTER TYPE activitytype RENAME VALUE 'TAG' TO 'tag'") + op.execute( + "ALTER TYPE activitytype RENAME VALUE 'SET_ATTRIBUTE' TO 'set_attribute'", + ) + op.execute( + "ALTER TYPE activitytype RENAME VALUE 'STATUS_CHANGE' TO 'status_change'", + ) + op.execute( + "UPDATE history set activity_type = lower(activity_type::text)::activitytype", + ) + + op.execute("ALTER TYPE entitytype RENAME VALUE 'ATTRIBUTE' TO 'attribute'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'AVAILABILITY' TO 'availability'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'BACKFILL' TO 'backfill'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'CATALOG' TO 'catalog'") + op.execute( + "ALTER TYPE entitytype RENAME VALUE 'COLUMN_ATTRIBUTE' TO 'column_attribute'", + ) + op.execute("ALTER TYPE entitytype RENAME VALUE 'DEPENDENCY' TO 'dependency'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'ENGINE' TO 'engine'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'LINK' TO 'link'") + op.execute( + "ALTER TYPE entitytype RENAME VALUE 'MATERIALIZATION' TO 'materialization'", + ) + op.execute("ALTER TYPE entitytype RENAME VALUE 'NAMESPACE' TO 'namespace'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'NODE' TO 'node'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'PARTITION' TO 'partition'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'QUERY' TO 'query'") + op.execute("ALTER TYPE entitytype RENAME VALUE 'TAG' TO 'tag'") + op.execute("UPDATE history set entity_type = lower(entity_type::text)::entitytype") + + op.execute("ALTER TYPE aggregationrule RENAME VALUE 'ADDITIVE' TO 'additive'") + op.execute( + "ALTER TYPE aggregationrule RENAME VALUE 'NON_ADDITIVE' TO 'non_additive'", + ) + op.execute( + "ALTER TYPE aggregationrule RENAME VALUE 'SEMI_ADDITIVE' TO 'semi_additive'", + ) + op.execute("UPDATE measures set additive = lower(additive::text)::aggregationrule") + + op.execute("ALTER TYPE materializationstrategy RENAME VALUE 'VIEW' TO 'view'") + op.execute( + "ALTER TYPE materializationstrategy RENAME VALUE 'INCREMENTAL_TIME' TO 'incremental_time'", + ) + op.execute( + "ALTER TYPE materializationstrategy " + "RENAME VALUE 'SNAPSHOT_PARTITION' TO 'snapshot_partition'", + ) + op.execute( + "ALTER TYPE materializationstrategy RENAME VALUE 'SNAPSHOT' TO 'snapshot'", + ) + op.execute("ALTER TYPE materializationstrategy RENAME VALUE 'FULL' TO 'full'") + op.execute( + "UPDATE materialization set strategy = lower(strategy::text)::materializationstrategy", + ) + + op.execute("ALTER TYPE oauthprovider RENAME VALUE 'GOOGLE' TO 'google'") + op.execute("ALTER TYPE oauthprovider RENAME VALUE 'GITHUB' TO 'github'") + op.execute("ALTER TYPE oauthprovider RENAME VALUE 'BASIC' TO 'basic'") + op.execute( + "UPDATE users set oauth_provider = lower(oauth_provider::text)::oauthprovider", + ) + + with op.batch_alter_table("nodenamespace", schema=None) as batch_op: + batch_op.drop_constraint("uq_nodenamespace_namespace", type_="unique") diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_01_23_0617-c9cef8864ecb_add_missing_table_attribute_to_source_.py b/datajunction-server/datajunction_server/alembic/versions/2024_01_23_0617-c9cef8864ecb_add_missing_table_attribute_to_source_.py new file mode 100644 index 000000000..4e84dc06c --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_01_23_0617-c9cef8864ecb_add_missing_table_attribute_to_source_.py @@ -0,0 +1,34 @@ +"""Add missing_table attribute to source nodes. + +Revision ID: c9cef8864ecb +Revises: 20f060b02772 +Create Date: 2024-01-23 06:17:10.054149+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c9cef8864ecb" +down_revision = "20f060b02772" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("node", schema=None) as batch_op: + batch_op.add_column( + sa.Column("missing_table", sa.Boolean(), nullable=True, default=False), + ) + + op.execute("UPDATE node SET missing_table = False WHERE missing_table IS NULL") + + with op.batch_alter_table("node", schema=None) as batch_op: + batch_op.alter_column("missing_table", nullable=False) + + +def downgrade(): + with op.batch_alter_table("node", schema=None) as batch_op: + batch_op.drop_column("missing_table") diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_01_23_1655-a8e22109be24_availability_state_s_valid_through_ts_.py b/datajunction-server/datajunction_server/alembic/versions/2024_01_23_1655-a8e22109be24_availability_state_s_valid_through_ts_.py new file mode 100644 index 000000000..322f16b6e --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_01_23_1655-a8e22109be24_availability_state_s_valid_through_ts_.py @@ -0,0 +1,27 @@ +"""Availability state's valid_through_ts should be bigint + +Revision ID: a8e22109be24 +Revises: c9cef8864ecb +Create Date: 2024-01-23 16:55:20.951715+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "a8e22109be24" +down_revision = "c9cef8864ecb" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("availabilitystate", schema=None) as batch_op: + batch_op.alter_column("valid_through_ts", type_=sa.BigInteger()) + + +def downgrade(): + with op.batch_alter_table("availabilitystate", schema=None) as batch_op: + batch_op.alter_column("valid_through_ts", type_=sa.Integer()) diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_02_22_0713-d61fb7e48cc3_cascade_deletes_to_dimension_links.py b/datajunction-server/datajunction_server/alembic/versions/2024_02_22_0713-d61fb7e48cc3_cascade_deletes_to_dimension_links.py new file mode 100644 index 000000000..973c1f382 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_02_22_0713-d61fb7e48cc3_cascade_deletes_to_dimension_links.py @@ -0,0 +1,119 @@ +"""Cascade deletes to dimension links + +Revision ID: d61fb7e48cc3 +Revises: a8e22109be24 +Create Date: 2024-02-22 07:13:51.441347+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "d61fb7e48cc3" +down_revision = "a8e22109be24" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("column", schema=None) as batch_op: + batch_op.drop_constraint("fk_column_measure_id_measures", type_="foreignkey") + batch_op.drop_constraint("fk_column_partition_id_partition", type_="foreignkey") + batch_op.create_foreign_key( + "fk_column_partition_id_partition", + "partition", + ["partition_id"], + ["id"], + ondelete="SET NULL", + ) + batch_op.create_foreign_key( + "fk_column_measure_id_measures", + "measures", + ["measure_id"], + ["id"], + ondelete="SET NULL", + ) + + with op.batch_alter_table("dimensionlink", schema=None) as batch_op: + batch_op.drop_constraint( + "fk_dimensionlink_dimension_id_node", + type_="foreignkey", + ) + batch_op.drop_constraint( + "fk_dimensionlink_node_revision_id_noderevision", + type_="foreignkey", + ) + batch_op.create_foreign_key( + "fk_dimensionlink_node_revision_id_noderevision", + "noderevision", + ["node_revision_id"], + ["id"], + ondelete="CASCADE", + ) + batch_op.create_foreign_key( + "fk_dimensionlink_dimension_id_node", + "node", + ["dimension_id"], + ["id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("partition", schema=None) as batch_op: + batch_op.drop_constraint("fk_partition_column_id_column", type_="foreignkey") + batch_op.create_foreign_key( + "fk_partition_column_id_column", + "column", + ["column_id"], + ["id"], + ondelete="CASCADE", + ) + + +def downgrade(): + with op.batch_alter_table("partition", schema=None) as batch_op: + batch_op.drop_constraint("fk_partition_column_id_column", type_="foreignkey") + batch_op.create_foreign_key( + "fk_partition_column_id_column", + "column", + ["column_id"], + ["id"], + ) + + with op.batch_alter_table("dimensionlink", schema=None) as batch_op: + batch_op.drop_constraint( + "fk_dimensionlink_dimension_id_node", + type_="foreignkey", + ) + batch_op.drop_constraint( + "fk_dimensionlink_node_revision_id_noderevision", + type_="foreignkey", + ) + batch_op.create_foreign_key( + "fk_dimensionlink_node_revision_id_noderevision", + "noderevision", + ["node_revision_id"], + ["id"], + ) + batch_op.create_foreign_key( + "fk_dimensionlink_dimension_id_node", + "node", + ["dimension_id"], + ["id"], + ) + + with op.batch_alter_table("column", schema=None) as batch_op: + batch_op.drop_constraint("fk_column_measure_id_measures", type_="foreignkey") + batch_op.drop_constraint("fk_column_partition_id_partition", type_="foreignkey") + batch_op.create_foreign_key( + "fk_column_partition_id_partition", + "partition", + ["partition_id"], + ["id"], + ) + batch_op.create_foreign_key( + "fk_column_measure_id_measures", + "measures", + ["measure_id"], + ["id"], + ) diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_04_30_1556-de7ec1c82fe0_add_query_requests.py b/datajunction-server/datajunction_server/alembic/versions/2024_04_30_1556-de7ec1c82fe0_add_query_requests.py new file mode 100644 index 000000000..f31ecceac --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_04_30_1556-de7ec1c82fe0_add_query_requests.py @@ -0,0 +1,97 @@ +"""Add query requests + +Revision ID: de7ec1c82fe0 +Revises: d61fb7e48cc3 +Create Date: 2024-04-30 15:56:20.193978+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "de7ec1c82fe0" +down_revision = "d61fb7e48cc3" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "queryrequest", + sa.Column("id", sa.BigInteger(), nullable=False), + sa.Column( + "query_type", + sa.Enum("METRICS", "MEASURES", "NODE", name="querybuildtype"), + nullable=False, + ), + sa.Column( + "nodes", + postgresql.JSONB(astext_type=sa.Text()), + server_default=sa.text("'[]'::jsonb"), + nullable=False, + ), + sa.Column( + "parents", + postgresql.JSONB(astext_type=sa.Text()), + server_default=sa.text("'[]'::jsonb"), + nullable=False, + ), + sa.Column( + "dimensions", + postgresql.JSONB(astext_type=sa.Text()), + server_default=sa.text("'[]'::jsonb"), + nullable=False, + ), + sa.Column( + "filters", + postgresql.JSONB(astext_type=sa.Text()), + server_default=sa.text("'[]'::jsonb"), + nullable=False, + ), + sa.Column("limit", sa.Integer(), nullable=True), + sa.Column( + "orderby", + postgresql.JSONB(astext_type=sa.Text()), + server_default=sa.text("'[]'::jsonb"), + nullable=False, + ), + sa.Column("engine_name", sa.String(), nullable=True), + sa.Column("engine_version", sa.String(), nullable=True), + sa.Column( + "other_args", + postgresql.JSONB(astext_type=sa.Text()), + server_default=sa.text("'{}'::jsonb"), + nullable=False, + ), + sa.Column("query", sa.String(), nullable=False), + sa.Column( + "columns", + postgresql.JSONB(astext_type=sa.Text()), + server_default=sa.text("'[]'::jsonb"), + nullable=False, + ), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "query_type", + "nodes", + "parents", + "dimensions", + "filters", + "engine_name", + "engine_version", + "limit", + "orderby", + name="query_request_unique", + postgresql_nulls_not_distinct=True, + ), + ) + + +def downgrade(): + op.drop_table("queryrequest") + op.execute("DROP TYPE querybuildtype") diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_05_09_1420-9b1227ff17f4_update_backfill_spec.py b/datajunction-server/datajunction_server/alembic/versions/2024_05_09_1420-9b1227ff17f4_update_backfill_spec.py new file mode 100644 index 000000000..526f80fa1 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_05_09_1420-9b1227ff17f4_update_backfill_spec.py @@ -0,0 +1,42 @@ +"""Update backfill spec + +Revision ID: 9b1227ff17f4 +Revises: de7ec1c82fe0 +Create Date: 2024-05-09 14:20:26.707322+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "9b1227ff17f4" +down_revision = "de7ec1c82fe0" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("backfill", schema=None) as batch_op: + batch_op.alter_column( + "spec", + existing_type=postgresql.JSON(astext_type=sa.Text()), + nullable=False, + ) + + # Update the backfill `spec` column to be a JSON list of partitions + op.execute("UPDATE backfill SET spec = jsonb_build_array(spec)") + + +def downgrade(): + with op.batch_alter_table("backfill", schema=None) as batch_op: + batch_op.alter_column( + "spec", + existing_type=postgresql.JSON(astext_type=sa.Text()), + nullable=True, + ) + + # This will cause data loss + op.execute("UPDATE backfill SET spec = spec->0") diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_05_21_0012-57fc93ef6947_add_query_id_to_queryrequest.py b/datajunction-server/datajunction_server/alembic/versions/2024_05_21_0012-57fc93ef6947_add_query_id_to_queryrequest.py new file mode 100644 index 000000000..4f961d2aa --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_05_21_0012-57fc93ef6947_add_query_id_to_queryrequest.py @@ -0,0 +1,27 @@ +"""Add query_id to queryrequest + +Revision ID: 57fc93ef6947 +Revises: 9b1227ff17f4 +Create Date: 2024-05-21 00:12:11.303914+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "57fc93ef6947" +down_revision = "9b1227ff17f4" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("queryrequest", schema=None) as batch_op: + batch_op.add_column(sa.Column("query_id", sa.String(), nullable=True)) + + +def downgrade(): + with op.batch_alter_table("queryrequest", schema=None) as batch_op: + batch_op.drop_column("query_id") diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_06_21_1301-640a814db2d8_add_collection_tables.py b/datajunction-server/datajunction_server/alembic/versions/2024_06_21_1301-640a814db2d8_add_collection_tables.py new file mode 100644 index 000000000..7691ca9a6 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_06_21_1301-640a814db2d8_add_collection_tables.py @@ -0,0 +1,55 @@ +"""Add collection tables + +Revision ID: 640a814db2d8 +Revises: 57fc93ef6947 +Create Date: 2024-06-21 13:01:48.141719+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "640a814db2d8" +down_revision = "57fc93ef6947" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "collection", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("deactivated_at", sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), + ) + op.create_table( + "collectionnodes", + sa.Column("collection_id", sa.Integer(), nullable=False), + sa.Column( + "node_id", + sa.BigInteger().with_variant(sa.Integer(), "sqlite"), + nullable=False, + ), + sa.ForeignKeyConstraint( + ["collection_id"], + ["collection.id"], + name="fk_collectionnodes_collection_id_collection", + ), + sa.ForeignKeyConstraint( + ["node_id"], + ["node.id"], + name="fk_collectionnodes_node_id_node", + ), + sa.PrimaryKeyConstraint("collection_id", "node_id"), + ) + + +def downgrade(): + op.drop_table("collectionnodes") + op.drop_table("collection") diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_07_12_0348-34171c92dd6d_set_user_username_to_be_unique.py b/datajunction-server/datajunction_server/alembic/versions/2024_07_12_0348-34171c92dd6d_set_user_username_to_be_unique.py new file mode 100644 index 000000000..484c054bc --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_07_12_0348-34171c92dd6d_set_user_username_to_be_unique.py @@ -0,0 +1,26 @@ +"""Set User.username to be unique + +Revision ID: 34171c92dd6d +Revises: 640a814db2d8 +Create Date: 2024-07-12 03:48:53.609685+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "34171c92dd6d" +down_revision = "640a814db2d8" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.create_unique_constraint(None, ["username"]) + + +def downgrade(): + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.drop_constraint(None, type_="unique") diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_08_18_0036-f3c9b40deb6f_add_create_by_to_nodes_node_revisions_.py b/datajunction-server/datajunction_server/alembic/versions/2024_08_18_0036-f3c9b40deb6f_add_create_by_to_nodes_node_revisions_.py new file mode 100644 index 000000000..32e3f5409 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_08_18_0036-f3c9b40deb6f_add_create_by_to_nodes_node_revisions_.py @@ -0,0 +1,95 @@ +"""add create_by to nodes, node revisions, collections, and tags + +Revision ID: f3c9b40deb6f +Revises: 34171c92dd6d +Create Date: 2024-08-18 00:36:51.859475+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "f3c9b40deb6f" +down_revision = "34171c92dd6d" +branch_labels = None +depends_on = None + + +def upgrade(): + """This upgrades adds the created_by_id field to Collection, Node, NodeRevision, and Tag + + Deployments prior to this upgrade will not have easily accessible information for who created + these objects in the past. Therefore, this upgrade backfills all existing objects as created by + the user with ID=1. If creation information can be gathered elsewhere, it's recommended that you + manually backfill the correct created_by_id after the database is upgraded. + """ + with op.batch_alter_table("collection", schema=None) as batch_op: + batch_op.add_column(sa.Column("created_by_id", sa.Integer(), nullable=True)) + batch_op.create_foreign_key(None, "users", ["created_by_id"], ["id"]) + + with op.batch_alter_table("node", schema=None) as batch_op: + batch_op.add_column(sa.Column("created_by_id", sa.Integer(), nullable=True)) + batch_op.create_foreign_key(None, "users", ["created_by_id"], ["id"]) + + with op.batch_alter_table("noderevision", schema=None) as batch_op: + batch_op.add_column(sa.Column("created_by_id", sa.Integer(), nullable=True)) + batch_op.create_foreign_key(None, "users", ["created_by_id"], ["id"]) + + with op.batch_alter_table("tag", schema=None) as batch_op: + batch_op.add_column(sa.Column("created_by_id", sa.Integer(), nullable=True)) + batch_op.create_foreign_key(None, "users", ["created_by_id"], ["id"]) + + # Backfill created by columns with user id 1 + op.execute("UPDATE collection SET created_by_id = 1 WHERE created_by_id IS NULL") + op.execute("UPDATE node SET created_by_id = 1 WHERE created_by_id IS NULL") + op.execute("UPDATE noderevision SET created_by_id = 1 WHERE created_by_id IS NULL") + op.execute("UPDATE tag SET created_by_id = 1 WHERE created_by_id IS NULL") + + # After the created by column is backfilled, make it required going forward + with op.batch_alter_table("collection", schema=None) as batch_op: + batch_op.alter_column( + "created_by_id", + existing_type=sa.Integer(), + nullable=False, + ) + + with op.batch_alter_table("node", schema=None) as batch_op: + batch_op.alter_column( + "created_by_id", + existing_type=sa.Integer(), + nullable=False, + ) + + with op.batch_alter_table("noderevision", schema=None) as batch_op: + batch_op.alter_column( + "created_by_id", + existing_type=sa.Integer(), + nullable=False, + ) + + with op.batch_alter_table("tag", schema=None) as batch_op: + batch_op.alter_column( + "created_by_id", + existing_type=sa.Integer(), + nullable=False, + ) + + +def downgrade(): + with op.batch_alter_table("tag", schema=None) as batch_op: + batch_op.drop_constraint(None, type_="foreignkey") + batch_op.drop_column("created_by_id") + + with op.batch_alter_table("noderevision", schema=None) as batch_op: + batch_op.drop_constraint(None, type_="foreignkey") + batch_op.drop_column("created_by_id") + + with op.batch_alter_table("node", schema=None) as batch_op: + batch_op.drop_constraint(None, type_="foreignkey") + batch_op.drop_column("created_by_id") + + with op.batch_alter_table("collection", schema=None) as batch_op: + batch_op.drop_constraint(None, type_="foreignkey") + batch_op.drop_column("created_by_id") diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_10_24_0015-4d6ab789e456_add_a_map_of_links_to_availability_state.py b/datajunction-server/datajunction_server/alembic/versions/2024_10_24_0015-4d6ab789e456_add_a_map_of_links_to_availability_state.py new file mode 100644 index 000000000..94209b25a --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_10_24_0015-4d6ab789e456_add_a_map_of_links_to_availability_state.py @@ -0,0 +1,27 @@ +"""Add a map of links to availability state. + +Revision ID: 4d6ab789e456 +Revises: f3c9b40deb6f +Create Date: 2024-10-24 00:15:17.022358+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "4d6ab789e456" +down_revision = "f3c9b40deb6f" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("availabilitystate", schema=None) as batch_op: + batch_op.add_column(sa.Column("links", sa.JSON(), nullable=True)) + + +def downgrade(): + with op.batch_alter_table("availabilitystate", schema=None) as batch_op: + batch_op.drop_column("links") diff --git a/datajunction-server/datajunction_server/alembic/versions/2024_10_26_0340-70904373eab3_add_indexes_on_history_and_node_tables.py b/datajunction-server/datajunction_server/alembic/versions/2024_10_26_0340-70904373eab3_add_indexes_on_history_and_node_tables.py new file mode 100644 index 000000000..391f641c0 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2024_10_26_0340-70904373eab3_add_indexes_on_history_and_node_tables.py @@ -0,0 +1,55 @@ +"""Add indexes on history and node tables + +Revision ID: 70904373eab3 +Revises: 4d6ab789e456 +Create Date: 2024-10-26 03:40:04.657089+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "70904373eab3" +down_revision = "4d6ab789e456" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("history", schema=None) as batch_op: + batch_op.create_index("ix_history_entity_name", ["entity_name"], unique=False) + + with op.batch_alter_table("history", schema=None) as batch_op: + batch_op.create_index("ix_history_user", ["user"], unique=False) + + with op.batch_alter_table("node", schema=None) as batch_op: + batch_op.create_index( + "cursor_index", + ["created_at", "id"], + unique=False, + postgresql_using="btree", + ) + + with op.batch_alter_table("node", schema=None) as batch_op: + batch_op.create_index( + "namespace_index", + ["namespace"], + unique=False, + postgresql_using="btree", + postgresql_ops={"identifier": "varchar_pattern_ops"}, + ) + + +def downgrade(): + with op.batch_alter_table("node", schema=None) as batch_op: + batch_op.drop_index("namespace_index", postgresql_using="text_pattern_ops") + + with op.batch_alter_table("node", schema=None) as batch_op: + batch_op.drop_index("cursor_index", postgresql_using="btree") + + with op.batch_alter_table("history", schema=None) as batch_op: + batch_op.drop_index("ix_history_user") + + with op.batch_alter_table("history", schema=None) as batch_op: + batch_op.drop_index("ix_history_entity_name") diff --git a/datajunction-server/datajunction_server/alembic/versions/2025_01_19_1808-9650f9b728a2_add_query_ast_for_noderevision.py b/datajunction-server/datajunction_server/alembic/versions/2025_01_19_1808-9650f9b728a2_add_query_ast_for_noderevision.py new file mode 100644 index 000000000..1f78c90ca --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2025_01_19_1808-9650f9b728a2_add_query_ast_for_noderevision.py @@ -0,0 +1,28 @@ +""" +Add query ast for noderevision + +Revision ID: 9650f9b728a2 +Revises: 70904373eab3 +Create Date: 2025-01-19 18:08:25.588956+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "9650f9b728a2" +down_revision = "70904373eab3" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("noderevision", schema=None) as batch_op: + batch_op.add_column(sa.Column("query_ast", sa.PickleType(), nullable=True)) + + +def downgrade(): + with op.batch_alter_table("noderevision", schema=None) as batch_op: + batch_op.drop_column("query_ast") diff --git a/datajunction-server/datajunction_server/alembic/versions/2025_01_24_0020-bec3296d7537_add_custom_metadata_field_to_nodes.py b/datajunction-server/datajunction_server/alembic/versions/2025_01_24_0020-bec3296d7537_add_custom_metadata_field_to_nodes.py new file mode 100644 index 000000000..7c81a6f2b --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2025_01_24_0020-bec3296d7537_add_custom_metadata_field_to_nodes.py @@ -0,0 +1,27 @@ +"""Add custom metadata field to nodes. + +Revision ID: bec3296d7537 +Revises: 9650f9b728a2 +Create Date: 2025-01-24 00:20:26.333974+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "bec3296d7537" +down_revision = "9650f9b728a2" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("noderevision", schema=None) as batch_op: + batch_op.add_column(sa.Column("custom_metadata", sa.JSON(), nullable=True)) + + +def downgrade(): + with op.batch_alter_table("noderevision", schema=None) as batch_op: + batch_op.drop_column("custom_metadata") diff --git a/datajunction-server/datajunction_server/alembic/versions/2025_02_11_1619-c3d5f327296c_notificationpreferences.py b/datajunction-server/datajunction_server/alembic/versions/2025_02_11_1619-c3d5f327296c_notificationpreferences.py new file mode 100644 index 000000000..50c1a7c90 --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2025_02_11_1619-c3d5f327296c_notificationpreferences.py @@ -0,0 +1,81 @@ +"""notificationpreferences + +Revision ID: c3d5f327296c +Revises: bec3296d7537 +Create Date: 2025-02-11 16:19:35.918790+00:00 + +""" +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c3d5f327296c" +down_revision = "bec3296d7537" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "notificationpreferences", + sa.Column("id", sa.BigInteger(), nullable=False), + sa.Column( + "entity_type", + sa.Enum( + "ATTRIBUTE", + "AVAILABILITY", + "BACKFILL", + "CATALOG", + "COLUMN_ATTRIBUTE", + "DEPENDENCY", + "ENGINE", + "LINK", + "MATERIALIZATION", + "NAMESPACE", + "NODE", + "PARTITION", + "QUERY", + "TAG", + name="notification_entitytype", + ), + nullable=False, + ), + sa.Column("entity_name", sa.String(), nullable=False), + sa.Column( + "activity_types", + sa.ARRAY( + sa.Enum( + "CREATE", + "DELETE", + "RESTORE", + "UPDATE", + "REFRESH", + "TAG", + "SET_ATTRIBUTE", + "STATUS_CHANGE", + name="notification_activitytype", + ), + ), + nullable=False, + ), + sa.Column( + "user_id", + sa.BigInteger().with_variant(sa.Integer(), "sqlite"), + nullable=False, + ), + sa.Column("alert_types", sa.ARRAY(sa.String()), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("entity_name", "entity_type", name="uix_entity_type_name"), + ) + + +def downgrade(): + op.drop_table("notificationpreferences") diff --git a/datajunction-server/datajunction_server/alembic/versions/2025_02_24_0549-b8ef80efd70c_associate_availability_and_.py b/datajunction-server/datajunction_server/alembic/versions/2025_02_24_0549-b8ef80efd70c_associate_availability_and_.py new file mode 100644 index 000000000..484f02a4c --- /dev/null +++ b/datajunction-server/datajunction_server/alembic/versions/2025_02_24_0549-b8ef80efd70c_associate_availability_and_.py @@ -0,0 +1,124 @@ +"""Associate availability and materialization + +Revision ID: b8ef80efd70c +Revises: c3d5f327296c +Create Date: 2025-02-24 05:49:06.588675+00:00 + +""" + +# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module +import json +import sqlalchemy as sa +from sqlalchemy.sql import table, column +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "b8ef80efd70c" +down_revision = "c3d5f327296c" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("availabilitystate", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "custom_metadata", + postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + ), + ) + batch_op.add_column( + sa.Column("materialization_id", sa.BigInteger(), nullable=True), + ) + batch_op.create_foreign_key( + "fk_availability_materialization_id_materialization", + "materialization", + ["materialization_id"], + ["id"], + ) + + availabilitystate = table( + "availabilitystate", + column("id", sa.BigInteger()), + column("url", sa.String()), + column("links", postgresql.JSON), + column("custom_metadata", postgresql.JSONB), + ) + + # Move data from url and links to custom_metadata + connection = op.get_bind() + results = connection.execute( + sa.select( + availabilitystate.c.id, + availabilitystate.c.url, + availabilitystate.c.links, + ), + ).fetchall() + + for row in results: + metadata = {} + if row.url: + metadata["url"] = row.url + if row.links: + metadata["links"] = row.links + + if metadata: + connection.execute( + sa.update(availabilitystate) + .where(availabilitystate.c.id == row.id) + .values(custom_metadata=metadata), + ) + + with op.batch_alter_table("availabilitystate", schema=None) as batch_op: + batch_op.drop_column("url") + batch_op.drop_column("links") + + +def downgrade(): + with op.batch_alter_table("availabilitystate", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "links", + postgresql.JSON(astext_type=sa.Text()), + autoincrement=False, + nullable=True, + ), + ) + batch_op.add_column( + sa.Column("url", sa.VARCHAR(), autoincrement=False, nullable=True), + ) + batch_op.drop_constraint( + "fk_availability_materialization_id_materialization", + type_="foreignkey", + ) + + # Restore `url` and `links` from `custom_metadata` + availabilitystate = table( + "availabilitystate", + column("id", sa.BigInteger()), + column("custom_metadata", postgresql.JSONB), + column("url", sa.String()), + column("links", postgresql.JSON), + ) + + conn = op.get_bind() + results = conn.execute( + sa.select(availabilitystate.c.id, availabilitystate.c.custom_metadata), + ).fetchall() + + for row in results: + metadata = json.loads(row.custom_metadata) if row.custom_metadata else {} + conn.execute( + sa.update(availabilitystate) + .where(availabilitystate.c.id == row.id) + .values( + url=metadata.get("url"), + links=metadata.get("links"), + ), + ) + + with op.batch_alter_table("availabilitystate", schema=None) as batch_op: + batch_op.drop_column("materialization_id") + batch_op.drop_column("custom_metadata") diff --git a/datajunction-server/datajunction_server/api/__init__.py b/datajunction-server/datajunction_server/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/datajunction_server/api/access/authentication/__init__.py b/datajunction-server/datajunction_server/api/access/authentication/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/datajunction_server/api/access/authentication/basic.py b/datajunction-server/datajunction_server/api/access/authentication/basic.py new file mode 100644 index 000000000..9e3587825 --- /dev/null +++ b/datajunction-server/datajunction_server/api/access/authentication/basic.py @@ -0,0 +1,105 @@ +""" +Basic OAuth Authentication Router +""" + +from datetime import timedelta +from http import HTTPStatus + +from fastapi import APIRouter, Depends, Form +from fastapi.responses import JSONResponse, Response +from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.constants import AUTH_COOKIE, LOGGED_IN_FLAG_COOKIE +from datajunction_server.database.user import OAuthProvider, User +from datajunction_server.errors import DJAlreadyExistsException, DJError, ErrorCode +from datajunction_server.internal.access.authentication.basic import ( + get_password_hash, + validate_user_password, +) +from datajunction_server.internal.access.authentication.tokens import create_token +from datajunction_server.utils import Settings, get_session, get_settings + +router = APIRouter(tags=["Basic OAuth2"]) + + +@router.post("/basic/user/") +async def create_a_user( + email: str = Form(), + username: str = Form(), + password: str = Form(), + session: AsyncSession = Depends(get_session), +) -> JSONResponse: + """ + Create a new user + """ + user_result = await session.execute(select(User).where(User.username == username)) + if user_result.unique().scalar_one_or_none(): + raise DJAlreadyExistsException( + errors=[ + DJError( + code=ErrorCode.ALREADY_EXISTS, + message=f"User {username} already exists.", + ), + ], + ) + new_user = User( + email=email, + username=username, + password=get_password_hash(password), + oauth_provider=OAuthProvider.BASIC, + ) + session.add(new_user) + await session.commit() + await session.refresh(new_user) + return JSONResponse( + content={"message": "User successfully created"}, + status_code=HTTPStatus.CREATED, + ) + + +@router.post("/basic/login/") +async def login( + form_data: OAuth2PasswordRequestForm = Depends(), + session: AsyncSession = Depends(get_session), + settings: Settings = Depends(get_settings), +): + """ + Get a JWT token and set it as an HTTP only cookie + """ + user = await validate_user_password( + username=form_data.username, + password=form_data.password, + session=session, + ) + response = JSONResponse( + content={"message": "Logged in successfully"}, + status_code=HTTPStatus.OK, + ) + response.set_cookie( + AUTH_COOKIE, + create_token( + {"username": user.username}, + secret=settings.secret, + iss=settings.url, + expires_delta=timedelta(days=365), + ), + httponly=True, + ) + response.set_cookie( + LOGGED_IN_FLAG_COOKIE, + "true", + ) + return response + + +@router.post("/logout/") +def logout(): + """ + Logout a user by deleting the auth cookie + """ + response = Response(status_code=HTTPStatus.OK) + response.delete_cookie(AUTH_COOKIE, httponly=True) + response.delete_cookie(LOGGED_IN_FLAG_COOKIE) + return response diff --git a/datajunction-server/datajunction_server/api/access/authentication/github.py b/datajunction-server/datajunction_server/api/access/authentication/github.py new file mode 100644 index 000000000..2f5f44a5b --- /dev/null +++ b/datajunction-server/datajunction_server/api/access/authentication/github.py @@ -0,0 +1,118 @@ +""" +GitHub OAuth Authentication Router +""" + +import logging +from datetime import timedelta +from http import HTTPStatus + +import requests +from fastapi import APIRouter, Depends, Response +from fastapi.responses import JSONResponse, RedirectResponse + +from datajunction_server.constants import AUTH_COOKIE, LOGGED_IN_FLAG_COOKIE +from datajunction_server.errors import ( + DJAuthenticationException, + DJConfigurationException, + DJError, + ErrorCode, +) +from datajunction_server.internal.access.authentication import github +from datajunction_server.internal.access.authentication.tokens import create_token +from datajunction_server.utils import Settings, get_settings + +_logger = logging.getLogger(__name__) +router = APIRouter(tags=["GitHub OAuth2"]) + + +@router.get("/github/login/", status_code=HTTPStatus.FOUND) +def login() -> RedirectResponse: # pragma: no cover + """ + Login + """ + settings = get_settings() + if not settings.github_oauth_client_id: + raise DJConfigurationException( + http_status_code=HTTPStatus.NOT_IMPLEMENTED, + errors=[ + DJError( + code=ErrorCode.OAUTH_ERROR, + message="GITHUB_OAUTH_CLIENT_ID is not set", + ), + ], + ) + return RedirectResponse( + url=github.get_authorize_url(oauth_client_id=settings.github_oauth_client_id), + status_code=HTTPStatus.FOUND, + ) + + +@router.get("/github/token/") +def get_access_token( + code: str, + response: Response, + settings: Settings = Depends(get_settings), +) -> JSONResponse: # pragma: no cover + """ + Get an access token using OAuth code + """ + settings = get_settings() + params = { + "client_id": settings.github_oauth_client_id, + "client_secret": settings.github_oauth_client_secret, + "code": code, + } + headers = {"Accept": "application/json"} + access_data = requests.post( + url="https://github.com/login/oauth/access_token", + params=params, + headers=headers, + timeout=10, # seconds + ).json() + if "error" in access_data: + raise DJAuthenticationException( + errors=[ + DJError( + code=ErrorCode.OAUTH_ERROR, + message=( + "Received an error from the GitHub authorization " + f"server: {access_data['error']}" + ), + ), + ], + ) + if "access_token" not in access_data: + message = "No user access token retrieved from GitHub OAuth API" + _logger.error(message) + raise DJAuthenticationException( + errors=[DJError(message=message, code=ErrorCode.OAUTH_ERROR)], + ) + user = github.get_github_user(access_data["access_token"]) + response = RedirectResponse( + url=settings.frontend_host, + ) + if not user: + raise DJAuthenticationException( + http_status_code=HTTPStatus.UNAUTHORIZED, + errors=DJError( + code=ErrorCode.OAUTH_ERROR, + message=( + "Could not retrieve user using the GitHub provided access token" + ), + ), + ) + response.set_cookie( + AUTH_COOKIE, + create_token( + {"username": user.username}, + secret=settings.secret, + iss=settings.url, + expires_delta=timedelta(days=365), + ), + httponly=True, + ) + response.set_cookie( + LOGGED_IN_FLAG_COOKIE, + "true", + ) + return response diff --git a/datajunction-server/datajunction_server/api/access/authentication/google.py b/datajunction-server/datajunction_server/api/access/authentication/google.py new file mode 100644 index 000000000..e94381f3e --- /dev/null +++ b/datajunction-server/datajunction_server/api/access/authentication/google.py @@ -0,0 +1,120 @@ +""" +Google OAuth Router +""" + +import logging +import secrets +from datetime import timedelta +from http import HTTPStatus +from typing import Optional +from urllib.parse import urljoin, urlparse + +import google.auth.transport.requests +import google.oauth2.credentials +import requests +from fastapi import APIRouter, Depends, Request +from google.oauth2 import id_token +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from starlette.responses import RedirectResponse + +from datajunction_server.constants import AUTH_COOKIE, LOGGED_IN_FLAG_COOKIE +from datajunction_server.database.user import User +from datajunction_server.errors import DJAuthenticationException +from datajunction_server.internal.access.authentication.basic import get_password_hash +from datajunction_server.internal.access.authentication.google import ( + flow, + get_authorize_url, +) +from datajunction_server.internal.access.authentication.tokens import create_token +from datajunction_server.models.user import OAuthProvider +from datajunction_server.utils import Settings, get_session, get_settings + +_logger = logging.getLogger(__name__) +router = APIRouter(tags=["Google OAuth"]) +settings = get_settings() + + +@router.get("/google/login/", status_code=HTTPStatus.FOUND) +def login(target: Optional[str] = None): + """ + Login using Google OAuth + """ + return RedirectResponse( + url=get_authorize_url(state=target), + status_code=HTTPStatus.FOUND, + ) + + +@router.get("/google/token/") +async def get_access_token( + request: Request, + state: Optional[str] = None, + error: Optional[str] = None, + session: AsyncSession = Depends(get_session), + setting: Settings = Depends(get_settings), +): + """ + Perform a token exchange, exchanging a google auth code for a google access token. + The google access token is then used to request user information and return a JWT + cookie. If the user does not already exist, a new user is created. + """ + if error: + raise DJAuthenticationException( + f"Ran into an error during Google auth: {error}", + ) + hostname = urlparse(settings.url).hostname + url = str(request.url) + flow.fetch_token(authorization_response=url) + credentials = flow.credentials + request_session = requests.session() + token_request = google.auth.transport.requests.Request(session=request_session) + user_data = id_token.verify_oauth2_token( + id_token=credentials._id_token, + request=token_request, + audience=setting.google_oauth_client_id, + ) + + existing_user = ( + await session.execute( + select(User).where(User.email == user_data["email"]), + ) + ).scalar() + if existing_user: + _logger.info("OAuth user found") + user = existing_user + else: + _logger.info("OAuth user does not exist, creating a new user") + new_user = User( + username=user_data["email"], + email=user_data["email"], + password=get_password_hash(secrets.token_urlsafe(13)), + name=user_data["name"], + oauth_provider=OAuthProvider.GOOGLE, + ) + session.add(new_user) + await session.commit() + await session.refresh(new_user) + user = new_user + response = RedirectResponse(url=urljoin(settings.frontend_host, state)) # type: ignore + response.set_cookie( + AUTH_COOKIE, + create_token( + {"username": user.email}, + secret=settings.secret, + iss=settings.url, + expires_delta=timedelta(days=365), + ), + httponly=True, + samesite="none", + secure=True, + domain=hostname, + ) + response.set_cookie( + LOGGED_IN_FLAG_COOKIE, + "true", + samesite="none", + secure=True, + domain=hostname, + ) + return response diff --git a/datajunction-server/datajunction_server/api/access/authentication/whoami.py b/datajunction-server/datajunction_server/api/access/authentication/whoami.py new file mode 100644 index 000000000..f96eaf627 --- /dev/null +++ b/datajunction-server/datajunction_server/api/access/authentication/whoami.py @@ -0,0 +1,53 @@ +""" +Router for getting the current active user +""" + +from datetime import timedelta +from http import HTTPStatus + +from fastapi import Depends, Request +from fastapi.responses import JSONResponse + +from datajunction_server.database.user import User +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.access.authentication.tokens import create_token +from datajunction_server.models.user import UserOutput +from datajunction_server.utils import ( + Settings, + get_and_update_current_user, + get_settings, +) + +router = SecureAPIRouter(tags=["Who am I?"]) + + +@router.get("/whoami/") +async def whoami( + current_user: User = Depends(get_and_update_current_user), +): + """ + Returns the current authenticated user + """ + return UserOutput.from_orm(current_user) + + +@router.get("/token/") +async def get_short_lived_token( + request: Request, + settings: Settings = Depends(get_settings), +) -> JSONResponse: + """ + Returns a token that expires in 24 hours + """ + expires_delta = timedelta(hours=24) + return JSONResponse( + status_code=HTTPStatus.OK, + content={ + "token": create_token( + {"username": request.state.user.username}, + secret=settings.secret, + iss=settings.url, + expires_delta=expires_delta, + ), + }, + ) diff --git a/datajunction-server/datajunction_server/api/attributes.py b/datajunction-server/datajunction_server/api/attributes.py new file mode 100644 index 000000000..7cd1e5a6d --- /dev/null +++ b/datajunction-server/datajunction_server/api/attributes.py @@ -0,0 +1,129 @@ +""" +Attributes related APIs. +""" + +import logging +from typing import List + +from fastapi import Depends +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database.attributetype import AttributeType +from datajunction_server.errors import DJAlreadyExistsException, DJInvalidInputException +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.models.attribute import ( + RESERVED_ATTRIBUTE_NAMESPACE, + AttributeTypeBase, + ColumnAttributes, + MutableAttributeTypeFields, +) +from datajunction_server.models.node_type import NodeType +from datajunction_server.utils import get_session, get_settings + +_logger = logging.getLogger(__name__) +settings = get_settings() +router = SecureAPIRouter(tags=["attributes"]) + + +@router.get("/attributes/", response_model=List[AttributeTypeBase]) +async def list_attributes( + *, session: AsyncSession = Depends(get_session) +) -> List[AttributeTypeBase]: + """ + List all available attribute types. + """ + attributes = await AttributeType.get_all(session) + return [AttributeTypeBase.from_orm(attr) for attr in attributes] + + +@router.post( + "/attributes/", + response_model=AttributeTypeBase, + status_code=201, + name="Add an Attribute Type", +) +async def add_attribute_type( + data: MutableAttributeTypeFields, *, session: AsyncSession = Depends(get_session) +) -> AttributeTypeBase: + """ + Add a new attribute type + """ + if data.namespace == RESERVED_ATTRIBUTE_NAMESPACE: + raise DJInvalidInputException( + message="Cannot use `system` as the attribute type namespace as it is reserved.", + ) + attribute_type = await AttributeType.get_by_name(session, data.name) + if attribute_type: + raise DJAlreadyExistsException( + message=f"Attribute type `{data.name}` already exists!", + ) + attribute_type = await AttributeType.create(session, data) + return AttributeTypeBase.from_orm(attribute_type) + + +async def default_attribute_types(session: AsyncSession = Depends(get_session)): + """ + Loads all the column attribute types that are supported by the system + by default into the database. + """ + defaults = [ + AttributeType( + namespace=RESERVED_ATTRIBUTE_NAMESPACE, + name=ColumnAttributes.PRIMARY_KEY.value, + description="Points to a column which is part of the primary key of the node", + uniqueness_scope=[], + allowed_node_types=[ + NodeType.SOURCE, + NodeType.TRANSFORM, + NodeType.DIMENSION, + ], + ), + AttributeType( + namespace=RESERVED_ATTRIBUTE_NAMESPACE, + name=ColumnAttributes.DIMENSION.value, + description="Points to a dimension attribute column", + uniqueness_scope=[], + allowed_node_types=[NodeType.SOURCE, NodeType.TRANSFORM], + ), + AttributeType( + namespace=RESERVED_ATTRIBUTE_NAMESPACE, + name=ColumnAttributes.HIDDEN.value, + description=( + "Points to a dimension column that's not useful " + "for end users and should be hidden" + ), + uniqueness_scope=[], + allowed_node_types=[NodeType.DIMENSION], + ), + ] + default_attribute_type_names = {type_.name: type_ for type_ in defaults} + + # Update existing default attribute types + statement = select(AttributeType).filter( + AttributeType.name.in_( # type: ignore + set(default_attribute_type_names.keys()), + ), + ) + attribute_types = (await session.execute(statement)).scalars().all() + for type_ in attribute_types: + # if type_: # pragma: no cover + type_.name = default_attribute_type_names[type_.name].name + type_.namespace = default_attribute_type_names[type_.name].namespace + type_.description = default_attribute_type_names[type_.name].description + type_.allowed_node_types = default_attribute_type_names[ + type_.name + ].allowed_node_types + type_.uniqueness_scope = default_attribute_type_names[ + type_.name + ].uniqueness_scope + session.add(type_) + + # Add new default attribute types + new_types = set(default_attribute_type_names.keys()) - { + type_.name for type_ in attribute_types if type_ + } + session.add_all( + [default_attribute_type_names[name] for name in new_types], + ) + await session.commit() diff --git a/datajunction-server/datajunction_server/api/catalogs.py b/datajunction-server/datajunction_server/api/catalogs.py new file mode 100644 index 000000000..5b9c49cce --- /dev/null +++ b/datajunction-server/datajunction_server/api/catalogs.py @@ -0,0 +1,163 @@ +""" +Catalog related APIs. +""" + +import logging +from http import HTTPStatus +from typing import List + +from fastapi import Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload + +from datajunction_server.api.engines import EngineInfo +from datajunction_server.api.helpers import get_catalog_by_name +from datajunction_server.database.catalog import Catalog +from datajunction_server.database.engine import Engine +from datajunction_server.errors import DJException +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.engines import get_engine +from datajunction_server.models.catalog import CatalogInfo +from datajunction_server.utils import get_session, get_settings + +_logger = logging.getLogger(__name__) +settings = get_settings() +router = SecureAPIRouter(tags=["catalogs"]) + +UNKNOWN_CATALOG_ID = 0 + + +@router.get("/catalogs/", response_model=List[CatalogInfo]) +async def list_catalogs( + *, session: AsyncSession = Depends(get_session) +) -> List[CatalogInfo]: + """ + List all available catalogs + """ + statement = select(Catalog).options(joinedload(Catalog.engines)) + return [ + CatalogInfo.from_orm(catalog) + for catalog in (await session.execute(statement)).unique().scalars() + ] + + +@router.get("/catalogs/{name}/", response_model=CatalogInfo, name="Get a Catalog") +async def get_catalog( + name: str, *, session: AsyncSession = Depends(get_session) +) -> CatalogInfo: + """ + Return a catalog by name + """ + return await get_catalog_by_name(session, name) + + +@router.post( + "/catalogs/", + response_model=CatalogInfo, + status_code=201, + name="Add A Catalog", +) +async def add_catalog( + data: CatalogInfo, + *, + session: AsyncSession = Depends(get_session), +) -> CatalogInfo: + """ + Add a Catalog + """ + try: + await get_catalog_by_name(session, data.name) + except DJException: + pass + else: + raise HTTPException( + status_code=HTTPStatus.CONFLICT, + detail=f"Catalog already exists: `{data.name}`", + ) + + catalog = Catalog( + name=data.name, + engines=[ + Engine( + name=engine.name, + version=engine.version, + uri=engine.uri, + dialect=engine.dialect, + ) + for engine in data.engines # type: ignore + ], + ) + catalog.engines.extend( + await list_new_engines( + session=session, + catalog=catalog, + create_engines=data.engines, # type: ignore + ), + ) + session.add(catalog) + await session.commit() + await session.refresh(catalog, ["engines"]) + + return CatalogInfo.from_orm(catalog) + + +@router.post( + "/catalogs/{name}/engines/", + response_model=CatalogInfo, + status_code=201, + name="Add Engines to a Catalog", +) +async def add_engines_to_catalog( + name: str, + data: List[EngineInfo], + *, + session: AsyncSession = Depends(get_session), +) -> CatalogInfo: + """ + Attach one or more engines to a catalog + """ + catalog = await get_catalog_by_name(session, name) + catalog.engines.extend( + await list_new_engines(session=session, catalog=catalog, create_engines=data), + ) + session.add(catalog) + await session.commit() + await session.refresh(catalog) + return CatalogInfo.from_orm(catalog) + + +async def list_new_engines( + session: AsyncSession, + catalog: Catalog, + create_engines: List[EngineInfo], +) -> List[Engine]: + """ + Filter to engines that are not already set on a catalog + """ + new_engines = [] + for engine_ref in create_engines: + already_set = False + engine = await get_engine(session, engine_ref.name, engine_ref.version) + for set_engine in catalog.engines: + if engine.name == set_engine.name and engine.version == set_engine.version: + already_set = True + if not already_set: + new_engines.append(engine) + return new_engines + + +async def default_catalog(session: AsyncSession = Depends(get_session)): + """ + Loads a default catalog for nodes that are pure SQL and don't belong in any + particular catalog. This typically applies to on-the-fly user-defined dimensions. + """ + statement = select(Catalog).filter(Catalog.id == UNKNOWN_CATALOG_ID) + catalogs = (await session.execute(statement)).all() + if not catalogs: + unknown = Catalog( + id=UNKNOWN_CATALOG_ID, + name="unknown", + ) + session.add(unknown) + await session.commit() diff --git a/datajunction-server/datajunction_server/api/client.py b/datajunction-server/datajunction_server/api/client.py new file mode 100644 index 000000000..c92b96fec --- /dev/null +++ b/datajunction-server/datajunction_server/api/client.py @@ -0,0 +1,213 @@ +""" +APIs related to generating client code used for performing various actions in DJ. +""" + +import json +import logging +import os +import tempfile +from datetime import datetime +from typing import Dict, Optional, cast + +from fastapi import BackgroundTasks, Depends, Request +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload, selectinload +from starlette.responses import FileResponse + +from datajunction_server.database import Node, NodeNamespace, NodeRevision +from datajunction_server.errors import DJInvalidInputException +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.client import ( + build_export_notebook, + python_client_code_for_linking_complex_dimension, + python_client_create_node, + python_client_initialize, +) +from datajunction_server.models.materialization import MaterializationJobTypeEnum +from datajunction_server.models.node_type import NodeType +from datajunction_server.sql.dag import get_upstream_nodes +from datajunction_server.utils import SEPARATOR, get_session, get_settings + +_logger = logging.getLogger(__name__) +settings = get_settings() +router = SecureAPIRouter(tags=["client"]) + + +@router.get("/datajunction-clients/python/new_node/{node_name}", response_model=str) +async def client_code_for_creating_node( + node_name: str, + *, + include_client_setup: bool = True, + session: AsyncSession = Depends(get_session), + replace_namespace: Optional[str] = None, + request: Request, +) -> str: + """ + Generate the Python client code used for creating this node + """ + client_setup = ( + python_client_initialize(str(request.url)) if include_client_setup else "" + ) + client_code = await python_client_create_node(session, node_name, replace_namespace) + return client_setup + "\n\n" + client_code # type: ignore + + +@router.get( + "/datajunction-clients/python/dimension_links/{node_name}", + response_model=str, +) +async def client_code_for_dimension_links_on_node( + node_name: str, + *, + include_client_setup: bool = True, + session: AsyncSession = Depends(get_session), + replace_namespace: Optional[str] = None, + request: Request, +) -> str: + """ + Generate the Python client code used for creating this node + """ + node = await Node.get_by_name( + session, + node_name, + options=[ + joinedload(Node.current).options( + selectinload(NodeRevision.dimension_links), + ), + ], + ) + code = [python_client_initialize(str(request.url))] if include_client_setup else [] + short_name = node_name.split(SEPARATOR)[-1] + code += [f'{short_name} = dj.node("{node_name}")'] + for link in node.current.dimension_links: # type: ignore + code.append( + python_client_code_for_linking_complex_dimension( + node_name, + link, + replace_namespace, + ), + ) + return "\n\n".join(code) + + +@router.get( + "/datajunction-clients/python/add_materialization/{node_name}/{materialization_name}", + response_model=str, +) +async def client_code_for_adding_materialization( + node_name: str, + materialization_name: str, + *, + session: AsyncSession = Depends(get_session), +) -> str: + """ + Generate the Python client code used for adding this materialization + """ + node_short_name = node_name.split(".")[-1] + node = await Node.get_by_name( + session, + node_name, + options=[ + joinedload(Node.current).options( + selectinload(NodeRevision.materializations), + ), + ], + ) + materialization = [ + materialization + for materialization in node.current.materializations # type: ignore + if materialization.name == materialization_name + ][0] + user_modified_config = { + key: materialization.config[key] + for key in materialization.config + if key in ("partitions", "spark", "druid", "") + } + with_b = "\n".join( + [ + f" {line}" + for line in json.dumps(user_modified_config, indent=4).split("\n") + ], + ) + client_code = f"""dj = DJBuilder(DJ_URL) + +{node_short_name} = dj.{node.type if node else ""}( + "{node.name if node else ""}" +) +materialization = MaterializationConfig( + job="{MaterializationJobTypeEnum.find_match(materialization.job).name.lower()}", + strategy="{materialization.strategy}", + schedule="{materialization.schedule}", + config={with_b.strip()}, +) +{node_short_name}.add_materialization( + materialization +)""" + return client_code # type: ignore + + +@router.get( + "/datajunction-clients/python/notebook", +) +async def notebook_for_exporting_nodes( + *, + namespace: Optional[str] = None, + cube: Optional[str] = None, + include_dimensions: bool = False, + include_sources: bool = False, + session: AsyncSession = Depends(get_session), + background_tasks: BackgroundTasks, + request: Request, +) -> FileResponse: + """ + Generate the Python client code used for exporting multiple nodes. There are two options: + * namespace: If `namespace` is specified, the generated notebook will contain Python client + code to export all nodes in the namespace. + * cube: If `cube` is specified, the generated notebook will contain Python client code + used for exporting a cube, including all metrics and dimensions referenced in the cube. + """ + if namespace and cube: + raise DJInvalidInputException( + "Can only specify export of either a namespace or a cube.", + ) + + if namespace: + nodes = await NodeNamespace.list_all_nodes(session, namespace) + introduction = ( + f"## DJ Namespace Export\n\n" + f"Exported `{namespace}`\n\n(As of {datetime.now()})" + ) + else: + nodes = await get_upstream_nodes(session, cast(str, cube)) + cube_node = cast( + Node, + await Node.get_by_name(session, cast(str, cube), raise_if_not_exists=True), + ) + nodes += [cube_node] + if not include_dimensions: + nodes = [node for node in nodes if node.type != NodeType.DIMENSION] + if not include_sources: + nodes = [node for node in nodes if node.type != NodeType.SOURCE] + introduction = ( + f"## DJ Cube Export\n\n" + f"Exported `{cube}` {cube_node.current_version}\n\n(As of {datetime.now()})" + ) + + notebook = await build_export_notebook(session, nodes, introduction, request.url) + return notebook_file_response(notebook, background_tasks) + + +def notebook_file_response(notebook: Dict, background_tasks: BackgroundTasks): + """ + Write the notebook contents to a temporary file and prepare a file response + with appropriate headers so that the API returns a downloadable .ipynb file. + """ + file_descriptor, path = tempfile.mkstemp(suffix=".ipynb") + with os.fdopen(file_descriptor, "w") as file: + file.write(json.dumps(notebook)) + + background_tasks.add_task(os.unlink, path) + headers = { + "Content-Disposition": 'attachment; filename="export.ipynb"', + } + return FileResponse(path, headers=headers) diff --git a/datajunction-server/datajunction_server/api/collection.py b/datajunction-server/datajunction_server/api/collection.py new file mode 100644 index 000000000..7c2dba0b6 --- /dev/null +++ b/datajunction-server/datajunction_server/api/collection.py @@ -0,0 +1,162 @@ +""" +Collection related APIs. +""" + +import logging +from http import HTTPStatus +from typing import List + +from fastapi import Depends, HTTPException, Response +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql.operators import is_ + +from datajunction_server.database.collection import Collection +from datajunction_server.database.node import Node +from datajunction_server.database.user import User +from datajunction_server.errors import DJException +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.models.collection import CollectionDetails, CollectionInfo +from datajunction_server.utils import ( + get_and_update_current_user, + get_session, + get_settings, +) + +_logger = logging.getLogger(__name__) +settings = get_settings() +router = SecureAPIRouter(tags=["collections"]) + + +@router.post( + "/collections/", + response_model=CollectionInfo, + status_code=HTTPStatus.CREATED, +) +async def create_a_collection( + data: CollectionInfo, + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> CollectionInfo: + """ + Create a Collection + """ + try: + await Collection.get_by_name(session, data.name, raise_if_not_exists=True) + except DJException: + pass + else: + raise HTTPException( + status_code=HTTPStatus.CONFLICT, + detail=f"Collection already exists: `{data.name}`", + ) + + collection = Collection( + name=data.name, + description=data.description, + created_by_id=current_user.id, + ) + session.add(collection) + await session.commit() + await session.refresh(collection) + + return CollectionInfo.from_orm(collection) + + +@router.delete("/collections/{name}", status_code=HTTPStatus.NO_CONTENT) +async def delete_a_collection( + name: str, + *, + session: AsyncSession = Depends(get_session), +): + """ + Delete a collection + """ + collection = await Collection.get_by_name(session, name) + await session.delete(collection) + await session.commit() + return Response(status_code=HTTPStatus.NO_CONTENT) + + +@router.get("/collections/") +async def list_collections( + *, + session: AsyncSession = Depends(get_session), +) -> List[CollectionInfo]: + """ + List all collections + """ + collections = await session.execute( + select(Collection).where(is_(Collection.deactivated_at, None)), + ) + return collections.scalars().all() + + +@router.get("/collections/{name}") +async def get_collection( + name: str, + *, + session: AsyncSession = Depends(get_session), +) -> CollectionDetails: + """ + Get a collection and its nodes + """ + collection = await Collection.get_by_name( + session, + name=name, + raise_if_not_exists=True, + ) + return collection # type: ignore + + +@router.post( + "/collections/{name}/nodes/", + status_code=HTTPStatus.NO_CONTENT, + name="Add Nodes to a Collection", +) +async def add_nodes_to_collection( + name: str, + data: List[str], + *, + session: AsyncSession = Depends(get_session), +): + """ + Add one or more nodes to a collection + """ + collection = await Collection.get_by_name(session, name, raise_if_not_exists=True) + nodes = await Node.get_by_names(session=session, names=data) + if not nodes: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Cannot add nodes to collection, no nodes found: `{data}`", + ) + collection.nodes.extend(nodes) # type: ignore + session.add(collection) + await session.commit() + await session.refresh(collection) + return Response(status_code=HTTPStatus.NO_CONTENT) + + +@router.post( + "/collections/{name}/remove/", + status_code=HTTPStatus.NO_CONTENT, + name="Delete Nodes from a Collection", +) +async def delete_nodes_from_collection( + name: str, + data: List[str], + *, + session: AsyncSession = Depends(get_session), +): + """ + Delete one or more nodes from a collection + """ + collection = await Collection.get_by_name(session, name) + nodes = await Node.get_by_names(session=session, names=data) + for node in nodes: + if node in collection.nodes: # type: ignore + collection.nodes.remove(node) # type: ignore + await session.commit() + await session.refresh(collection) + return Response(status_code=HTTPStatus.NO_CONTENT) diff --git a/datajunction-server/datajunction_server/api/cubes.py b/datajunction-server/datajunction_server/api/cubes.py new file mode 100644 index 000000000..0a493a98b --- /dev/null +++ b/datajunction-server/datajunction_server/api/cubes.py @@ -0,0 +1,267 @@ +""" +Cube related APIs. +""" + +import logging +from typing import List, Optional + +from fastapi import Depends, Query, Request +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.api.helpers import get_catalog_by_name +from datajunction_server.construction.dimensions import build_dimensions_from_cube_query +from datajunction_server.database.node import Node +from datajunction_server.database.user import User +from datajunction_server.errors import DJInvalidInputException +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.access.authorization import validate_access +from datajunction_server.internal.materializations import build_cube_materialization +from datajunction_server.internal.nodes import get_cube_revision_metadata +from datajunction_server.models import access +from datajunction_server.models.cube import ( + CubeRevisionMetadata, + DimensionValue, + DimensionValues, +) +from datajunction_server.models.cube_materialization import ( + DruidCubeMaterializationInput, + UpsertCubeMaterialization, +) +from datajunction_server.models.materialization import ( + Granularity, + MaterializationJobTypeEnum, + MaterializationStrategy, +) +from datajunction_server.models.metric import TranslatedSQL +from datajunction_server.models.query import QueryCreate +from datajunction_server.naming import from_amenable_name +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.utils import ( + get_and_update_current_user, + get_query_service_client, + get_session, + get_settings, +) + +_logger = logging.getLogger(__name__) +settings = get_settings() +router = SecureAPIRouter(tags=["cubes"]) + + +@router.get("/cubes/{name}/", name="Get a Cube") +async def get_cube( + name: str, *, session: AsyncSession = Depends(get_session) +) -> CubeRevisionMetadata: + """ + Get information on a cube + """ + return await get_cube_revision_metadata(session, name) + + +@router.get("/cubes/{name}/materialization", name="Cube Materialization Config") +async def cube_materialization_info( + name: str, + session: AsyncSession = Depends(get_session), +) -> DruidCubeMaterializationInput: + """ + The standard cube materialization config. DJ makes sensible materialization choices + where possible. + + Requirements: + - The cube must have a temporal partition column specified. + - The job strategy will always be "incremental time". + + Outputs: + "measures_materializations": + We group the metrics by parent node. Then we try to pre-aggregate each parent node as + much as possible to prepare for metric queries on the cube's dimensions. + "combiners": + We combine each set of measures materializations on their shared grain. Note that we don't + support materializing cubes with measures materializations that don't share the same grain. + However, we keep `combiners` as a list in the eventual future where we support that. + "metrics": + We include a list of metrics, their required measures, and the derived expression (e.g., the + expression used by the metric that makes use of the pre-aggregated measures) + + Once we create a scheduled materialization workflow, we freeze the metadata for that particular + materialized dataset. This allows us to reconstruct metrics SQL from the dataset when needed. + To request metrics from the materialized cube, use the metrics' measures metadata. + """ + node = await Node.get_cube_by_name(session, name) + temporal_partitions = node.current.temporal_partition_columns() # type: ignore + if len(temporal_partitions) != 1: + raise DJInvalidInputException( + "The cube must have a single temporal partition column set " + "in order for it to be materialized.", + ) + temporal_partition = temporal_partitions[0] if temporal_partitions else None + granularity_lookback_defaults = { + Granularity.MINUTE: "1 MINUTE", + Granularity.HOUR: "1 HOUR", + Granularity.DAY: "1 DAY", + Granularity.WEEK: "1 WEEK", + Granularity.MONTH: "1 MONTH", + Granularity.QUARTER: "1 QUARTER", + Granularity.YEAR: "1 YEAR", + } + granularity_cron_defaults = { + Granularity.MINUTE: "* * * * *", # Runs every minute + Granularity.HOUR: "0 * * * *", # Runs at the start of every hour + Granularity.DAY: "0 0 * * *", # Runs at midnight every day + Granularity.WEEK: "0 0 * * 0", # Runs at midnight on Sundays + Granularity.MONTH: "0 0 1 * *", # Runs at midnight on the first of every month + Granularity.QUARTER: "0 0 1 */3 *", # Runs at midnight on the first day of each quarter + Granularity.YEAR: "0 0 1 1 *", # Runs at midnight on January 1st every year + } + upsert = UpsertCubeMaterialization( + job=MaterializationJobTypeEnum.DRUID_CUBE, + strategy=( + MaterializationStrategy.INCREMENTAL_TIME + if temporal_partition + else MaterializationStrategy.FULL + ), + lookback_window=granularity_lookback_defaults.get( + temporal_partition.partition.granularity, + granularity_lookback_defaults[Granularity.DAY], + ), + schedule=granularity_cron_defaults.get( + temporal_partition.partition.granularity, + granularity_cron_defaults[Granularity.DAY], + ), + ) + cube_config = await build_cube_materialization( + session, + node.current, # type: ignore + upsert, + ) + return DruidCubeMaterializationInput( + name="", + cube=cube_config.cube, + dimensions=cube_config.dimensions, + metrics=cube_config.metrics, + strategy=upsert.strategy, + schedule=upsert.schedule, + job=upsert.job.name, + measures_materializations=cube_config.measures_materializations, + combiners=cube_config.combiners, + ) + + +@router.get("/cubes/{name}/dimensions/sql", name="Dimensions SQL for Cube") +async def get_cube_dimension_sql( + name: str, + *, + dimensions: List[str] = Query([], description="Dimensions to get values for"), + filters: Optional[str] = Query( + None, + description="Filters on dimensional attributes", + ), + limit: Optional[int] = Query( + None, + description="Number of rows to limit the data retrieved to", + ), + include_counts: bool = False, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> TranslatedSQL: + """ + Generates SQL to retrieve all unique values of a dimension for the cube + """ + node = await Node.get_cube_by_name(session, name) + node_revision = node.current # type: ignore + return await build_dimensions_from_cube_query( + session, + node_revision, + dimensions, + current_user, + validate_access, + filters, + limit, + include_counts, + ) + + +@router.get( + "/cubes/{name}/dimensions/data", + name="Dimensions Values for Cube", +) +async def get_cube_dimension_values( + name: str, + *, + dimensions: List[str] = Query([], description="Dimensions to get values for"), + filters: Optional[str] = Query( + None, + description="Filters on dimensional attributes", + ), + limit: Optional[int] = Query( + None, + description="Number of rows to limit the data retrieved to", + ), + include_counts: bool = False, + async_: bool = False, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> DimensionValues: + """ + All unique values of a dimension from the cube + """ + request_headers = dict(request.headers) + node = await Node.get_cube_by_name(session, name) + cube = node.current # type: ignore + translated_sql = await build_dimensions_from_cube_query( + session, + cube, + dimensions, + current_user, + validate_access, + filters, + limit, + include_counts, + ) + if cube.availability: + catalog = await get_catalog_by_name( # pragma: no cover + session, + cube.availability.catalog, # type: ignore + ) + else: + catalog = cube.catalog + query_create = QueryCreate( + engine_name=catalog.engines[0].name, + catalog_name=catalog.name, + engine_version=catalog.engines[0].version, + submitted_query=translated_sql.sql, + async_=async_, + ) + result = query_service_client.submit_query( + query_create, + request_headers=request_headers, + ) + count_column = [ + idx + for idx, col in enumerate(translated_sql.columns) # type: ignore + if col.name == "count" + ] + dimension_values = [ # pragma: no cover + DimensionValue( + value=row[0 : count_column[0]] if count_column else row, + count=row[count_column[0]] if count_column else None, + ) + for row in result.results.__root__[0].rows + ] + return DimensionValues( # pragma: no cover + dimensions=[ + from_amenable_name(col.name) + for col in translated_sql.columns # type: ignore + if col.name != "count" + ], + values=dimension_values, + cardinality=len(dimension_values), + ) diff --git a/datajunction-server/datajunction_server/api/data.py b/datajunction-server/datajunction_server/api/data.py new file mode 100644 index 000000000..39ebe4d9e --- /dev/null +++ b/datajunction-server/datajunction_server/api/data.py @@ -0,0 +1,485 @@ +""" +Data related APIs. +""" + +import logging +from typing import Callable, Dict, List, Optional + +from fastapi import BackgroundTasks, Depends, Query, Request +from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload, selectinload +from sse_starlette.sse import EventSourceResponse + +from datajunction_server.api.helpers import ( + build_sql_for_multiple_metrics, + query_event_stream, +) +from datajunction_server.api.notifications import get_notifier +from datajunction_server.api.sql import get_node_sql +from datajunction_server.database.availabilitystate import AvailabilityState +from datajunction_server.database.history import ActivityType, EntityType, History +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.user import User +from datajunction_server.errors import ( + DJInvalidInputException, + DJQueryServiceClientException, +) +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.access.authorization import ( + validate_access, + validate_access_requests, +) +from datajunction_server.internal.engines import get_engine +from datajunction_server.models import access +from datajunction_server.models.node import AvailabilityStateBase +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.query import QueryCreate, QueryWithResults +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.utils import ( + get_and_update_current_user, + get_query_service_client, + get_session, + get_settings, +) + +_logger = logging.getLogger(__name__) + +settings = get_settings() +router = SecureAPIRouter(tags=["data"]) + + +@router.post("/data/{node_name}/availability", name="Add Availability State to Node") +async def add_availability_state( + node_name: str, + data: AvailabilityStateBase, + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), + notify: Callable = Depends(get_notifier), +) -> JSONResponse: + """ + Add an availability state to a node. + """ + _logger.info("Storing availability for node=%s", node_name) + + node = await Node.get_by_name( + session, + node_name, + options=[ + joinedload(Node.current).options( + selectinload(NodeRevision.catalog), + selectinload(NodeRevision.availability), + ), + ], + raise_if_not_exists=True, + ) + + # Source nodes require that any availability states set are for one of the defined tables + node_revision = node.current # type: ignore + validate_access_requests( + validate_access, + current_user, + [ + access.ResourceRequest( + verb=access.ResourceRequestVerb.WRITE, + access_object=access.Resource.from_node(node_revision), + ), + ], + True, + ) + + if node.current.type == NodeType.SOURCE: # type: ignore + if ( + data.catalog != node_revision.catalog.name + or data.schema_ != node_revision.schema_ + or data.table != node_revision.table + ): + raise DJInvalidInputException( + message=( + "Cannot set availability state, " + "source nodes require availability " + "states to match the set table: " + f"{data.catalog}." + f"{data.schema_}." + f"{data.table} " + "does not match " + f"{node_revision.catalog.name}." + f"{node_revision.schema_}." + f"{node_revision.table} " + ), + ) + + # Merge the new availability state with the current availability state if one exists + old_availability = node_revision.availability + if ( + old_availability + and old_availability.catalog == data.catalog + and old_availability.schema_ == data.schema_ + and old_availability.table == data.table + ): + data.merge(node_revision.availability) + + # Update the node with the new availability state + node_revision.availability = AvailabilityState( + catalog=data.catalog, + schema_=data.schema_, + table=data.table, + valid_through_ts=data.valid_through_ts, + custom_metadata=data.custom_metadata, + min_temporal_partition=data.min_temporal_partition, + max_temporal_partition=data.max_temporal_partition, + partitions=[ + partition.dict() if not isinstance(partition, Dict) else partition + for partition in data.partitions # type: ignore + ], + categorical_partitions=data.categorical_partitions, + temporal_partitions=data.temporal_partitions, + materialization_id=data.materialization_id, + ) + if node_revision.availability and not node_revision.availability.partitions: + node_revision.availability.partitions = [] + session.add(node_revision) + event = History( + entity_type=EntityType.AVAILABILITY, + node=node.name, # type: ignore + activity_type=ActivityType.CREATE, + pre=AvailabilityStateBase.from_orm(old_availability).dict() + if old_availability + else {}, + post=AvailabilityStateBase.from_orm(node_revision.availability).dict(), + user=current_user.username, + ) + session.add(event) + await session.commit() + notify(event) + return JSONResponse( + status_code=200, + content={"message": "Availability state successfully posted"}, + ) + + +@router.get("/data/{node_name}/", name="Get Data for a Node") +async def get_data( + node_name: str, + *, + dimensions: List[str] = Query([], description="Dimensional attributes to group by"), + filters: List[str] = Query([], description="Filters on dimensional attributes"), + orderby: List[str] = Query([], description="Expression to order by"), + limit: Optional[int] = Query( + None, + description="Number of rows to limit the data retrieved to", + ), + async_: bool = Query( + default=False, + description="Whether to run the query async or wait for results from the query engine", + ), + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), + background_tasks: BackgroundTasks, +) -> QueryWithResults: + """ + Gets data for a node + """ + request_headers = dict(request.headers) + query, query_request = await get_node_sql( + node_name, + dimensions, + filters, + orderby, + limit, + session=session, + engine_name=engine_name, + engine_version=engine_version, + current_user=current_user, + validate_access=validate_access, + background_tasks=background_tasks, + ) + node = await Node.get_by_name(session, node_name, raise_if_not_exists=True) + available_engines = node.current.catalog.engines # type: ignore + engine = ( + await get_engine( + session, + engine_name or query_request.engine_name, + engine_version or query_request.engine_version, # type: ignore + ) + if engine_name or query_request.engine_name + else available_engines[0] + ) + if engine not in available_engines and engine.name != query_request.engine_name: + raise DJInvalidInputException( # pragma: no cover + f"The selected engine is not available for the node {node_name}. " + f"Available engines include: {', '.join(engine.name for engine in available_engines)}", + ) + + query_create = QueryCreate( + engine_name=engine.name, + catalog_name=node.current.catalog.name, # type: ignore + engine_version=engine.version, + submitted_query=query.sql, + async_=async_, + ) + result = query_service_client.submit_query( + query_create, + request_headers=request_headers, + ) + query_request.query_id = result.id + + # Inject column info if there are results + if result.results.__root__: # pragma: no cover + result.results.__root__[0].columns = query.columns # type: ignore + return result + + +@router.get("/stream/{node_name}", response_model=QueryWithResults) +async def get_data_stream_for_node( + node_name: str, + *, + dimensions: List[str] = Query([], description="Dimensional attributes to group by"), + filters: List[str] = Query([], description="Filters on dimensional attributes"), + orderby: List[str] = Query([], description="Expression to order by"), + limit: Optional[int] = Query( + None, + description="Number of rows to limit the data retrieved to", + ), + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), + background_tasks: BackgroundTasks, +) -> QueryWithResults: + """ + Return data for a node using server side events + """ + request_headers = dict(request.headers) + query, query_request = await get_node_sql( + node_name, + dimensions, + [filter_ for filter_ in filters if filter_], + orderby, + limit, + session=session, + engine_name=engine_name, + engine_version=engine_version, + current_user=current_user, + validate_access=validate_access, + background_tasks=background_tasks, + ) + if query_request and query_request.query_id: + return EventSourceResponse( + query_event_stream( + query=QueryWithResults( + id=query_request.query_id, + submitted_query=query_request.query, + results=[], + errors=[], + ), + query_service_client=query_service_client, + request_headers=request_headers, + columns=query.columns, # type: ignore + request=request, + ), + ) + + node = await Node.get_by_name(session, node_name, raise_if_not_exists=True) + available_engines = node.current.catalog.engines # type: ignore + engine = ( + await get_engine(session, engine_name, engine_version) # type: ignore + if engine_name + else available_engines[0] + ) + if engine not in available_engines: + raise DJInvalidInputException( # pragma: no cover + f"The selected engine is not available for the node {node_name}. " + f"Available engines include: {', '.join(engine.name for engine in available_engines)}", + ) + + query_create = QueryCreate( + engine_name=engine.name, + catalog_name=node.current.catalog.name, # type: ignore + engine_version=engine.version, + submitted_query=query.sql, + async_=True, + ) + initial_query_info = query_service_client.submit_query( + query_create, + request_headers=request_headers, + ) + + # Save the external query id reference + query_request.query_id = initial_query_info.id + session.add(query_request) + await session.commit() + + return EventSourceResponse( + query_event_stream( + query=initial_query_info, + request_headers=request_headers, + query_service_client=query_service_client, + columns=query.columns, # type: ignore + request=request, + ), + ) + + +@router.get( + "/data/query/{query_id}", + response_model=QueryWithResults, + name="Get Data For Query ID", +) +def get_data_for_query( + query_id: str, + *, + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), +) -> QueryWithResults: + """ + Return data for a specific query ID. + """ + request_headers = dict(request.headers) + try: + return query_service_client.get_query( + query_id=query_id, + request_headers=request_headers, + ) + except DJQueryServiceClientException as exc: + raise DJQueryServiceClientException( # pragma: no cover + f"DJ Query Service Error: {exc.message}", + ) from exc + + +@router.get("/data/", response_model=QueryWithResults, name="Get Data For Metrics") +async def get_data_for_metrics( + metrics: List[str] = Query([]), + dimensions: List[str] = Query([]), + filters: List[str] = Query([]), + orderby: List[str] = Query([]), + limit: Optional[int] = None, + async_: bool = False, + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> QueryWithResults: + """ + Return data for a set of metrics with dimensions and filters + """ + request_headers = dict(request.headers) + access_control = access.AccessControlStore( + validate_access=validate_access, + user=current_user, + base_verb=access.ResourceRequestVerb.READ, + ) + + translated_sql, engine, catalog = await build_sql_for_multiple_metrics( + session, + metrics, + dimensions, + filters, + orderby, + limit, + engine_name, + engine_version, + access_control, + ) + + query_create = QueryCreate( + engine_name=engine.name, + catalog_name=catalog.name, + engine_version=engine.version, + submitted_query=translated_sql.sql, + async_=async_, + ) + result = query_service_client.submit_query( + query_create, + request_headers=request_headers, + ) + + # Inject column info if there are results + if result.results.__root__: # pragma: no cover + result.results.__root__[0].columns = translated_sql.columns or [] + return result + + +@router.get("/stream/", response_model=QueryWithResults) +async def get_data_stream_for_metrics( + metrics: List[str] = Query([]), + dimensions: List[str] = Query([]), + filters: List[str] = Query([]), + orderby: List[str] = Query([]), + limit: Optional[int] = None, + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> QueryWithResults: + """ + Return data for a set of metrics with dimensions and filters using server sent events + """ + request_headers = dict(request.headers) + access_control = access.AccessControlStore( + validate_access=validate_access, + user=current_user, + base_verb=access.ResourceRequestVerb.READ, + ) + + translated_sql, engine, catalog = await build_sql_for_multiple_metrics( + session, + metrics, + dimensions, + filters, + orderby, + limit, + engine_name, + engine_version, + access_control, + ) + + query_create = QueryCreate( + engine_name=engine.name, + catalog_name=catalog.name, + engine_version=engine.version, + submitted_query=translated_sql.sql, + async_=True, + ) + # Submits the query, equivalent to calling POST /data/ directly + initial_query_info = query_service_client.submit_query( + query_create, + request_headers=request_headers, + ) + return EventSourceResponse( + query_event_stream( + query=initial_query_info, + request_headers=request_headers, + query_service_client=query_service_client, + columns=translated_sql.columns, # type: ignore + request=request, + ), + ) diff --git a/datajunction-server/datajunction_server/api/dimensions.py b/datajunction-server/datajunction_server/api/dimensions.py new file mode 100644 index 000000000..3e6d3fabf --- /dev/null +++ b/datajunction-server/datajunction_server/api/dimensions.py @@ -0,0 +1,135 @@ +""" +Dimensions related APIs. +""" + +import logging +from typing import List, Optional + +from fastapi import Depends, Query +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.api.helpers import get_node_by_name +from datajunction_server.api.nodes import list_nodes +from datajunction_server.database.node import Node +from datajunction_server.database.user import User +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.access.authorization import ( + validate_access, + validate_access_requests, +) +from datajunction_server.models import access +from datajunction_server.models.node import NodeIndegreeOutput, NodeRevisionOutput +from datajunction_server.models.node_type import NodeType +from datajunction_server.sql.dag import ( + get_dimension_dag_indegree, + get_nodes_with_common_dimensions, + get_nodes_with_dimension, +) +from datajunction_server.utils import ( + get_and_update_current_user, + get_session, + get_settings, +) + +settings = get_settings() +_logger = logging.getLogger(__name__) +router = SecureAPIRouter(tags=["dimensions"]) + + +@router.get("/dimensions/", response_model=List[NodeIndegreeOutput]) +async def list_dimensions( + prefix: Optional[str] = None, + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> List[NodeIndegreeOutput]: + """ + List all available dimensions. + """ + node_names = await list_nodes( + node_type=NodeType.DIMENSION, + prefix=prefix, + session=session, + current_user=current_user, + validate_access=validate_access, + ) + node_indegrees = await get_dimension_dag_indegree(session, node_names) + return sorted( + [ + NodeIndegreeOutput(name=node, indegree=node_indegrees[node]) + for node in node_names + ], + key=lambda n: -n.indegree, + ) + + +@router.get("/dimensions/{name}/nodes/", response_model=List[NodeRevisionOutput]) +async def find_nodes_with_dimension( + name: str, + *, + node_type: List[NodeType] = Query([]), + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> List[NodeRevisionOutput]: + """ + List all nodes that have the specified dimension + """ + dimension_node = await Node.get_by_name(session, name) + nodes = await get_nodes_with_dimension(session, dimension_node, node_type) # type: ignore + resource_requests = [ + access.ResourceRequest( + verb=access.ResourceRequestVerb.READ, + access_object=access.Resource.from_node(node), + ) + for node in nodes + ] + approvals = validate_access_requests( + validate_access=validate_access, + user=current_user, + resource_requests=resource_requests, + ) + + approved_nodes: List[str] = [request.access_object.name for request in approvals] + return [node for node in nodes if node.name in approved_nodes] + + +@router.get("/dimensions/common/", response_model=List[NodeRevisionOutput]) +async def find_nodes_with_common_dimensions( + dimension: List[str] = Query([]), + node_type: List[NodeType] = Query([]), + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> List[NodeRevisionOutput]: + """ + Find all nodes that have the list of common dimensions + """ + nodes = await get_nodes_with_common_dimensions( + session, + [await get_node_by_name(session, dim) for dim in dimension], # type: ignore + node_type, + ) + approvals = [ + approval.access_object.name + for approval in validate_access_requests( + validate_access, + current_user, + [ + access.ResourceRequest( + verb=access.ResourceRequestVerb.READ, + access_object=access.Resource.from_node(node), + ) + for node in nodes + ], + ) + ] + return [node for node in nodes if node.name in approvals] diff --git a/datajunction-server/datajunction_server/api/djsql.py b/datajunction-server/datajunction_server/api/djsql.py new file mode 100644 index 000000000..795d8ff71 --- /dev/null +++ b/datajunction-server/datajunction_server/api/djsql.py @@ -0,0 +1,132 @@ +""" +Data related APIs. +""" + +from typing import Optional + +from fastapi import Depends, Request +from sqlalchemy.ext.asyncio import AsyncSession +from sse_starlette.sse import EventSourceResponse + +from datajunction_server.api.helpers import build_sql_for_dj_query, query_event_stream +from datajunction_server.database.user import User +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.access.authorization import validate_access +from datajunction_server.models import access +from datajunction_server.models.query import QueryCreate, QueryWithResults +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.utils import ( + get_and_update_current_user, + get_query_service_client, + get_session, + get_settings, +) + +settings = get_settings() +router = SecureAPIRouter(tags=["DJSQL"]) + + +@router.get("/djsql/data", response_model=QueryWithResults) +async def get_data_for_djsql( + query: str, + async_: bool = False, + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> QueryWithResults: + """ + Return data for a DJ SQL query + """ + request_headers = dict(request.headers) + access_control = access.AccessControlStore( + validate_access=validate_access, + user=current_user, + base_verb=access.ResourceRequestVerb.EXECUTE, + ) + translated_sql, engine, catalog = await build_sql_for_dj_query( + session, + query, + access_control, + engine_name, + engine_version, + ) + + query_create = QueryCreate( + engine_name=engine.name, + catalog_name=catalog.name, + engine_version=engine.version, + submitted_query=translated_sql.sql, + async_=async_, + ) + + result = query_service_client.submit_query( + query_create, + request_headers=request_headers, + ) + + # Inject column info if there are results + if result.results.__root__: # pragma: no cover + result.results.__root__[0].columns = translated_sql.columns or [] + return result + + +@router.get("/djsql/stream/", response_model=QueryWithResults) +async def get_data_stream_for_djsql( + query: str, + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> QueryWithResults: # pragma: no cover + """ + Return data for a DJ SQL query using server side events + """ + request_headers = dict(request.headers) + access_control = access.AccessControlStore( + validate_access=validate_access, + user=current_user, + base_verb=access.ResourceRequestVerb.EXECUTE, + ) + translated_sql, engine, catalog = await build_sql_for_dj_query( + session, + query, + access_control, + engine_name, + engine_version, + ) + + query_create = QueryCreate( + engine_name=engine.name, + catalog_name=catalog.name, + engine_version=engine.version, + submitted_query=translated_sql.sql, + async_=True, + ) + + # Submits the query, equivalent to calling POST /data/ directly + initial_query_info = query_service_client.submit_query( + query_create, + request_headers=request_headers, + ) + return EventSourceResponse( + query_event_stream( + query=initial_query_info, + request_headers=request_headers, + query_service_client=query_service_client, + columns=translated_sql.columns, # type: ignore + request=request, + ), + ) diff --git a/datajunction-server/datajunction_server/api/engines.py b/datajunction-server/datajunction_server/api/engines.py new file mode 100644 index 000000000..df79167a6 --- /dev/null +++ b/datajunction-server/datajunction_server/api/engines.py @@ -0,0 +1,79 @@ +""" +Engine related APIs. +""" + +from http import HTTPStatus +from typing import List + +from fastapi import Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database.engine import Engine +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.engines import get_engine +from datajunction_server.models.engine import EngineInfo +from datajunction_server.utils import get_session, get_settings + +settings = get_settings() +router = SecureAPIRouter(tags=["engines"]) + + +@router.get("/engines/", response_model=List[EngineInfo]) +async def list_engines( + *, session: AsyncSession = Depends(get_session) +) -> List[EngineInfo]: + """ + List all available engines + """ + return [ + EngineInfo.from_orm(engine) + for engine in (await session.execute(select(Engine))).scalars() + ] + + +@router.get("/engines/{name}/{version}/", response_model=EngineInfo) +async def get_an_engine( + name: str, version: str, *, session: AsyncSession = Depends(get_session) +) -> EngineInfo: + """ + Return an engine by name and version + """ + return EngineInfo.from_orm(await get_engine(session, name, version)) + + +@router.post( + "/engines/", + response_model=EngineInfo, + status_code=201, + name="Add An Engine", +) +async def add_engine( + data: EngineInfo, + *, + session: AsyncSession = Depends(get_session), +) -> EngineInfo: + """ + Add a new engine + """ + try: + await get_engine(session, data.name, data.version) + except HTTPException: + pass + else: + raise HTTPException( + status_code=HTTPStatus.CONFLICT, + detail=f"Engine already exists: `{data.name}` version `{data.version}`", + ) + + engine = Engine( + name=data.name, + version=data.version, + uri=data.uri, + dialect=data.dialect, + ) + session.add(engine) + await session.commit() + await session.refresh(engine) + + return EngineInfo.from_orm(engine) diff --git a/datajunction-server/datajunction_server/api/graphql/__init__.py b/datajunction-server/datajunction_server/api/graphql/__init__.py new file mode 100644 index 000000000..1f96c11e1 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/__init__.py @@ -0,0 +1,3 @@ +""" +DJ graphql api +""" diff --git a/datajunction-server/datajunction_server/api/graphql/main.py b/datajunction-server/datajunction_server/api/graphql/main.py new file mode 100644 index 000000000..df6fab095 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/main.py @@ -0,0 +1,123 @@ +"""DJ graphql""" + +import logging +from functools import wraps + +import strawberry +from fastapi import Depends +from strawberry.fastapi import GraphQLRouter +from strawberry.types import Info + +from datajunction_server.api.graphql.queries.catalogs import list_catalogs +from datajunction_server.api.graphql.queries.dag import common_dimensions +from datajunction_server.api.graphql.queries.engines import list_engines +from datajunction_server.api.graphql.queries.nodes import ( + find_nodes, + find_nodes_paginated, +) +from datajunction_server.api.graphql.queries.sql import measures_sql +from datajunction_server.api.graphql.queries.tags import list_tag_types, list_tags +from datajunction_server.api.graphql.scalars import Connection +from datajunction_server.api.graphql.scalars.catalog_engine import Catalog, Engine +from datajunction_server.api.graphql.scalars.node import DimensionAttribute, Node +from datajunction_server.api.graphql.scalars.sql import GeneratedSQL +from datajunction_server.api.graphql.scalars.tag import Tag +from datajunction_server.utils import get_session, get_settings + +logger = logging.getLogger(__name__) + + +def log_resolver(func): + """ + Adds generic logging to the GQL resolver. + """ + + @wraps(func) + async def wrapper(*args, **kwargs): + resolver_name = func.__name__ + + info: Info = kwargs.get("info") if "info" in kwargs else None + user = info.context.get("user", "anonymous") if info else "unknown" + args_dict = {key: val for key, val in kwargs.items() if key != "info"} + log_tags = { + "query_name": resolver_name, + "user": user, + **args_dict, + } + log_args = " ".join( + [f"{tag}={value}" for tag, value in log_tags.items() if value], + ) + try: + result = await func(*args, **kwargs) + logger.info("[GQL] %s", log_args) + return result + except Exception as exc: # pragma: no cover + logger.error( # pragma: no cover + "[GQL] status=error %s", + log_args, + exc_info=True, + ) + raise exc # pragma: no cover + + return wrapper + + +async def get_context( + session=Depends(get_session), + settings=Depends(get_settings), +): + """ + Provides the context for graphql requests + """ + return {"session": session, "settings": settings} + + +@strawberry.type +class Query: + """ + Parent of all DJ graphql queries + """ + + # Catalog and engine queries + list_catalogs: list[Catalog] = strawberry.field( + resolver=log_resolver(list_catalogs), + ) + list_engines: list[Engine] = strawberry.field( + resolver=log_resolver(list_engines), + ) + + # Node search queries + find_nodes: list[Node] = strawberry.field( + resolver=log_resolver(find_nodes), + description="Find nodes based on the search parameters.", + ) + find_nodes_paginated: Connection[Node] = strawberry.field( + resolver=log_resolver(find_nodes_paginated), + description="Find nodes based on the search parameters with pagination", + ) + + # DAG queries + common_dimensions: list[DimensionAttribute] = strawberry.field( + resolver=log_resolver(common_dimensions), + description="Get common dimensions for one or more nodes", + ) + + # Generate SQL queries + measures_sql: list[GeneratedSQL] = strawberry.field( + resolver=log_resolver(measures_sql), + ) + + # Tags queries + list_tags: list[Tag] = strawberry.field( + resolver=log_resolver(list_tags), + description="Find DJ node tags based on the search parameters.", + ) + list_tag_types: list[str] = strawberry.field( + resolver=log_resolver(list_tag_types), + description="List all DJ node tag types", + ) + + +schema = strawberry.Schema(query=Query) + +graphql_app = GraphQLRouter(schema, context_getter=get_context) diff --git a/datajunction-server/datajunction_server/api/graphql/queries/__init__.py b/datajunction-server/datajunction_server/api/graphql/queries/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/datajunction_server/api/graphql/queries/catalogs.py b/datajunction-server/datajunction_server/api/graphql/queries/catalogs.py new file mode 100644 index 000000000..65e49fb04 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/queries/catalogs.py @@ -0,0 +1,25 @@ +""" +Catalog related queries. +""" + +from typing import List + +from sqlalchemy import select +from strawberry.types import Info + +from datajunction_server.api.graphql.scalars.catalog_engine import Catalog +from datajunction_server.database.catalog import Catalog as DBCatalog + + +async def list_catalogs( + *, + info: Info = None, +) -> List[Catalog]: + """ + List all available catalogs + """ + session = info.context["session"] # type: ignore + return [ + Catalog.from_pydantic(catalog) # type: ignore + for catalog in (await session.execute(select(DBCatalog))).scalars().all() + ] diff --git a/datajunction-server/datajunction_server/api/graphql/queries/dag.py b/datajunction-server/datajunction_server/api/graphql/queries/dag.py new file mode 100644 index 000000000..16ea8fb8f --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/queries/dag.py @@ -0,0 +1,39 @@ +""" +DAG-related queries. +""" + +from typing import Annotated + +import strawberry +from strawberry.types import Info + +from datajunction_server.api.graphql.resolvers.nodes import find_nodes_by +from datajunction_server.api.graphql.scalars.node import DimensionAttribute +from datajunction_server.sql.dag import get_common_dimensions +from datajunction_server.utils import SEPARATOR + + +async def common_dimensions( + nodes: Annotated[ + list[str] | None, + strawberry.argument( + description="A list of nodes to find common dimensions for", + ), + ] = None, + *, + info: Info, +) -> list[DimensionAttribute]: + """ + Return a list of common dimensions for a set of nodes. + """ + nodes = await find_nodes_by(info, nodes) # type: ignore + dimensions = await get_common_dimensions(info.context["session"], nodes) # type: ignore + return [ + DimensionAttribute( # type: ignore + name=dim.name, + attribute=dim.name.split(SEPARATOR)[-1], + properties=dim.properties, + type=dim.type, + ) + for dim in dimensions + ] diff --git a/datajunction-server/datajunction_server/api/graphql/queries/engines.py b/datajunction-server/datajunction_server/api/graphql/queries/engines.py new file mode 100644 index 000000000..d1bc87ed5 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/queries/engines.py @@ -0,0 +1,25 @@ +""" +Engine related queries. +""" + +from typing import List + +from sqlalchemy import select +from strawberry.types import Info + +from datajunction_server.api.graphql.scalars.catalog_engine import Engine +from datajunction_server.database.engine import Engine as DBEngine + + +async def list_engines( + *, + info: Info = None, +) -> List[Engine]: + """ + List all available engines + """ + session = info.context["session"] # type: ignore + return [ + Engine.from_pydantic(engine) # type: ignore #pylint: disable=E1101 + for engine in (await session.execute(select(DBEngine))).scalars().all() + ] diff --git a/datajunction-server/datajunction_server/api/graphql/queries/nodes.py b/datajunction-server/datajunction_server/api/graphql/queries/nodes.py new file mode 100644 index 000000000..1b0fbddca --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/queries/nodes.py @@ -0,0 +1,122 @@ +""" +Find nodes GraphQL queries. +""" + +from typing import Annotated + +import strawberry +from strawberry.types import Info + +from datajunction_server.api.graphql.resolvers.nodes import find_nodes_by +from datajunction_server.api.graphql.scalars import Connection +from datajunction_server.api.graphql.scalars.node import Node +from datajunction_server.models.node import NodeCursor, NodeType + + +async def find_nodes( + fragment: Annotated[ + str | None, + strawberry.argument( + description="A fragment of a node name to search for", + ), + ] = None, + names: Annotated[ + list[str] | None, + strawberry.argument( + description="Filter to nodes with these names", + ), + ] = None, + node_types: Annotated[ + list[NodeType] | None, + strawberry.argument( + description="Filter nodes to these node types", + ), + ] = None, + tags: Annotated[ + list[str] | None, + strawberry.argument( + description="Filter to nodes tagged with these tags", + ), + ] = None, + *, + info: Info, +) -> list[Node]: + """ + Find nodes based on the search parameters. + """ + return await find_nodes_by(info, names, fragment, node_types, tags) # type: ignore + + +async def find_nodes_paginated( + fragment: Annotated[ + str | None, + strawberry.argument( + description="A fragment of a node name to search for", + ), + ] = None, + names: Annotated[ + list[str] | None, + strawberry.argument( + description="Filter to nodes with these names", + ), + ] = None, + node_types: Annotated[ + list[NodeType] | None, + strawberry.argument( + description="Filter nodes to these node types", + ), + ] = None, + tags: Annotated[ + list[str] | None, + strawberry.argument( + description="Filter to nodes tagged with these tags", + ), + ] = None, + edited_by: Annotated[ + str | None, + strawberry.argument( + description="Filter to nodes edited by this user", + ), + ] = None, + namespace: Annotated[ + str | None, + strawberry.argument( + description="Filter to nodes in this namespace", + ), + ] = None, + after: str | None = None, + before: str | None = None, + limit: Annotated[ + int | None, + strawberry.argument(description="Limit nodes"), + ] = 100, + *, + info: Info, +) -> Connection[Node]: + """ + Find nodes based on the search parameters. + """ + if not limit or limit < 0: + limit = 100 + nodes_list = await find_nodes_by( + info, + names, + fragment, + node_types, + tags, + edited_by, + namespace, + limit + 1, + before, + after, + ) + return Connection.from_list( + items=nodes_list, + before=before, + after=after, + limit=limit, + encode_cursor=lambda dj_node: NodeCursor( + created_at=dj_node.created_at, + id=dj_node.id, + ), + ) diff --git a/datajunction-server/datajunction_server/api/graphql/queries/sql.py b/datajunction-server/datajunction_server/api/graphql/queries/sql.py new file mode 100644 index 000000000..b7306f32a --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/queries/sql.py @@ -0,0 +1,104 @@ +"""Generate SQL-related GraphQL queries.""" + +from typing import Annotated, Optional, OrderedDict + +import strawberry +from strawberry.types import Info + +from datajunction_server.api.graphql.scalars.sql import GeneratedSQL +from datajunction_server.construction.build_v2 import get_measures_query + + +@strawberry.input +class CubeDefinition: + """ + The cube definition for the query + """ + + metrics: Annotated[ + list[str], + strawberry.argument( + description="A list of metric node names", + ), + ] = None # type: ignore + dimensions: Annotated[ + list[str] | None, + strawberry.argument( + description="A list of dimension attribute names", + ), + ] = None + filters: Annotated[ + list[str] | None, + strawberry.argument( + description="A list of filter SQL clauses", + ), + ] = None + orderby: Annotated[ + list[str] | None, + strawberry.argument( + description="A list of order by clauses", + ), + ] = None + + +@strawberry.input +class EngineSettings: + """ + The engine settings for the query + """ + + name: str = strawberry.field( + description="The name of the engine used by the generated SQL", + ) + version: str | None = strawberry.field( + description="The version of the engine used by the generated SQL", + ) + + +async def measures_sql( + cube: CubeDefinition, + engine: Optional[EngineSettings] = None, + use_materialized: Annotated[ + bool, + strawberry.argument( + description="Whether to use materialized nodes where applicable", + ), + ] = True, + include_all_columns: Annotated[ + bool, + strawberry.argument( + description="Whether to include all columns or only those necessary " + "for the metrics and dimensions in the cube", + ), + ] = False, + preaggregate: Annotated[ + bool, + strawberry.argument( + description="Whether to pre-aggregate to the requested dimensions so that " + "subsequent queries are more efficient.", + ), + ] = False, + *, + info: Info, +) -> list[GeneratedSQL]: + """ + Get measures SQL for a set of metrics with dimensions and filters + """ + session, settings = info.context["session"], info.context["settings"] + queries = await get_measures_query( + session=session, + metrics=list(OrderedDict.fromkeys(cube.metrics)), # type: ignore + dimensions=cube.dimensions, # type: ignore + filters=cube.filters, # type: ignore + orderby=cube.orderby, + engine_name=engine.name if engine else None, + engine_version=engine.version if engine else None, + include_all_columns=include_all_columns, + sql_transpilation_library=settings.sql_transpilation_library, + use_materialized=use_materialized, + preagg_requested=preaggregate, + ) + return [ + await GeneratedSQL.from_pydantic(info, measures_query) + for measures_query in queries + ] diff --git a/datajunction-server/datajunction_server/api/graphql/queries/tags.py b/datajunction-server/datajunction_server/api/graphql/queries/tags.py new file mode 100644 index 000000000..8e27b0249 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/queries/tags.py @@ -0,0 +1,42 @@ +""" +Tags related queries. +""" + +from strawberry.types import Info + +from datajunction_server.api.graphql.scalars.tag import Tag +from datajunction_server.database.tag import Tag as DBTag + + +async def list_tags( + *, + info: Info = None, + tag_names: list[str] | None = None, + tag_types: list[str] | None = None, +) -> list[Tag]: + """ + Find available tags by the search parameters + """ + session = info.context["session"] # type: ignore + db_tags = await DBTag.find_tags(session, tag_names, tag_types) + return [ + Tag( # type: ignore + name=db_tag.name, + display_name=db_tag.display_name, + description=db_tag.description, + tag_type=db_tag.tag_type, + tag_metadata=db_tag.tag_metadata, + ) + for db_tag in db_tags + ] + + +async def list_tag_types( + *, + info: Info = None, +) -> list[str]: + """ + List all tag types + """ + session = info.context["session"] # type: ignore + return await DBTag.get_tag_types(session) diff --git a/datajunction-server/datajunction_server/api/graphql/resolvers/__init__.py b/datajunction-server/datajunction_server/api/graphql/resolvers/__init__.py new file mode 100644 index 000000000..4daec0512 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/resolvers/__init__.py @@ -0,0 +1 @@ +"""GraphQL resolvers""" diff --git a/datajunction-server/datajunction_server/api/graphql/resolvers/nodes.py b/datajunction-server/datajunction_server/api/graphql/resolvers/nodes.py new file mode 100644 index 000000000..c653ffa4f --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/resolvers/nodes.py @@ -0,0 +1,159 @@ +""" +Node resolvers +""" + +from typing import Any, List, Optional + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import defer, joinedload, selectinload +from strawberry.types import Info + +from datajunction_server.api.graphql.scalars.node import NodeName +from datajunction_server.api.graphql.utils import extract_fields +from datajunction_server.database.dimensionlink import DimensionLink +from datajunction_server.database.node import Column, ColumnAttribute +from datajunction_server.database.node import Node as DBNode +from datajunction_server.database.node import NodeRevision as DBNodeRevision +from datajunction_server.models.node import NodeType + + +async def find_nodes_by( + info: Info, + names: Optional[List[str]] = None, + fragment: Optional[str] = None, + node_types: Optional[List[NodeType]] = None, + tags: Optional[List[str]] = None, + edited_by: Optional[str] = None, + namespace: Optional[str] = None, + limit: Optional[int] = 100, + before: Optional[str] = None, + after: Optional[str] = None, +) -> List[DBNode]: + """ + Finds nodes based on the search parameters. This function also tries to optimize + the database query by only retrieving joined-in fields if they were requested. + """ + session = info.context["session"] # type: ignore + fields = extract_fields(info) + options = load_node_options( + fields["nodes"] + if "nodes" in fields + else fields["edges"]["node"] + if "edges" in fields + else fields, + ) + return await DBNode.find_by( + session, + names, + fragment, + node_types, + tags, + edited_by, + namespace, + limit, + before, + after, + options=options, + ) + + +async def get_node_by_name( + session: AsyncSession, + fields: dict[str, Any] | None, + name: str, +) -> DBNode | NodeName | None: + """ + Retrieves a node by name. This function also tries to optimize the database + query by only retrieving joined-in fields if they were requested. + """ + if not fields: + return None # pragma: no cover + if "name" in fields and len(fields) == 1: + return NodeName(name=name) # type: ignore + + options = load_node_options( + fields["nodes"] + if "nodes" in fields + else fields["edges"]["node"] + if "edges" in fields + else fields, + ) + return await DBNode.get_by_name( + session, + name=name, + options=options, + ) + + +def load_node_options(fields): + """ + Based on the GraphQL query input fields, builds a list of node load options. + """ + options = [] + if "revisions" in fields: + node_revision_options = load_node_revision_options(fields["revisions"]) + options.append(joinedload(DBNode.revisions).options(*node_revision_options)) + if fields.get("current"): + node_revision_options = load_node_revision_options(fields["current"]) + options.append(joinedload(DBNode.current).options(*node_revision_options)) + if "created_by" in fields: + options.append(joinedload(DBNode.created_by)) + if "edited_by" in fields: + options.append(selectinload(DBNode.history)) + if "tags" in fields: + options.append(selectinload(DBNode.tags)) + return options + + +def load_node_revision_options(node_revision_fields): + """ + Based on the GraphQL query input fields, builds a list of node revision + load options. + """ + options = [defer(DBNodeRevision.query_ast)] + is_cube_request = ( + "cube_metrics" in node_revision_fields + or "cube_dimensions" in node_revision_fields + ) + if "columns" in node_revision_fields or is_cube_request: + options.append( + selectinload(DBNodeRevision.columns).options( + joinedload(Column.attributes).joinedload( + ColumnAttribute.attribute_type, + ), + joinedload(Column.dimension), + joinedload(Column.partition), + ), + ) + if "catalog" in node_revision_fields: + options.append(joinedload(DBNodeRevision.catalog)) + if "parents" in node_revision_fields: + options.append(selectinload(DBNodeRevision.parents)) + if "materializations" in node_revision_fields: + options.append(selectinload(DBNodeRevision.materializations)) + if "metric_metadata" in node_revision_fields: + options.append(selectinload(DBNodeRevision.metric_metadata)) + if "availability" in node_revision_fields: + options.append(selectinload(DBNodeRevision.availability)) + if "dimension_links" in node_revision_fields: + options.append( + selectinload(DBNodeRevision.dimension_links).options( + joinedload(DimensionLink.dimension).options( + selectinload(DBNode.current), + ), + ), + ) + if "required_dimensions" in node_revision_fields: + options.append( + selectinload(DBNodeRevision.required_dimensions), + ) + + if "cube_elements" in node_revision_fields or is_cube_request: + options.append( + selectinload(DBNodeRevision.cube_elements) + .selectinload(Column.node_revisions) + .options( + selectinload(DBNodeRevision.node), + ), + ) + return options diff --git a/datajunction-server/datajunction_server/api/graphql/resolvers/tags.py b/datajunction-server/datajunction_server/api/graphql/resolvers/tags.py new file mode 100644 index 000000000..8a93517d2 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/resolvers/tags.py @@ -0,0 +1,35 @@ +""" +Node resolvers +""" + +from typing import Any + +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database.node import Node as DBNode +from datajunction_server.database.tag import Tag as DBTag + + +async def get_nodes_by_tag( + session: AsyncSession, + tag_name: str, + fields: dict[str, Any], +) -> list[DBNode]: + """ + Retrieves all nodes with the given tag. A list of fields must be requested on the node, + or this will not return any data. + """ + from datajunction_server.api.graphql.resolvers.nodes import load_node_options + + options = load_node_options( + fields["nodes"] + if "nodes" in fields + else fields["edges"]["node"] + if "edges" in fields + else fields, + ) + return await DBTag.list_nodes_with_tag( + session, + tag_name=tag_name, + options=options, + ) diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/__init__.py b/datajunction-server/datajunction_server/api/graphql/scalars/__init__.py new file mode 100644 index 000000000..c3783aa4f --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/scalars/__init__.py @@ -0,0 +1,144 @@ +""" +GraphQL scalars +""" + +import dataclasses +import datetime +import json +from base64 import b64decode, b64encode +from dataclasses import dataclass +from typing import Callable, Generic, List, Optional, TypeVar, Union + +import strawberry +from strawberry import field + +BigInt = strawberry.scalar( + Union[int, str], # type: ignore + serialize=int, + parse_value=str, + description="BigInt field", +) + +GenericItem = TypeVar("GenericItem") +GenericItemNode = TypeVar("GenericItemNode") + + +class DateTimeJSONEncoder(json.JSONEncoder): + """ + JSON encoder that handles datetime objects + """ + + def default(self, obj): + """ + Check if there are datetime objects and serialize them as ISO + format strings. + """ + if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)): + return obj.isoformat() + return super().default(obj) # pragma: no cover + + +class DateTimeJSONDecoder(json.JSONDecoder): + """ + JSON decoder that handles ISO format datetime strings + """ + + def __init__(self, *args, **kwargs): + json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs) + + def object_hook(self, source): + """ + Check if the string is in ISO 8601 format and convert to a datetime + object if it is. + """ + for k, v in source.items(): + if isinstance(v, str): + try: + source[k] = datetime.datetime.fromisoformat(str(v)) + except ValueError: # pragma: no cover + pass # pragma: no cover + return source + + +@dataclass +class Cursor: + """ + Dataclass that serializes into a string and back. + """ + + def encode(self) -> str: + """Serialize this cursor into a string.""" + json_str = json.dumps(dataclasses.asdict(self), cls=DateTimeJSONEncoder) + return b64encode(json_str.encode()).decode() + + @classmethod + def decode(cls, serialized: str) -> "Cursor": + """Parse the string into an instance of this Cursor class.""" + json_str = b64decode(serialized.encode()).decode() + json_obj = json.loads(json_str, cls=DateTimeJSONDecoder) + return cls(**json_obj) + + +@strawberry.type +class PageInfo: + """Metadata about a page in a connection.""" + + has_next_page: bool = field( + description="When paginating forwards, are there more nodes?", + ) + has_prev_page: bool = field( + description="When paginating forwards, are there more nodes?", + ) + start_cursor: str | None = field( + description="When paginating back, the cursor to continue.", + ) + end_cursor: str | None = field( + description="When paginating forwards, the cursor to continue.", + ) + + +@strawberry.type +class Edge(Generic[GenericItemNode]): + """Metadata about an item in a connection.""" + + node: GenericItemNode + + +@strawberry.type +class Connection(Generic[GenericItemNode]): + """ + Pagination for a list of items. + """ + + page_info: PageInfo + edges: list[Edge[GenericItemNode]] + + @classmethod + def from_list( + cls, + items: List[GenericItem], + before: Optional[str], + after: Optional[str], + limit: int, + encode_cursor: Callable[[GenericItem], Cursor], + ) -> "Connection": + """ + Construct a Connection from a list of items. + """ + has_next_page = len(items) > limit or ( + before is not None and len(items) > 0 and items[0] is not None + ) + has_prev_page = (before is not None and len(items) > limit) or ( + after is not None and len(items) > 0 and items[0] is not None + ) + start_cursor = encode_cursor(items[0]).encode() if items else None + end_cursor = encode_cursor(items[-1]).encode() if items else None + return Connection( # type: ignore + page_info=PageInfo( # type: ignore + has_prev_page=has_prev_page, + start_cursor=start_cursor, + has_next_page=has_next_page, + end_cursor=end_cursor, + ), + edges=[Edge(node=item) for item in items[:limit]], # type: ignore + ) diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/availabilitystate.py b/datajunction-server/datajunction_server/api/graphql/scalars/availabilitystate.py new file mode 100644 index 000000000..bb1bcb1b0 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/scalars/availabilitystate.py @@ -0,0 +1,50 @@ +"""Availability state scalars""" + +from typing import List, Optional + +import strawberry + + +@strawberry.type +class PartitionAvailability: + """ + Partition-level availability + """ + + min_temporal_partition: Optional[List[str]] + max_temporal_partition: Optional[List[str]] + + # This list maps to the ordered list of categorical partitions at the node level. + # For example, if the node's `categorical_partitions` are configured as ["country", "group_id"], + # a valid entry for `value` may be ["DE", null]. + value: List[Optional[str]] + + # Valid through timestamp + valid_through_ts: Optional[int] + + +@strawberry.type +class AvailabilityState: + """ + A materialized table that is available for the node + """ + + catalog: str + schema_: Optional[str] + table: str + valid_through_ts: int + url: Optional[str] + + # An ordered list of categorical partitions like ["country", "group_id"] + # or ["region_id", "age_group"] + categorical_partitions: Optional[List[str]] + + # An ordered list of temporal partitions like ["date", "hour"] or ["date"] + temporal_partitions: Optional[List[str]] + + # Node-level temporal ranges + min_temporal_partition: Optional[List[str]] + max_temporal_partition: Optional[List[str]] + + # Partition-level availabilities + partitions: Optional[List[PartitionAvailability]] diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/catalog_engine.py b/datajunction-server/datajunction_server/api/graphql/scalars/catalog_engine.py new file mode 100644 index 000000000..59d519677 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/scalars/catalog_engine.py @@ -0,0 +1,23 @@ +"""Catalog/engine related scalars""" + +import strawberry + +from datajunction_server.models.catalog import CatalogInfo +from datajunction_server.models.engine import EngineInfo +from datajunction_server.models.node import Dialect as Dialect_ + +Dialect = strawberry.enum(Dialect_) + + +@strawberry.experimental.pydantic.type(model=EngineInfo, all_fields=True) +class Engine: + """ + Database engine + """ + + +@strawberry.experimental.pydantic.type(model=CatalogInfo, all_fields=True) +class Catalog: + """ + Class for a Catalog + """ diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/column.py b/datajunction-server/datajunction_server/api/graphql/scalars/column.py new file mode 100644 index 000000000..1a8658b74 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/scalars/column.py @@ -0,0 +1,63 @@ +"""Column scalars""" + +from typing import List, Optional + +import strawberry + +from datajunction_server.models.partition import PartitionType as PartitionType_ + +PartitionType = strawberry.enum(PartitionType_) + + +@strawberry.type +class AttributeTypeName: + """ + Attribute type name. + """ + + namespace: str + name: str + + +@strawberry.type +class Attribute: + """ + Column attribute + """ + + attribute_type: AttributeTypeName + + +@strawberry.type +class NodeName: + """ + Node name + """ + + name: str + + +@strawberry.type +class Partition: + """ + A partition configuration for a column + """ + + type_: PartitionType # type: ignore + format: Optional[str] + granularity: Optional[str] + expression: Optional[str] + + +@strawberry.type +class Column: + """ + A column on a node + """ + + name: str + display_name: Optional[str] + type: str + attributes: Optional[List[Attribute]] + dimension: Optional[NodeName] + partition: Optional[Partition] diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/errors.py b/datajunction-server/datajunction_server/api/graphql/scalars/errors.py new file mode 100644 index 000000000..c467f9f06 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/scalars/errors.py @@ -0,0 +1,18 @@ +"""Error-related scalar types.""" + +import strawberry + +from datajunction_server.errors import ErrorCode as ErrorCode_ + +ErrorCode = strawberry.enum(ErrorCode_) + + +@strawberry.type +class DJError: + """ + A DJ error + """ + + code: ErrorCode # type: ignore + message: str | None + context: str | None diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/materialization.py b/datajunction-server/datajunction_server/api/graphql/scalars/materialization.py new file mode 100644 index 000000000..ee226dc6f --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/scalars/materialization.py @@ -0,0 +1,46 @@ +"""Materialization scalars""" + +from typing import List, Optional + +import strawberry +from strawberry.scalars import JSON + + +@strawberry.type +class PartitionBackfill: + """ + Used for setting backfilled values + """ + + column_name: str + + # Backfilled values and range. Most temporal partitions will just use `range`, but some may + # optionally use `values` to specify specific values + # Ex: values: [20230901] + # range: [20230901, 20231001] + values: Optional[List[str]] + range: Optional[List[str]] + + +@strawberry.type +class Backfill: + """ + Materialization job backfill + """ + + spec: Optional[List[PartitionBackfill]] + urls: Optional[List[str]] + + +@strawberry.type +class MaterializationConfig: + """ + Materialization config + """ + + name: Optional[str] + config: JSON + schedule: str + job: Optional[str] + backfills: List[Backfill] + strategy: Optional[str] diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/metricmetadata.py b/datajunction-server/datajunction_server/api/graphql/scalars/metricmetadata.py new file mode 100644 index 000000000..78f5800e4 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/scalars/metricmetadata.py @@ -0,0 +1,60 @@ +"""Metric metadata scalars""" + +from typing import Optional + +import strawberry + +from datajunction_server.models.cube_materialization import ( + Aggregability as Aggregability_, +) +from datajunction_server.models.cube_materialization import ( + AggregationRule as AggregationRule_, +) +from datajunction_server.models.cube_materialization import Measure as Measure_ +from datajunction_server.models.node import MetricDirection as MetricDirection_ + +MetricDirection = strawberry.enum(MetricDirection_) +Aggregability = strawberry.enum(Aggregability_) + + +@strawberry.type +class Unit: + """ + Metric unit + """ + + name: str + label: Optional[str] + category: Optional[str] + abbreviation: Optional[str] + + +@strawberry.experimental.pydantic.type(model=AggregationRule_, all_fields=True) +class AggregationRule: ... + + +@strawberry.experimental.pydantic.type(model=Measure_, all_fields=True) +class Measure: ... + + +@strawberry.type +class ExtractedMeasures: + """ + extracted measures from metric + """ + + measures: list[Measure] + derived_query: str + derived_expression: str + + +@strawberry.type +class MetricMetadata: + """ + Metric metadata output + """ + + direction: Optional[MetricDirection] # type: ignore + unit: Optional[Unit] + expression: str + incompatible_druid_functions: list[str] diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/node.py b/datajunction-server/datajunction_server/api/graphql/scalars/node.py new file mode 100644 index 000000000..fca4f4873 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/scalars/node.py @@ -0,0 +1,265 @@ +"""Node-related scalars.""" + +import datetime +from typing import List, Optional + +import strawberry +from strawberry.scalars import JSON +from strawberry.types import Info + +from datajunction_server.api.graphql.scalars import BigInt +from datajunction_server.api.graphql.scalars.availabilitystate import AvailabilityState +from datajunction_server.api.graphql.scalars.catalog_engine import Catalog +from datajunction_server.api.graphql.scalars.column import Column, NodeName, Partition +from datajunction_server.api.graphql.scalars.materialization import ( + MaterializationConfig, +) +from datajunction_server.api.graphql.scalars.metricmetadata import ( + ExtractedMeasures, + MetricMetadata, +) +from datajunction_server.api.graphql.scalars.user import User +from datajunction_server.api.graphql.utils import extract_fields +from datajunction_server.database.dimensionlink import ( + JoinCardinality as JoinCardinality_, +) +from datajunction_server.database.dimensionlink import JoinType as JoinType_ +from datajunction_server.database.node import Node as DBNode +from datajunction_server.database.node import NodeRevision as DBNodeRevision +from datajunction_server.models.engine import Dialect +from datajunction_server.models.node import NodeMode as NodeMode_ +from datajunction_server.models.node import NodeStatus as NodeStatus_ +from datajunction_server.models.node import NodeType as NodeType_ +from datajunction_server.sql.decompose import MeasureExtractor +from datajunction_server.sql.parsing.backends.antlr4 import ast, parse + +NodeType = strawberry.enum(NodeType_) +NodeStatus = strawberry.enum(NodeStatus_) +NodeMode = strawberry.enum(NodeMode_) +JoinType = strawberry.enum(JoinType_) +JoinCardinality = strawberry.enum(JoinCardinality_) + + +@strawberry.type +class CubeElement: + """ + An element in a cube, either a metric or dimension + """ + + name: str + display_name: str + type: str + partition: Optional[Partition] + + +@strawberry.type +class DimensionLink: + """ + A dimension link between a dimension and a node + """ + + dimension: NodeName + join_type: JoinType # type: ignore + join_sql: str + join_cardinality: Optional[JoinCardinality] # type: ignore + role: Optional[str] + foreign_keys: JSON + + +@strawberry.type +class DimensionAttribute: + """ + A dimensional column attribute + """ + + name: str + attribute: str | None + role: str | None = None + properties: list[str] + type: str + + _dimension_node: Optional["Node"] = None + + @strawberry.field(description="The dimension node this attribute belongs to") + async def dimension_node(self, info: Info) -> "Node": + """ + Lazy load the dimension node when queried. + """ + if self._dimension_node: + return self._dimension_node + + from datajunction_server.api.graphql.resolvers.nodes import get_node_by_name + + dimension_node_name = self.name.rsplit(".", 1)[0] + fields = extract_fields(info) + return await get_node_by_name( # type: ignore + session=info.context["session"], + fields=fields, + name=dimension_node_name, + ) + + +@strawberry.type +class NodeRevision: + """ + The base fields of a node revision, which does not include joined in entities. + """ + + id: BigInt + type: NodeType # type: ignore + name: str + display_name: Optional[str] + version: str + status: NodeStatus # type: ignore + mode: Optional[NodeMode] # type: ignore + description: str = "" + updated_at: datetime.datetime + + @strawberry.field + def catalog(self, root: "DBNodeRevision") -> Optional[Catalog]: + """ + Catalog for the node + """ + return Catalog.from_pydantic(root.catalog) # type: ignore + + query: Optional[str] = None + columns: List[Column] + + # Dimensions and data graph-related outputs + dimension_links: List[DimensionLink] + parents: List[NodeName] + + # Materialization-related outputs + availability: Optional[AvailabilityState] = None + materializations: Optional[List[MaterializationConfig]] = None + + # Only source nodes will have these fields + schema_: Optional[str] + table: Optional[str] + + # Only metrics will have these fields + required_dimensions: Optional[List[Column]] = None + + @strawberry.field + def metric_metadata(self, root: "DBNodeRevision") -> MetricMetadata | None: + """ + Metric metadata + """ + if root.type != NodeType.METRIC: + return None + + query_ast = parse(root.query) + functions = [func.function() for func in query_ast.find_all(ast.Function)] + return MetricMetadata( # type: ignore + direction=root.metric_metadata.direction if root.metric_metadata else None, + unit=root.metric_metadata.unit if root.metric_metadata else None, + expression=str(query_ast.select.projection[0]), + incompatible_druid_functions={ + func.__name__.upper() + for func in functions + if Dialect.DRUID not in func.dialects + }, + ) + + @strawberry.field + def extracted_measures(self, root: "DBNodeRevision") -> ExtractedMeasures | None: + """ + A list of measures for a metric node + """ + if root.type != NodeType.METRIC: + return None + extractor = MeasureExtractor.from_query_string(root.query) + measures, derived_ast = extractor.extract() + return ExtractedMeasures( # type: ignore + measures=measures, + derived_query=str(derived_ast), + derived_expression=str(derived_ast.select.projection[0]), + ) + + # Only cubes will have these fields + @strawberry.field + def cube_metrics(self, root: "DBNodeRevision") -> List["NodeRevision"]: + """ + Metrics for a cube node + """ + if root.type != NodeType.CUBE: + return [] + ordering = root.ordering() + return sorted( + [ + node_revision + for _, node_revision in root.cube_elements_with_nodes() + if node_revision and node_revision.type == NodeType.METRIC + ], + key=lambda x: ordering[x.name], + ) + + @strawberry.field + def cube_dimensions(self, root: "DBNodeRevision") -> List[DimensionAttribute]: + """ + Dimensions for a cube node + """ + if root.type != NodeType.CUBE: + return [] + dimension_to_roles = {col.name: col.dimension_column for col in root.columns} + ordering = root.ordering() + return sorted( + [ + DimensionAttribute( # type: ignore + name=( + node_revision.name + + "." + + element.name + + dimension_to_roles.get(element.name, "") + ), + attribute=element.name, + role=dimension_to_roles.get(element.name, ""), + _dimension_node=node_revision, + type=element.type, + properties=element.attribute_names(), + ) + for element, node_revision in root.cube_elements_with_nodes() + if node_revision and node_revision.type != NodeType.METRIC + ], + key=lambda x: ordering[x.name], + ) + + +@strawberry.type +class TagBase: + """ + A DJ node tag without any referential fields + """ + + name: str + tag_type: str + description: str | None + display_name: str | None + tag_metadata: JSON | None = strawberry.field(default_factory=dict) + + +@strawberry.type +class Node: + """ + A DJ node + """ + + id: BigInt + name: str + type: NodeType # type: ignore + current_version: str + created_at: datetime.datetime + deactivated_at: Optional[datetime.datetime] + + current: NodeRevision + revisions: List[NodeRevision] + + tags: List[TagBase] + created_by: User + + @strawberry.field + def edited_by(self, root: "DBNode") -> List[str]: + """ + The users who edited this node + """ + return root.edited_by diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/sql.py b/datajunction-server/datajunction_server/api/graphql/scalars/sql.py new file mode 100644 index 000000000..22800e7b1 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/scalars/sql.py @@ -0,0 +1,107 @@ +"""SQL-related scalars.""" + +from functools import cached_property + +import strawberry +from strawberry.types import Info + +from datajunction_server.api.graphql.resolvers.nodes import get_node_by_name +from datajunction_server.api.graphql.scalars.errors import DJError +from datajunction_server.api.graphql.scalars.node import Node +from datajunction_server.api.graphql.utils import extract_fields +from datajunction_server.database.queryrequest import QueryBuildType as QueryBuildType_ +from datajunction_server.models.column import SemanticType as SemanticType_ +from datajunction_server.models.engine import Dialect as Dialect_ +from datajunction_server.models.sql import GeneratedSQL as GeneratedSQL_ +from datajunction_server.utils import SEPARATOR + +SemanticType = strawberry.enum(SemanticType_) +Dialect = strawberry.enum(Dialect_) +QueryBuildType = strawberry.enum(QueryBuildType_) + + +@strawberry.type +class SemanticEntity: + """ + Column metadata for generated SQL + """ + + name: str + + @cached_property + def _split_name(self) -> list[str]: + """ + Private, cached property that splits the name into node and column parts. + """ + return self.name.rsplit(SEPARATOR, 1) + + @strawberry.field(description="The node this semantic entity is sourced from") + async def node(self) -> str: + """ + Returns the node name that this semantic entity is sourced from + """ + return self._split_name[0] + + @strawberry.field( + description="The column on the node this semantic entity is sourced from", + ) + async def column(self) -> str: + """ + Returns the column on the node this semantic entity is sourced from + """ + return self._split_name[1] + + +@strawberry.type +class ColumnMetadata: + """ + Column metadata for generated SQL + """ + + name: str + type: str + semantic_entity: SemanticEntity | None + semantic_type: SemanticType | None # type: ignore + + +@strawberry.type +class GeneratedSQL: + """ + Generated SQL for a given node + """ + + node: Node + sql: str + sql_transpilation_library: str | None = None + columns: list[ColumnMetadata] + dialect: Dialect # type: ignore + upstream_tables: list[str] + errors: list[DJError] + + @classmethod + async def from_pydantic(cls, info: Info, obj: GeneratedSQL_): + """ + Loads a strawberry GeneratedSQL from the original pydantic model. + """ + fields = extract_fields(info) + return GeneratedSQL( # type: ignore + node=await get_node_by_name( + session=info.context["session"], + fields=fields.get("node"), + name=obj.node.name, + ), + sql=obj.sql, + sql_transpilation_library=obj.sql_transpilation_library, + columns=[ + ColumnMetadata( # type: ignore + name=col.name, + type=col.type, + semantic_entity=SemanticEntity(name=col.semantic_entity), # type: ignore + semantic_type=SemanticType(col.semantic_type), + ) + for col in obj.columns # type: ignore + ], + dialect=obj.dialect, + upstream_tables=obj.upstream_tables, + errors=obj.errors, + ) diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/tag.py b/datajunction-server/datajunction_server/api/graphql/scalars/tag.py new file mode 100644 index 000000000..728c09ed5 --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/scalars/tag.py @@ -0,0 +1,27 @@ +"""Tag-related scalars.""" + +import strawberry +from strawberry.types import Info + +from datajunction_server.api.graphql.resolvers.tags import get_nodes_by_tag +from datajunction_server.api.graphql.scalars.node import Node, TagBase +from datajunction_server.api.graphql.utils import extract_fields + + +@strawberry.type +class Tag(TagBase): + """ + A DJ node tag with nodes + """ + + @strawberry.field(description="The nodes with this tag") + async def nodes(self, info: Info) -> list[Node]: + """ + Lazy load the nodes with this tag. + """ + fields = extract_fields(info) + return await get_nodes_by_tag( # type: ignore + session=info.context["session"], + fields=fields, + tag_name=self.name, + ) diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/user.py b/datajunction-server/datajunction_server/api/graphql/scalars/user.py new file mode 100644 index 000000000..09585b42c --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/scalars/user.py @@ -0,0 +1,35 @@ +""" +User related scalars +""" + +from enum import Enum +from typing import Optional + +import strawberry + +from datajunction_server.api.graphql.scalars import BigInt + + +@strawberry.enum +class OAuthProvider(Enum): + """ + An oauth implementation provider + """ + + BASIC = "basic" + GITHUB = "github" + GOOGLE = "google" + + +@strawberry.type +class User: + """ + A DataJunction User + """ + + id: BigInt + username: str + email: Optional[str] + name: Optional[str] + oauth_provider: OAuthProvider + is_admin: bool diff --git a/datajunction-server/datajunction_server/api/graphql/utils.py b/datajunction-server/datajunction_server/api/graphql/utils.py new file mode 100644 index 000000000..fc915c2ca --- /dev/null +++ b/datajunction-server/datajunction_server/api/graphql/utils.py @@ -0,0 +1,46 @@ +"""Utils for handling GraphQL queries.""" + +import re +from typing import Any, Dict + +CURSOR_SEPARATOR = "-" + + +def convert_camel_case(name): + """ + Convert from camel case to snake case + """ + pattern = re.compile(r"(? Dict[str, Any]: + """ + Extract fields from GraphQL query input into a dictionary + """ + fields = {} + + for query_field in query_fields.selected_fields: + for selection in query_field.selections: + field_name = convert_camel_case(selection.name) + if selection.selections: + subfield = extract_subfields(selection) + fields[field_name] = subfield + else: + fields[field_name] = None + + return fields diff --git a/datajunction-server/datajunction_server/api/health.py b/datajunction-server/datajunction_server/api/health.py new file mode 100644 index 000000000..db4714feb --- /dev/null +++ b/datajunction-server/datajunction_server/api/health.py @@ -0,0 +1,64 @@ +""" +Application healthchecks. +""" + +from typing import List + +from fastapi import APIRouter, Depends +from pydantic.main import BaseModel +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.enum import StrEnum +from datajunction_server.utils import get_session, get_settings + +settings = get_settings() + +router = APIRouter(tags=["health"]) + + +class HealthcheckStatus(StrEnum): + """ + Possible health statuses. + """ + + OK = "ok" + FAILED = "failed" + + +class HealthCheck(BaseModel): + """ + A healthcheck response. + """ + + name: str + status: HealthcheckStatus + + +async def database_health(session: AsyncSession) -> HealthcheckStatus: + """ + The status of the database. + """ + try: + result = (await session.execute(select(1))).one() + health_status = ( + HealthcheckStatus.OK if result == (1,) else HealthcheckStatus.FAILED + ) + return health_status + except Exception: # pragma: no cover + return HealthcheckStatus.FAILED # pragma: no cover + + +@router.get("/health/", response_model=List[HealthCheck]) +async def health_check( + session: AsyncSession = Depends(get_session), +) -> List[HealthCheck]: + """ + Healthcheck for services. + """ + return [ + HealthCheck( + name="database", + status=await database_health(session), + ), + ] diff --git a/datajunction-server/datajunction_server/api/helpers.py b/datajunction-server/datajunction_server/api/helpers.py new file mode 100644 index 000000000..fd26ce096 --- /dev/null +++ b/datajunction-server/datajunction_server/api/helpers.py @@ -0,0 +1,905 @@ +""" +Helpers for API endpoints +""" + +import asyncio +import http.client +import json +import logging +import re +import time +import uuid +from http import HTTPStatus +from typing import Dict, List, Optional, Set, Tuple, cast + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import defer, joinedload, selectinload +from sqlalchemy.sql.operators import and_, is_ + +from datajunction_server.construction.build import ( + build_materialized_cube_node, + build_metric_nodes, + get_default_criteria, + rename_columns, + validate_shared_dimensions, +) +from datajunction_server.construction.dj_query import build_dj_query +from datajunction_server.database.attributetype import AttributeType +from datajunction_server.database.catalog import Catalog +from datajunction_server.database.column import Column +from datajunction_server.database.engine import Engine +from datajunction_server.database.history import EntityType, History +from datajunction_server.database.namespace import NodeNamespace +from datajunction_server.database.node import ( + MissingParent, + Node, + NodeMissingParents, + NodeRevision, +) +from datajunction_server.database.user import User +from datajunction_server.errors import ( + DJAlreadyExistsException, + DJDoesNotExistException, + DJError, + DJInvalidInputException, + DJNodeNotFound, + ErrorCode, +) +from datajunction_server.internal.engines import get_engine +from datajunction_server.models import access +from datajunction_server.models.attribute import RESERVED_ATTRIBUTE_NAMESPACE +from datajunction_server.models.history import status_change_history +from datajunction_server.models.metric import TranslatedSQL +from datajunction_server.models.node import NodeStatus +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.query import ColumnMetadata, QueryWithResults +from datajunction_server.naming import LOOKUP_CHARS +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.sql.parsing import ast +from datajunction_server.typing import END_JOB_STATES +from datajunction_server.utils import SEPARATOR, refresh_if_needed + +_logger = logging.getLogger(__name__) + +COLUMN_NAME_REGEX = r"([A-Za-z0-9_\.]+)(\[[A-Za-z0-9_]+\])?" + + +async def get_node_namespace( + session: AsyncSession, + namespace: str, + raise_if_not_exists: bool = True, +) -> NodeNamespace: + """ + Get a node namespace + """ + statement = select(NodeNamespace).where(NodeNamespace.namespace == namespace) + node_namespace = (await session.execute(statement)).scalar_one_or_none() + if raise_if_not_exists: # pragma: no cover + if not node_namespace: + raise DJDoesNotExistException( + message=(f"node namespace `{namespace}` does not exist."), + http_status_code=404, + ) + return node_namespace + + +async def get_node_by_name( + session: AsyncSession, + name: Optional[str], + node_type: Optional[NodeType] = None, + with_current: bool = False, + raise_if_not_exists: bool = True, + include_inactive: bool = False, +) -> Node: + """ + Get a node by name + """ + statement = select(Node).where(Node.name == name) + if not include_inactive: + statement = statement.where(is_(Node.deactivated_at, None)) + if node_type: + statement = statement.where(Node.type == node_type) + if with_current: + statement = statement.options(joinedload(Node.current)).options( + joinedload(Node.tags), + ) + result = await session.execute(statement) + node = result.unique().scalar_one_or_none() + else: + result = await session.execute(statement) + node = result.unique().scalar_one_or_none() + if raise_if_not_exists: + if not node: + raise DJNodeNotFound( + message=( + f"A {'' if not node_type else node_type + ' '}" + f"node with name `{name}` does not exist." + ), + http_status_code=404, + ) + return node + + +async def raise_if_node_exists(session: AsyncSession, name: str) -> None: + """ + Raise an error if the node with the given name already exists. + """ + node = await Node.get_by_name(session, name, raise_if_not_exists=False) + if node: + raise DJAlreadyExistsException( + message=f"A node with name `{name}` already exists.", + http_status_code=HTTPStatus.CONFLICT, + ) + + +async def get_column( + session: AsyncSession, + node: NodeRevision, + column_name: str, +) -> Column: + """ + Get a column from a node revision + """ + requested_column = None + await session.refresh(node, ["columns"]) + for node_column in node.columns: + if node_column.name == column_name: + requested_column = node_column + break + + if not requested_column: + raise DJDoesNotExistException( + message=f"Column {column_name} does not exist on node {node.name}", + http_status_code=404, + ) + return requested_column + + +async def get_attribute_type( + session: AsyncSession, + name: str, + namespace: Optional[str] = RESERVED_ATTRIBUTE_NAMESPACE, +) -> Optional[AttributeType]: + """ + Gets an attribute type by name. + """ + statement = ( + select(AttributeType) + .where(AttributeType.name == name) + .where(AttributeType.namespace == namespace) + ) + return (await session.execute(statement)).scalar_one_or_none() + + +async def get_catalog_by_name(session: AsyncSession, name: str) -> Catalog: + """ + Get a catalog by name + """ + statement = ( + select(Catalog).where(Catalog.name == name).options(joinedload(Catalog.engines)) + ) + catalog = (await session.execute(statement)).scalar() + if not catalog: + raise DJDoesNotExistException( + message=f"Catalog with name `{name}` does not exist.", + http_status_code=404, + ) + return catalog + + +async def get_query( + session: AsyncSession, + node_name: str, + dimensions: List[str], + filters: List[str], + orderby: List[str], + limit: Optional[int] = None, + engine: Optional[Engine] = None, + access_control: Optional[access.AccessControlStore] = None, + use_materialized: bool = True, +) -> ast.Query: + """ + Get a query for a metric, dimensions, and filters + """ + from datajunction_server.construction.build_v2 import QueryBuilder + + node = await Node.get_by_name(session, node_name, raise_if_not_exists=True) + build_criteria = get_default_criteria(node.current, engine) # type: ignore + query_builder = await QueryBuilder.create( + session, + node.current, # type: ignore + use_materialized=use_materialized, + ) + query_ast = await ( + query_builder.ignore_errors() + .with_access_control(access_control) + .with_build_criteria(build_criteria) + .add_dimensions(dimensions) + .add_filters(filters) + .limit(limit) + .order_by(orderby) + .build() + ) + query_ast = rename_columns(query_ast, node.current) # type: ignore + return query_ast + + +def find_bound_dimensions( + validated_node: NodeRevision, + dependencies_map: Dict[NodeRevision, List[ast.Table]], +) -> Tuple[Set[str], List[Column]]: + """ + Finds the matched required dimensions + """ + invalid_required_dimensions = set() + matched_bound_columns = [] + required_dimensions_mapping = {} + for col in validated_node.required_dimensions: + column_name = col.name if isinstance(col, Column) else col + for parent in dependencies_map.keys(): + parent_columns = { + parent_col.name: parent_col for parent_col in parent.columns + } + required_dimensions_mapping[column_name] = parent_columns.get(column_name) + for column_name, required_column in required_dimensions_mapping.items(): + if required_column is not None: + matched_bound_columns.append(required_column) + else: + invalid_required_dimensions.add(column_name) + return invalid_required_dimensions, matched_bound_columns # type: ignore + + +async def resolve_downstream_references( + session: AsyncSession, + node_revision: NodeRevision, + current_user: User, +) -> List[NodeRevision]: + """ + Find all node revisions with missing parent references to `node` and resolve them + """ + from datajunction_server.internal.validation import validate_node_data + + missing_parents = ( + ( + await session.execute( + select(MissingParent).where(MissingParent.name == node_revision.name), + ) + ) + .scalars() + .all() + ) + newly_valid_nodes = [] + for missing_parent in missing_parents: + missing_parent_links = ( + ( + await session.execute( + select(NodeMissingParents).where( + NodeMissingParents.missing_parent_id == missing_parent.id, + ), + ) + ) + .scalars() + .all() + ) + for ( + link + ) in missing_parent_links: # Remove from missing parents and add to parents + downstream_node_id = link.referencing_node_id + downstream_node_revision = ( + ( + await session.execute( + select(NodeRevision) + .where(NodeRevision.id == downstream_node_id) + .options( + joinedload(NodeRevision.missing_parents), + joinedload(NodeRevision.parents), + ), + ) + ) + .unique() + .scalar_one() + ) + await session.refresh(node_revision, ["node"]) + await session.refresh( + downstream_node_revision, + ["parents", "missing_parents"], + ) + downstream_node_revision.parents.append(node_revision.node) + downstream_node_revision.missing_parents.remove(missing_parent) + node_validator = await validate_node_data( + data=downstream_node_revision, + session=session, + ) + event = None + if downstream_node_revision.status != node_validator.status: + event = status_change_history( + downstream_node_revision, + downstream_node_revision.status, + node_validator.status, + parent_node=node_revision.name, + current_user=current_user, + ) + + downstream_node_revision.status = node_validator.status + + await session.refresh(downstream_node_revision, ["columns"]) + downstream_node_revision.columns = node_validator.columns + if node_validator.status == NodeStatus.VALID: + newly_valid_nodes.append(downstream_node_revision) + session.add(downstream_node_revision) + if event: + session.add(event) + await session.commit() + await session.refresh(downstream_node_revision) + + await session.delete(missing_parent) # Remove missing parent reference to node + return newly_valid_nodes + + +def map_dimensions_to_roles(dimensions: List[str]) -> Dict[str, str]: + """ + Returns a mapping between dimension attributes and their roles. + For example, ["default.users.user_id[user]"] would turn into + {"default.users.user_id": "[user]"} + """ + dimension_roles = [re.findall(COLUMN_NAME_REGEX, dim)[0] for dim in dimensions] + return {dim_rols[0]: dim_rols[1] for dim_rols in dimension_roles} + + +async def validate_cube( + session: AsyncSession, + metric_names: List[str], + dimension_names: List[str], + require_dimensions: bool = False, +) -> Tuple[List[Column], List[Node], List[Node], List[Column], Optional[Catalog]]: + """ + Validate that a set of metrics and dimensions can be built together. + """ + metric_nodes = await check_metrics_exist(session, metric_names) + catalogs = [metric.current.catalog for metric in metric_nodes] + catalog = catalogs[0] if catalogs else None + + # Verify that the provided metrics are metric nodes + metrics: List[Column] = [metric.current.columns[0] for metric in metric_nodes] + for metric in metrics: + await session.refresh(metric, ["node_revisions"]) + if not metrics: + raise DJInvalidInputException( + message=("At least one metric is required"), + http_status_code=http.client.UNPROCESSABLE_ENTITY, + ) + non_metrics = [metric for metric in metric_nodes if metric.type != NodeType.METRIC] + if non_metrics: + message = ( + f"Node {non_metrics[0].name} of type {non_metrics[0].type} " # type: ignore + f"cannot be added to a cube." + + " Did you mean to add a dimension attribute?" + if non_metrics[0].type == NodeType.DIMENSION # type: ignore + else "" + ) + raise DJInvalidInputException( + message=message, + errors=[DJError(code=ErrorCode.NODE_TYPE_ERROR, message=message)], + http_status_code=http.client.UNPROCESSABLE_ENTITY, + ) + + dimension_attributes, dimension_nodes = await check_dimension_attributes_exist( + session, + dimension_names, + ) + dimension_mapping: Dict[str, Node] = { + f"{node_name}{SEPARATOR}{attr}": dimension_nodes[node_name] + for node_name, attr in dimension_attributes + } + dimensions: List[Column] = [] + for node_name, column_name in dimension_attributes: + dimension_node = dimension_mapping[f"{node_name}{SEPARATOR}{column_name}"] + columns = {col.name: col for col in dimension_node.current.columns} # type: ignore + + column_name_without_role = column_name + match = re.fullmatch(COLUMN_NAME_REGEX, column_name) + if match: + column_name_without_role = match.groups()[0] + + if column_name_without_role in columns: # pragma: no cover + dimensions.append(columns[column_name_without_role]) + + if require_dimensions and not dimensions: + raise DJInvalidInputException(message="At least one dimension is required") + + if len(set(catalogs)) > 1: + raise DJInvalidInputException( + message=( + f"Metrics and dimensions cannot be from multiple catalogs: {catalogs}" + ), + ) + + if len(set(catalogs)) < 1: # pragma: no cover + raise DJInvalidInputException( + message=("Metrics and dimensions must be part of a common catalog"), + ) + + await validate_shared_dimensions( + session, + metric_nodes, + dimension_names, + ) + return metrics, metric_nodes, list(dimension_nodes.values()), dimensions, catalog + + +async def check_metrics_exist(session: AsyncSession, metrics: list[str]) -> list[Node]: + """ + Check that the list of metrics are valid metric nodes and return them. + """ + metrics_sorting_order = {val: idx for idx, val in enumerate(metrics)} + metric_nodes: List[Node] = sorted( + await Node.get_by_names( + session, + metrics, + options=[ + joinedload(Node.current).options( + selectinload(NodeRevision.columns), + joinedload(NodeRevision.catalog), + selectinload(NodeRevision.parents), + ), + ], + include_inactive=False, + ), + key=lambda x: metrics_sorting_order.get(x.name, 0), + ) + if len(metric_nodes) != len(metrics): + not_found = set(metrics) - {metric.name for metric in metric_nodes} + message = f"The following metric nodes were not found: {', '.join(not_found)}" + raise DJNodeNotFound( + message, + errors=[DJError(code=ErrorCode.UNKNOWN_NODE, message=message)], + ) + return metric_nodes + + +async def check_dimension_attributes_exist( + session: AsyncSession, + dimensions: list[str], +) -> Tuple[list[list[str]], Dict[str, Node]]: + """ + Verify that the provided dimension attributes exist + """ + dimension_attributes: List[List[str]] = [ + dimension_attribute.rsplit(".", 1) for dimension_attribute in dimensions + ] + dimension_node_names = [node_name for node_name, _ in dimension_attributes] + dimension_nodes: Dict[str, Node] = { + node.name: node + for node in await Node.get_by_names( + session, + dimension_node_names, + options=[ + joinedload(Node.current).options( + selectinload(NodeRevision.columns), + defer(NodeRevision.query_ast), + ), + ], + ) + } + missing_dimensions = set(dimension_node_names) - set(dimension_nodes) + if missing_dimensions: # pragma: no cover + missing_dimension_attributes = ", ".join( # pragma: no cover + [ + attr + for node_name, attr in dimension_attributes + if node_name in missing_dimensions + ], + ) + message = ( + f"Please make sure that `{missing_dimension_attributes}` " + "is a dimensional attribute." + ) + raise DJInvalidInputException( # pragma: no cover + message, + errors=[DJError(code=ErrorCode.INVALID_DIMENSION, message=message)], + ) + return dimension_attributes, dimension_nodes + + +async def get_history( + session: AsyncSession, + entity_type: EntityType, + entity_name: str, + offset: int, + limit: int, +): + """ + Get the history for a given entity type and name + """ + return ( + ( + await session.execute( + select(History) + .where(History.entity_type == entity_type) + .where(History.entity_name == entity_name) + .offset(offset) + .limit(limit) + .order_by(History.created_at.desc()), + ) + ) + .scalars() + .all() + ) + + +def validate_orderby( + orderby: List[str], + metrics: List[str], + dimension_attributes: List[str], +): + """ + Validate that all elements in an order by match a metric or dimension attribute + """ + invalid_orderbys = [] + for orderby_element in orderby: + if orderby_element.split(" ")[0] not in metrics + dimension_attributes: + invalid_orderbys.append(orderby_element) + if invalid_orderbys: + raise DJInvalidInputException( + message=( + f"Columns {invalid_orderbys} in order by clause must also be " + "specified in the metrics or dimensions" + ), + ) + + +async def find_existing_cube( + session: AsyncSession, + metric_columns: List[Column], + dimension_columns: List[Column], + materialized: bool = True, +) -> Optional[NodeRevision]: + """ + Find an existing cube with these metrics and dimensions, if any. + If `materialized` is set, it will only look for materialized cubes. + """ + element_names = [col.name for col in (metric_columns + dimension_columns)] + statement = select(Node).join( + NodeRevision, + onclause=( + and_( + (Node.id == NodeRevision.node_id), + (Node.current_version == NodeRevision.version), + ) + ), + ) + for name in element_names: + statement = statement.filter( + NodeRevision.cube_elements.any(Column.name == name), # type: ignore + ).options( + joinedload(Node.current).options( + joinedload(NodeRevision.materializations), + joinedload(NodeRevision.availability), + ), + ) + + existing_cubes = (await session.execute(statement)).unique().scalars().all() + for cube in existing_cubes: + if not materialized or ( # pragma: no cover + materialized and cube.current.materializations and cube.current.availability + ): + return cube.current + + return None + + +async def build_sql_for_multiple_metrics( + session: AsyncSession, + metrics: List[str], + dimensions: List[str], + filters: List[str] = None, + orderby: List[str] = None, + limit: Optional[int] = None, + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + access_control: Optional[access.AccessControlStore] = None, + ignore_errors: bool = True, + use_materialized: bool = True, +) -> Tuple[TranslatedSQL, Engine, Catalog]: + """ + Build SQL for multiple metrics. Used by both /sql and /data endpoints + """ + if not filters: + filters = [] + if not orderby: + orderby = [] + + metric_columns, metric_nodes, _, dimension_columns, _ = await validate_cube( + session, + metrics, + dimensions, + require_dimensions=False, + ) + leading_metric_node = await Node.get_by_name( + session, + metrics[0], + options=[ + joinedload(Node.current).options( + joinedload(NodeRevision.catalog).options(joinedload(Catalog.engines)), + ), + ], + ) + if access_control: + access_control.add_request_by_node(leading_metric_node.current) # type: ignore + available_engines = ( + leading_metric_node.current.catalog.engines # type: ignore + if leading_metric_node.current.catalog # type: ignore + else [] + ) + + # Try to find a built cube that already has the given metrics and dimensions + # The cube needs to have a materialization configured and an availability state + # posted in order for us to use the materialized datasource + cube = await find_existing_cube( + session, + metric_columns, + dimension_columns, + materialized=True, + ) + materialized_cube_catalog = None + if cube: + materialized_cube_catalog = await get_catalog_by_name( + session, + cube.availability.catalog, # type: ignore + ) + + # Check if selected engine is available + engine = ( + await get_engine(session, engine_name, engine_version) # type: ignore + if engine_name + else available_engines[0] + ) + if engine not in available_engines: + raise DJInvalidInputException( # pragma: no cover + f"The selected engine is not available for the node {metrics[0]}. " + f"Available engines include: {', '.join(engine.name for engine in available_engines)}", + ) + + # Do not use the materialized cube if the chosen engine is not available for + # the materialized cube's catalog + if ( + cube + and materialized_cube_catalog + and engine.name not in [eng.name for eng in materialized_cube_catalog.engines] + ): + cube = None + + validate_orderby(orderby, metrics, dimensions) + + if cube and cube.availability and use_materialized and materialized_cube_catalog: + if access_control: # pragma: no cover + access_control.add_request_by_node(cube) + access_control.state = access.AccessControlState.INDIRECT + access_control.raise_if_invalid_requests() + query_ast = build_materialized_cube_node( + metric_columns, + dimension_columns, + cube, + filters, + orderby, + limit, + ) + query_metric_columns = [ + ColumnMetadata( + name=col.name, + type=str(col.type), + column=col.name, + node=col.node_revision().name, # type: ignore + ) + for col in metric_columns + ] + query_dimension_columns = [ + ColumnMetadata( + name=(col.node_revision().name + SEPARATOR + col.name).replace( # type: ignore + SEPARATOR, + f"_{LOOKUP_CHARS.get(SEPARATOR)}_", + ), + type=str(col.type), + node=col.node_revision().name, # type: ignore + column=col.name, # type: ignore + ) + for col in dimension_columns + ] + engine = materialized_cube_catalog.engines[0] + return ( + TranslatedSQL( + sql=str(query_ast), + columns=query_metric_columns + query_dimension_columns, + dialect=materialized_cube_catalog.engines[0].dialect, + ), + engine, + cube.catalog, + ) + + query_ast = await build_metric_nodes( + session, + metric_nodes, + filters=filters or [], + dimensions=dimensions or [], + orderby=orderby or [], + limit=limit, + access_control=access_control, + ignore_errors=ignore_errors, + ) + columns = [ + assemble_column_metadata(col) # type: ignore + for col in query_ast.select.projection + ] + upstream_tables = [tbl for tbl in query_ast.find_all(ast.Table) if tbl.dj_node] + for tbl in upstream_tables: + await refresh_if_needed(session, tbl.dj_node, ["availability"]) + return ( + TranslatedSQL( + sql=str(query_ast), + columns=columns, + dialect=engine.dialect if engine else None, + upstream_tables=[ + f"{leading_metric_node.current.catalog.name}.{tbl.identifier()}" # type: ignore + for tbl in upstream_tables + # If a table has a corresponding node with an associated physical table (either + # a source node or a node with a materialized table). + if cast(NodeRevision, tbl.dj_node).type == NodeType.SOURCE + or cast(NodeRevision, tbl.dj_node).availability is not None + ], + ), + engine, + leading_metric_node.current.catalog, # type: ignore + ) + + +async def query_event_stream( + query: QueryWithResults, + request_headers: Optional[Dict[str, str]], + query_service_client: QueryServiceClient, + columns: List[Column], + request, + timeout: float = 0.0, + stream_delay: float = 0.5, + retry_timeout: int = 5000, +): + """ + A generator of events from a query submitted to the query service + """ + starting_time = time.time() + # Start with query and query_next as the initial state of the query + query_prev = query_next = query + query_id = query_prev.id + _logger.info("sending initial event to the client for query %s", query_id) + yield { + "event": "message", + "id": uuid.uuid4(), + "retry": retry_timeout, + "data": json.dumps(query.json()), + } + # Continuously check the query until it's complete + while not timeout or (time.time() - starting_time < timeout): + # Check if the client closed the connection + if await request.is_disconnected(): # pragma: no cover + _logger.error("connection closed by the client") + break + + # Check the current state of the query + query_next = query_service_client.get_query( # type: ignore # pragma: no cover + query_id=query_id, + request_headers=request_headers, + ) + if query_next.state in END_JOB_STATES: # pragma: no cover + _logger.info( # pragma: no cover + "query end state detected (%s), sending final event to the client", + query_next.state, + ) + if query_next.results.__root__: # pragma: no cover + query_next.results.__root__[0].columns = columns or [] + yield { + "event": "message", + "id": uuid.uuid4(), + "retry": retry_timeout, + "data": json.dumps(query_next.json()), + } + _logger.info("connection closed by the server") + break + if query_prev != query_next: # pragma: no cover + _logger.info( + "query information has changed, sending an event to the client", + ) + yield { + "event": "message", + "id": uuid.uuid4(), + "retry": retry_timeout, + "data": json.dumps(query_next.json()), + } + + query = query_next + await asyncio.sleep(stream_delay) # pragma: no cover + + +async def build_sql_for_dj_query( # pragma: no cover + session: AsyncSession, + query: str, + access_control: access.AccessControl, + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, +) -> Tuple[TranslatedSQL, Engine, Catalog]: + """ + Build SQL for multiple metrics. Used by /djsql endpoints + """ + + query_ast, dj_nodes = await build_dj_query(session, query) + + for node in dj_nodes: # pragma: no cover + access_control.add_request_by_node( # pragma: no cover + node.current, + ) + + access_control.validate_and_raise() # pragma: no cover + + leading_metric_node = dj_nodes[0] # pragma: no cover + available_engines = ( # pragma: no cover + leading_metric_node.current.catalog.engines + if leading_metric_node.current.catalog + else [] + ) + + # Check if selected engine is available + engine = ( # pragma: no cover + await get_engine(session, engine_name, engine_version) # type: ignore + if engine_name + else available_engines[0] + ) + + if engine not in available_engines: # pragma: no cover + raise DJInvalidInputException( # pragma: no cover + f"The selected engine is not available for the node {leading_metric_node.name}. " + f"Available engines include: {', '.join(engine.name for engine in available_engines)}", + ) + + columns = [ # pragma: no cover + ColumnMetadata(name=col.alias_or_name.name, type=str(col.type)) # type: ignore + for col in query_ast.select.projection + ] + + return ( # pragma: no cover + TranslatedSQL( + sql=str(query_ast), + columns=columns, + dialect=engine.dialect if engine else None, + ), + engine, + leading_metric_node.current.catalog, + ) + + +def assemble_column_metadata( + column: ast.Column, + # node_name: Union[List[str], str], +) -> ColumnMetadata: + """ + Extract column metadata from AST + """ + metadata = ColumnMetadata( + name=column.alias_or_name.name, + type=str(column.type), + column=( + column.semantic_entity.split(SEPARATOR)[-1] + if hasattr(column, "semantic_entity") and column.semantic_entity + else None + ), + node=( + SEPARATOR.join(column.semantic_entity.split(SEPARATOR)[:-1]) + if hasattr(column, "semantic_entity") and column.semantic_entity + else None + ), + semantic_entity=column.semantic_entity + if hasattr(column, "semantic_entity") + else None, + semantic_type=column.semantic_type + if hasattr(column, "semantic_type") + else None, + ) + return metadata diff --git a/datajunction-server/datajunction_server/api/history.py b/datajunction-server/datajunction_server/api/history.py new file mode 100644 index 000000000..acf5cf087 --- /dev/null +++ b/datajunction-server/datajunction_server/api/history.py @@ -0,0 +1,69 @@ +""" +History related APIs. +""" + +import logging +from typing import List + +from fastapi import Depends, Query +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.api.helpers import get_history +from datajunction_server.database.history import EntityType, History +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.models.history import HistoryOutput +from datajunction_server.utils import get_session, get_settings + +_logger = logging.getLogger(__name__) +settings = get_settings() +router = SecureAPIRouter(tags=["history"]) + + +@router.get("/history/{entity_type}/{entity_name}/", response_model=List[HistoryOutput]) +async def list_history( + entity_type: EntityType, + entity_name: str, + offset: int = 0, + limit: int = Query(default=100, lte=100), + *, + session: AsyncSession = Depends(get_session), +) -> List[HistoryOutput]: + """ + List history for an entity type (i.e. Node) and entity name + """ + hist = await get_history( + session=session, + entity_name=entity_name, + entity_type=entity_type, + offset=offset, + limit=limit, + ) + return [HistoryOutput.from_orm(entry) for entry in hist] + + +@router.get("/history/", response_model=List[HistoryOutput]) +async def list_history_by_node_context( + node: str, + offset: int = 0, + limit: int = Query(default=100, lte=100), + *, + session: AsyncSession = Depends(get_session), +) -> List[HistoryOutput]: + """ + List all activity history for a node context + """ + hist = ( + ( + await session.execute( + select(History) + .where(History.node == node) + .order_by(History.created_at.desc()) + .offset(offset) + .limit(limit), + ) + ) + .scalars() + .all() + ) + return [HistoryOutput.from_orm(entry) for entry in hist] diff --git a/datajunction-server/datajunction_server/api/logging.conf b/datajunction-server/datajunction_server/api/logging.conf new file mode 100644 index 000000000..867f5ed84 --- /dev/null +++ b/datajunction-server/datajunction_server/api/logging.conf @@ -0,0 +1,30 @@ +[loggers] +keys=root + +[handlers] +keys=consoleHandler,detailedConsoleHandler + +[formatters] +keys=normalFormatter,detailedFormatter + +[logger_root] +level=INFO +handlers=consoleHandler + +[handler_consoleHandler] +class=StreamHandler +level=DEBUG +formatter=normalFormatter +args=(sys.stdout,) + +[handler_detailedConsoleHandler] +class=StreamHandler +level=DEBUG +formatter=detailedFormatter +args=(sys.stdout,) + +[formatter_normalFormatter] +format=[%(asctime)s] %(levelname)s: %(name)s: %(message)s + +[formatter_detailedFormatter] +format=%(asctime)s loglevel=%(levelname)-6s logger=%(name)s %(funcName)s() L%(lineno)-4d %(message)s call_trace=%(pathname)s L%(lineno)-4d diff --git a/datajunction-server/datajunction_server/api/main.py b/datajunction-server/datajunction_server/api/main.py new file mode 100644 index 000000000..9f748183a --- /dev/null +++ b/datajunction-server/datajunction_server/api/main.py @@ -0,0 +1,161 @@ +""" +Main DJ server app. +""" + +# All the models need to be imported here so that SQLModel can define their +# relationships at runtime without causing circular imports. +# See https://sqlmodel.tiangolo.com/tutorial/code-structure/#make-circular-imports-work. + +import logging +from http import HTTPStatus +from logging import config +from os import path +from typing import TYPE_CHECKING + +from fastapi import Depends, FastAPI, Request +from fastapi.responses import JSONResponse +from fastapi_cache import FastAPICache +from fastapi_cache.backends.inmemory import InMemoryBackend +from starlette.middleware.cors import CORSMiddleware + +from datajunction_server import __version__ +from datajunction_server.api import ( + attributes, + catalogs, + client, + collection, + cubes, + data, + dimensions, + djsql, + engines, + health, + history, + materializations, + measures, + metrics, + namespaces, + nodes, + notifications, + sql, + tags, + users, +) +from datajunction_server.api.access.authentication import basic, whoami +from datajunction_server.api.attributes import default_attribute_types +from datajunction_server.api.catalogs import default_catalog +from datajunction_server.api.graphql.main import graphql_app +from datajunction_server.constants import AUTH_COOKIE, LOGGED_IN_FLAG_COOKIE +from datajunction_server.errors import DJException +from datajunction_server.utils import get_settings + +if TYPE_CHECKING: # pragma: no cover + pass + +_logger = logging.getLogger(__name__) +settings = get_settings() + +config.fileConfig( + path.join(path.dirname(path.abspath(__file__)), "logging.conf"), + disable_existing_loggers=False, +) + +dependencies = [Depends(default_attribute_types), Depends(default_catalog)] + +app = FastAPI( + title=settings.name, + description=settings.description, + version=__version__, + license_info={ + "name": "MIT License", + "url": "https://mit-license.org/", + }, + dependencies=dependencies, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origin_whitelist, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(catalogs.router) +app.include_router(collection.router) +app.include_router(engines.router) +app.include_router(metrics.router) +app.include_router(djsql.router) +app.include_router(nodes.router) +app.include_router(namespaces.router) +app.include_router(materializations.router) +app.include_router(measures.router) +app.include_router(data.router) +app.include_router(health.router) +app.include_router(history.router) +app.include_router(cubes.router) +app.include_router(tags.router) +app.include_router(attributes.router) +app.include_router(sql.router) +app.include_router(client.router) +app.include_router(dimensions.router) +app.include_router(graphql_app, prefix="/graphql") +app.include_router(whoami.router) +app.include_router(users.router) +app.include_router(basic.router) +app.include_router(notifications.router) + + +@app.on_event("startup") +async def startup(): + """ + Initialize FastAPI cache when the server starts up + """ + FastAPICache.init(InMemoryBackend(), prefix="inmemory-cache") # pragma: no cover + + +@app.exception_handler(DJException) +async def dj_exception_handler( + request: Request, + exc: DJException, +) -> JSONResponse: + """ + Capture errors and return JSON. + """ + _logger.exception(exc) + response = JSONResponse( + status_code=exc.http_status_code, + content=exc.to_dict(), + headers={"X-DJ-Error": "true", "X-DBAPI-Exception": exc.dbapi_exception}, + ) + # If unauthorized, clear out any DJ cookies + if exc.http_status_code == HTTPStatus.UNAUTHORIZED: + response.delete_cookie(AUTH_COOKIE, httponly=True) + response.delete_cookie(LOGGED_IN_FLAG_COOKIE) + return response + + +# Only mount github auth router if a github client id and secret are configured +if all( + [ + settings.secret, + settings.github_oauth_client_id, + settings.github_oauth_client_secret, + ], +): # pragma: no cover + from datajunction_server.api.access.authentication import github + + app.include_router(github.router) + +# Only mount google auth router if a google oauth is configured +if all( + [ + settings.secret, + settings.google_oauth_client_id, + settings.google_oauth_client_secret, + settings.google_oauth_client_secret_file, + ], +): # pragma: no cover + from datajunction_server.api.access.authentication import google + + app.include_router(google.router) diff --git a/datajunction-server/datajunction_server/api/materializations.py b/datajunction-server/datajunction_server/api/materializations.py new file mode 100644 index 000000000..82ade21a6 --- /dev/null +++ b/datajunction-server/datajunction_server/api/materializations.py @@ -0,0 +1,469 @@ +""" +Node materialization related APIs. +""" + +import logging +from datetime import datetime +from http import HTTPStatus +from typing import List + +from fastapi import Depends, Request +from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload, selectinload + +from datajunction_server.database import Node, NodeRevision +from datajunction_server.database.backfill import Backfill +from datajunction_server.database.column import Column, ColumnAttribute +from datajunction_server.database.history import ActivityType, EntityType, History +from datajunction_server.database.user import User +from datajunction_server.errors import DJDoesNotExistException, DJInvalidInputException +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.access.authorization import validate_access +from datajunction_server.internal.materializations import ( + create_new_materialization, + schedule_materialization_jobs, +) +from datajunction_server.materialization.jobs import MaterializationJob +from datajunction_server.models import access +from datajunction_server.models.base import labelize +from datajunction_server.models.cube_materialization import UpsertCubeMaterialization +from datajunction_server.models.materialization import ( + MaterializationConfigInfoUnified, + MaterializationConfigOutput, + MaterializationInfo, + MaterializationJobTypeEnum, + MaterializationStrategy, + UpsertMaterialization, +) +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.partition import PartitionBackfill +from datajunction_server.naming import amenable_name +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.typing import UTCDatetime +from datajunction_server.utils import ( + get_and_update_current_user, + get_query_service_client, + get_session, + get_settings, +) + +_logger = logging.getLogger(__name__) +settings = get_settings() +router = SecureAPIRouter(tags=["materializations"]) + + +@router.get( + "/materialization/info", + status_code=200, + name="Materialization Jobs Info", +) +def materialization_jobs_info() -> JSONResponse: + """ + Materialization job types and strategies + """ + return JSONResponse( + status_code=200, + content={ + "job_types": [value.value.dict() for value in MaterializationJobTypeEnum], + "strategies": [ + {"name": value, "label": labelize(value)} + for value in MaterializationStrategy + ], + }, + ) + + +@router.post( + "/nodes/{node_name}/materialization/", + status_code=201, + name="Insert or Update a Materialization for a Node", +) +async def upsert_materialization( + node_name: str, + data: UpsertMaterialization | UpsertCubeMaterialization, + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> JSONResponse: + """ + Add or update a materialization of the specified node. If a node_name is specified + for the materialization config, it will always update that named config. + """ + request_headers = dict(request.headers) + node = await Node.get_by_name(session, node_name, raise_if_not_exists=True) + if node.type == NodeType.SOURCE: # type: ignore + raise DJInvalidInputException( + http_status_code=HTTPStatus.BAD_REQUEST, + message=f"Cannot set materialization config for source node `{node_name}`!", + ) + if node.type == NodeType.CUBE: # type: ignore + node = await Node.get_cube_by_name(session, node_name) + _logger.info( + "Upserting materialization for node=%s version=%s", + node.name, # type: ignore + node.current_version, # type: ignore + ) + + current_revision = node.current # type: ignore + old_materializations = {mat.name: mat for mat in current_revision.materializations} + + if data.strategy == MaterializationStrategy.INCREMENTAL_TIME: + if not node.current.temporal_partition_columns(): # type: ignore + raise DJInvalidInputException( + http_status_code=HTTPStatus.BAD_REQUEST, + message="Cannot create materialization with strategy " + f"`{data.strategy}` without specifying a time partition column!", + ) + + # Create a new materialization + new_materialization = await create_new_materialization( + session, + current_revision, + data, + validate_access, + current_user=current_user, + ) + + # Check to see if a materialization for this engine already exists with the exact same config + existing_materialization = old_materializations.get(new_materialization.name) + deactivated_before = False + if ( + existing_materialization + and existing_materialization.config == new_materialization.config + ): + _logger.info( + "Existing materialization found for node=%s version=%s", + node.name, # type: ignore + node.current_version, # type: ignore + ) + new_materialization.node_revision = None # type: ignore + # if the materialization was deactivated before, restore it + if existing_materialization.deactivated_at is not None: + deactivated_before = True + existing_materialization.deactivated_at = None # type: ignore + session.add( + History( + entity_type=EntityType.MATERIALIZATION, + entity_name=existing_materialization.name, + node=node.name, # type: ignore + activity_type=ActivityType.RESTORE, + details={}, + user=current_user.username, + ), + ) + await session.commit() + await session.refresh(existing_materialization) + existing_materialization_info = query_service_client.get_materialization_info( + node_name, + current_revision.version, # type: ignore + current_revision.type, + new_materialization.name, # type: ignore + request_headers=request_headers, + ) + _logger.info( + "Refresh materialization workflows for node=%s version=%s", + node.name, # type: ignore + node.current_version, # type: ignore + ) + await schedule_materialization_jobs( + session, + node_revision_id=current_revision.id, + materialization_names=[new_materialization.name], + query_service_client=get_query_service_client(request), # type: ignore + request_headers=request_headers, + ) + return JSONResponse( + status_code=HTTPStatus.CREATED, + content={ + "message": ( + f"The same materialization config with name `{new_materialization.name}` " + f"already exists for node `{node_name}` so no update was performed." + if not deactivated_before + else f"The same materialization config with name `{new_materialization.name}` " + f"already exists for node `{node_name}` but was deactivated. It has now been " + f"restored." + ), + "info": existing_materialization_info.dict(), + }, + ) + # If changes are detected, update the existing or save the new materialization + if existing_materialization: + _logger.info( + "Updating existing materialization for node=%s version=%s", + node.name, # type: ignore + node.current_version, # type: ignore + ) + existing_materialization.config = new_materialization.config + existing_materialization.schedule = new_materialization.schedule + new_materialization.node_revision = None # type: ignore + new_materialization = existing_materialization + new_materialization.deactivated_at = None + else: + _logger.info( + "Adding new materialization for node=%s version=%s", + node.name, # type: ignore + node.current_version, # type: ignore + ) + unchanged_existing_materializations = [ + config + for config in current_revision.materializations + if config.name != new_materialization.name + ] + current_revision.materializations = unchanged_existing_materializations + [ # type: ignore + new_materialization, + ] + + # This will add the materialization config, the new node rev, and update the node's version. + session.add(current_revision) + session.add(node) + + session.add( + History( + entity_type=EntityType.MATERIALIZATION, + node=node.name, # type: ignore + entity_name=new_materialization.name, + activity_type=( + ActivityType.CREATE + if not existing_materialization + else ActivityType.UPDATE + ), + details={ + "node": node.name, # type: ignore + "materialization": new_materialization.name, + }, + user=current_user.username, + ), + ) + await session.commit() + _logger.info( + "Scheduling materialization workflows for node=%s version=%s", + node.name, # type: ignore + node.current_version, # type: ignore + ) + materialization_response = await schedule_materialization_jobs( + session, + node_revision_id=current_revision.id, + materialization_names=[new_materialization.name], + query_service_client=get_query_service_client(request), # type: ignore + request_headers=request_headers, + ) + return JSONResponse( + status_code=200, + content={ + "message": ( + f"Successfully updated materialization config named `{new_materialization.name}` " + f"for node `{node_name}`" + ), + "urls": [output.urls for output in materialization_response.values()], + }, + ) + + +@router.get( + "/nodes/{node_name}/materializations/", + response_model=List[MaterializationConfigInfoUnified], + name="List Materializations for a Node", +) +async def list_node_materializations( + node_name: str, + show_deleted: bool = False, + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), +) -> List[MaterializationConfigInfoUnified]: + """ + Show all materializations configured for the node, with any associated metadata + like urls from the materialization service, if available. + """ + request_headers = dict(request.headers) + node = await Node.get_by_name(session, node_name) + materializations = [] + for materialization in node.current.materializations: # type: ignore + if not materialization.deactivated_at or show_deleted: # pragma: no cover + info = query_service_client.get_materialization_info( + node_name, + node.current.version, # type: ignore + node.type, # type: ignore + materialization.name, # type: ignore + request_headers=request_headers, + ) + if materialization.strategy != MaterializationStrategy.INCREMENTAL_TIME: + info.urls = [info.urls[0]] + materialization_config_output = MaterializationConfigOutput.from_orm( + materialization, + ) + materialization = MaterializationConfigInfoUnified( + **materialization_config_output.dict(), + **info.dict(), + ) + materializations.append(materialization) + return materializations + + +@router.delete( + "/nodes/{node_name}/materializations/", + response_model=List[MaterializationConfigInfoUnified], + name="Deactivate a Materialization for a Node", +) +async def deactivate_node_materializations( + node_name: str, + materialization_name: str, + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + current_user: User = Depends(get_and_update_current_user), +) -> List[MaterializationConfigInfoUnified]: + """ + Deactivate the node materialization with the provided name. + Also calls the query service to deactivate the associated scheduled jobs. + """ + request_headers = dict(request.headers) + node = await Node.get_by_name(session, node_name) + query_service_client.deactivate_materialization( + node_name, + materialization_name, + request_headers=request_headers, + ) + for materialization in node.current.materializations: # type: ignore + if ( + materialization.name == materialization_name + and not materialization.deactivated_at + ): # pragma: no cover + now = datetime.utcnow() + materialization.deactivated_at = UTCDatetime( + year=now.year, + month=now.month, + day=now.day, + hour=now.hour, + minute=now.minute, + second=now.second, + ) + session.add(materialization) + + session.add( + History( + entity_type=EntityType.MATERIALIZATION, + entity_name=materialization_name, + node=node.name, # type: ignore + activity_type=ActivityType.DELETE, + details={}, + user=current_user.username, + ), + ) + await session.commit() + await session.refresh(node.current) # type: ignore + return JSONResponse( + status_code=HTTPStatus.OK, + content={ + "message": f"The materialization named `{materialization_name}` on node `{node_name}` " + "has been successfully deactivated", + }, + ) + + +@router.post( + "/nodes/{node_name}/materializations/{materialization_name}/backfill", + status_code=201, + name="Kick off a backfill run for a configured materialization", +) +async def run_materialization_backfill( + node_name: str, + materialization_name: str, + backfill_partitions: List[PartitionBackfill], + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + current_user: User = Depends(get_and_update_current_user), +) -> MaterializationInfo: + """ + Start a backfill for a configured materialization. + """ + request_headers = dict(request.headers) + node = await Node.get_by_name( + session, + node_name, + options=[ + joinedload(Node.current).options( + selectinload(NodeRevision.columns).options( + selectinload(Column.attributes).joinedload( + ColumnAttribute.attribute_type, + ), + selectinload(Column.dimension), + selectinload(Column.partition), + ), + selectinload(NodeRevision.materializations), + ), + ], + ) + node_revision = node.current # type: ignore + materializations = [ + mat + for mat in node_revision.materializations + if mat.name == materialization_name + ] + if not materializations: + raise DJDoesNotExistException( + f"Materialization with name {materialization_name} not found", + ) + + materialization = materializations[0] + temporal_partitions = { + col.name: col for col in node_revision.temporal_partition_columns() + } + categorical_partitions = { + col.name: col for col in node_revision.categorical_partition_columns() + } + for backfill_spec in backfill_partitions: + if backfill_spec.column_name not in set(temporal_partitions).union( + set(categorical_partitions), + ): + raise DJDoesNotExistException( # pragma: no cover + f"Partition with name {backfill_spec.column_name} does not exist on node", + ) + backfill_spec.column_name = amenable_name(backfill_spec.column_name) + materialization_jobs = { + cls.__name__: cls for cls in MaterializationJob.__subclasses__() + } + clazz = materialization_jobs.get(materialization.job) + if not clazz: + raise DJDoesNotExistException( # pragma: no cover + f"Materialization job {materialization.job} does not exist", + ) + + materialization_output = clazz().run_backfill( # type: ignore + materialization, + backfill_partitions, + query_service_client, + request_headers=request_headers, + ) + backfill = Backfill( + materialization=materialization, + spec=[backfill_partition.dict() for backfill_partition in backfill_partitions], + urls=materialization_output.urls, + ) + materialization.backfills.append(backfill) + + backfill_event = History( + entity_type=EntityType.BACKFILL, + node=node_name, + activity_type=ActivityType.CREATE, + details={ + "materialization": materialization_name, + "partition": [ + backfill_partition.dict() for backfill_partition in backfill_partitions + ], + }, + user=current_user.username, + ) + session.add(backfill_event) + await session.commit() + return materialization_output diff --git a/datajunction-server/datajunction_server/api/measures.py b/datajunction-server/datajunction_server/api/measures.py new file mode 100644 index 000000000..87c519e40 --- /dev/null +++ b/datajunction-server/datajunction_server/api/measures.py @@ -0,0 +1,169 @@ +""" +Measures related APIs. +""" + +import logging +from typing import List, Optional + +from fastapi import Depends +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload + +from datajunction_server.database import Node, NodeRevision +from datajunction_server.database.column import Column +from datajunction_server.database.measure import Measure +from datajunction_server.errors import DJAlreadyExistsException, DJDoesNotExistException +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.models.measure import ( + CreateMeasure, + EditMeasure, + MeasureOutput, + NodeColumn, +) +from datajunction_server.utils import get_session, get_settings + +_logger = logging.getLogger(__name__) +settings = get_settings() +router = SecureAPIRouter(tags=["measures"]) + + +async def get_measure_by_name( + session: AsyncSession, + measure_name: str, + raise_if_not_exists: bool = True, +) -> Measure: + """Retrieve a measure by name""" + measure = ( + (await session.execute(select(Measure).where(Measure.name == measure_name))) + .unique() + .scalars() + .one_or_none() + ) + if raise_if_not_exists and not measure: + raise DJDoesNotExistException( + message=f"Measure with name `{measure_name}` does not exist", + ) + return measure + + +async def get_node_columns( + session: AsyncSession, + node_columns: List[NodeColumn], +) -> List[Column]: + """ + Finds all the specified node columns or raises if they don't exist + """ + measure_columns = [] + for node_column in node_columns: + node = await Node.get_by_name( + session, + node_column.node, + options=[ + joinedload(Node.current).options(*NodeRevision.default_load_options()), + ], + ) + available = [ + col + for col in node.current.columns # type: ignore + if col.name == node_column.column + ] + if len(available) == 0: + raise DJDoesNotExistException( + message=f"Column `{node_column.column}` does not exist on " + f"node `{node_column.node}`", + ) + measure_columns.extend(available) + return measure_columns + + +@router.get("/measures/", response_model=List[str]) +async def list_measures( + prefix: Optional[str] = None, + session: AsyncSession = Depends(get_session), +) -> List[str]: + """ + List all measures. + """ + statement = select(Measure.name) + if prefix: + statement = statement.where( + Measure.name.like(f"{prefix}%"), # type: ignore + ) + return (await session.execute(statement)).scalars().all() + + +@router.get("/measures/{measure_name}", response_model=MeasureOutput) +async def get_measure( + measure_name: str, *, session: AsyncSession = Depends(get_session) +) -> MeasureOutput: + """ + Get info on a measure. + """ + measure = await get_measure_by_name(session, measure_name, raise_if_not_exists=True) + return measure + + +@router.post( + "/measures/", + response_model=MeasureOutput, + status_code=201, + name="Add a Measure", +) +async def add_measure( + data: CreateMeasure, *, session: AsyncSession = Depends(get_session) +) -> MeasureOutput: + """ + Add a measure + """ + measure = await get_measure_by_name(session, data.name, raise_if_not_exists=False) + if measure: + raise DJAlreadyExistsException(message=f"Measure `{data.name}` already exists!") + measure_columns = await get_node_columns(session, data.columns) + measure = Measure( + name=data.name, + display_name=data.display_name, + description=data.description, + columns=measure_columns, + additive=data.additive, + ) + session.add(measure) + await session.commit() + await session.refresh(measure) + return measure + + +@router.patch( + "/measures/{measure_name}", + response_model=MeasureOutput, + status_code=201, + name="Edit a Measure", +) +async def edit_measure( + measure_name: str, + data: EditMeasure, + *, + session: AsyncSession = Depends(get_session), +) -> MeasureOutput: + """ + Edit a measure + """ + measure = await get_measure_by_name(session, measure_name, raise_if_not_exists=True) + + if data.description: + measure.description = data.description + + if data.columns is not None: + measure_columns = await get_node_columns(session, data.columns) + measure.columns = measure_columns + + if data.additive: + measure.additive = data.additive + + if data.display_name: + measure.display_name = data.display_name + + session.add(measure) + await session.commit() + await session.refresh(measure) + return measure diff --git a/datajunction-server/datajunction_server/api/metrics.py b/datajunction-server/datajunction_server/api/metrics.py new file mode 100644 index 000000000..329bb61ed --- /dev/null +++ b/datajunction-server/datajunction_server/api/metrics.py @@ -0,0 +1,149 @@ +""" +Metric related APIs. +""" + +from http import HTTPStatus +from typing import List, Optional + +from fastapi import Depends, HTTPException, Query +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload +from sqlalchemy.sql.operators import is_ + +from datajunction_server.api.nodes import list_nodes +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.user import User +from datajunction_server.errors import DJError, DJInvalidInputException, ErrorCode +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.access.authorization import validate_access +from datajunction_server.models import access +from datajunction_server.models.metric import Metric +from datajunction_server.models.node import ( + DimensionAttributeOutput, + MetricDirection, + MetricMetadataOptions, + MetricUnit, +) +from datajunction_server.models.node_type import NodeType +from datajunction_server.sql.dag import get_dimensions, get_shared_dimensions +from datajunction_server.utils import ( + get_and_update_current_user, + get_session, + get_settings, +) + +settings = get_settings() +router = SecureAPIRouter(tags=["metrics"]) + + +async def get_metric(session: AsyncSession, name: str) -> Node: + """ + Return a metric node given a node name. + """ + node = await Node.get_by_name( + session, + name, + options=[ + selectinload(Node.current).options( + *NodeRevision.default_load_options(), + selectinload(NodeRevision.required_dimensions), + ), + ], + raise_if_not_exists=True, + ) + if node.type != NodeType.METRIC: # type: ignore + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"Not a metric node: `{name}`", + ) + return node # type: ignore + + +@router.get("/metrics/", response_model=List[str]) +async def list_metrics( + prefix: Optional[str] = None, + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> List[str]: + """ + List all available metrics. + """ + return await list_nodes( + node_type=NodeType.METRIC, + prefix=prefix, + session=session, + current_user=current_user, + validate_access=validate_access, + ) + + +@router.get("/metrics/metadata") +async def list_metric_metadata() -> MetricMetadataOptions: + """ + Return available metric metadata attributes + """ + return_obj = MetricMetadataOptions( + directions=[MetricDirection(e) for e in MetricDirection], + units=[MetricUnit(e).value for e in MetricUnit], + ) + return return_obj + + +@router.get("/metrics/{name}/", response_model=Metric) +async def get_a_metric( + name: str, *, session: AsyncSession = Depends(get_session) +) -> Metric: + """ + Return a metric by name. + """ + node = await get_metric(session, name) + dims = await get_dimensions(session, node.current.parents[0]) + metric = Metric.parse_node(node, dims) + return metric + + +@router.get( + "/metrics/common/dimensions/", + response_model=List[DimensionAttributeOutput], +) +async def get_common_dimensions( + metric: List[str] = Query( + title="List of metrics to find common dimensions for", + default=[], + ), + session: AsyncSession = Depends(get_session), +) -> List[DimensionAttributeOutput]: + """ + Return common dimensions for a set of metrics. + """ + input_errors = [] + statement = ( + select(Node) + .where(Node.name.in_(metric)) # type: ignore + .where(is_(Node.deactivated_at, None)) + ) + metric_nodes = (await session.execute(statement)).scalars().all() + for node in metric_nodes: + if node.type != NodeType.METRIC: + input_errors.append( + DJError( + message=f"Not a metric node: {node.name}", + code=ErrorCode.NODE_TYPE_ERROR, + ), + ) + if not metric_nodes: + input_errors.append( + DJError( + message=f"Metric nodes not found: {','.join(metric)}", + code=ErrorCode.UNKNOWN_NODE, + ), + ) + + if input_errors: + raise DJInvalidInputException(errors=input_errors) + return await get_shared_dimensions(session, metric_nodes) diff --git a/datajunction-server/datajunction_server/api/namespaces.py b/datajunction-server/datajunction_server/api/namespaces.py new file mode 100644 index 000000000..2946f2a4e --- /dev/null +++ b/datajunction-server/datajunction_server/api/namespaces.py @@ -0,0 +1,360 @@ +""" +Node namespace related APIs. +""" + +import logging +from http import HTTPStatus +from typing import Dict, List, Optional + +from fastapi import Depends, Query +from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.api.helpers import get_node_namespace +from datajunction_server.database.namespace import NodeNamespace +from datajunction_server.database.user import User +from datajunction_server.errors import DJAlreadyExistsException +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.access.authorization import ( + validate_access, + validate_access_requests, +) +from datajunction_server.internal.namespaces import ( + create_namespace, + get_nodes_in_namespace, + get_nodes_in_namespace_detailed, + get_project_config, + hard_delete_namespace, + mark_namespace_deactivated, + mark_namespace_restored, + validate_namespace, +) +from datajunction_server.internal.nodes import activate_node, deactivate_node +from datajunction_server.models import access +from datajunction_server.models.node import NamespaceOutput, NodeMinimumDetail +from datajunction_server.models.node_type import NodeType +from datajunction_server.utils import ( + get_and_update_current_user, + get_session, + get_settings, +) + +_logger = logging.getLogger(__name__) +settings = get_settings() +router = SecureAPIRouter(tags=["namespaces"]) + + +@router.post("/namespaces/{namespace}/", status_code=HTTPStatus.CREATED) +async def create_node_namespace( + namespace: str, + include_parents: Optional[bool] = False, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """ + Create a node namespace + """ + if node_namespace := await NodeNamespace.get( + session, + namespace, + raise_if_not_exists=False, + ): # pragma: no cover + if node_namespace.deactivated_at: + node_namespace.deactivated_at = None + session.add(node_namespace) + await session.commit() + return JSONResponse( + status_code=HTTPStatus.CREATED, + content={ + "message": ( + "The following node namespace has been successfully reactivated: " + + namespace + ), + }, + ) + return JSONResponse( + status_code=409, + content={ + "message": f"Node namespace `{namespace}` already exists", + }, + ) + validate_namespace(namespace) + created_namespaces = await create_namespace( + session=session, + namespace=namespace, + include_parents=include_parents, # type: ignore + current_user=current_user, + ) + return JSONResponse( + status_code=HTTPStatus.CREATED, + content={ + "message": ( + "The following node namespaces have been successfully created: " + + ", ".join(created_namespaces) + ), + }, + ) + + +@router.get( + "/namespaces/", + response_model=List[NamespaceOutput], + status_code=200, +) +async def list_namespaces( + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> List[NamespaceOutput]: + """ + List namespaces with the number of nodes contained in them + """ + results = await NodeNamespace.get_all_with_node_count(session) + resource_requests = [ + access.ResourceRequest( + verb=access.ResourceRequestVerb.BROWSE, + access_object=access.Resource.from_namespace(record.namespace), + ) + for record in results + ] + approvals = validate_access_requests( + validate_access, + current_user, + resource_requests=resource_requests, + ) + approved_namespaces: List[str] = [ + request.access_object.name for request in approvals + ] + return [ + NamespaceOutput(namespace=record.namespace, num_nodes=record.num_nodes) + for record in results + if record.namespace in approved_namespaces + ] + + +@router.get( + "/namespaces/{namespace}/", + response_model=List[NodeMinimumDetail], + status_code=HTTPStatus.OK, +) +async def list_nodes_in_namespace( + namespace: str, + type_: Optional[NodeType] = Query( + default=None, + description="Filter the list of nodes to this type", + ), + with_edited_by: bool = Query( + default=False, + description="Whether to include a list of users who edited each node", + ), + session: AsyncSession = Depends(get_session), +) -> List[NodeMinimumDetail]: + """ + List node names in namespace, filterable to a given type if desired. + """ + return await NodeNamespace.list_nodes( + session, + namespace, + type_, + with_edited_by=with_edited_by, + ) + + +@router.delete("/namespaces/{namespace}/", status_code=HTTPStatus.OK) +async def deactivate_a_namespace( + namespace: str, + cascade: bool = Query( + default=False, + description="Cascade the deletion down to the nodes in the namespace", + ), + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """ + Deactivates a node namespace + """ + node_namespace = await NodeNamespace.get( + session, + namespace, + raise_if_not_exists=True, + ) + + if node_namespace.deactivated_at: # type: ignore + raise DJAlreadyExistsException( + message=f"Namespace `{namespace}` is already deactivated.", + ) + + # If there are no active nodes in the namespace, we can safely deactivate this namespace + node_list = await NodeNamespace.list_nodes(session, namespace) + node_names = [node.name for node in node_list] + if len(node_names) == 0: + message = f"Namespace `{namespace}` has been deactivated." + await mark_namespace_deactivated( + session=session, + namespace=node_namespace, # type: ignore + message=message, + current_user=current_user, + ) + return JSONResponse( + status_code=HTTPStatus.OK, + content={"message": message}, + ) + + # If cascade=true is set, we'll deactivate all nodes in this namespace and then + # subsequently deactivate this namespace + if cascade: + for node_name in node_names: + await deactivate_node( + session=session, + name=node_name, + message=f"Cascaded from deactivating namespace `{namespace}`", + current_user=current_user, + ) + message = ( + f"Namespace `{namespace}` has been deactivated. The following nodes" + f" have also been deactivated: {','.join(node_names)}" + ) + await mark_namespace_deactivated( + session=session, + namespace=node_namespace, # type: ignore + message=message, + current_user=current_user, + ) + + return JSONResponse( + status_code=HTTPStatus.OK, + content={ + "message": message, + }, + ) + + return JSONResponse( + status_code=405, + content={ + "message": f"Cannot deactivate node namespace `{namespace}` as there are " + "still active nodes under that namespace.", + }, + ) + + +@router.post("/namespaces/{namespace}/restore/", status_code=HTTPStatus.CREATED) +async def restore_a_namespace( + namespace: str, + cascade: bool = Query( + default=False, + description="Cascade the restore down to the nodes in the namespace", + ), + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """ + Restores a node namespace + """ + node_namespace = await get_node_namespace( + session=session, + namespace=namespace, + raise_if_not_exists=True, + ) + if not node_namespace.deactivated_at: + raise DJAlreadyExistsException( + message=f"Node namespace `{namespace}` already exists and is active.", + ) + + node_list = await get_nodes_in_namespace( + session, + namespace, + include_deactivated=True, + ) + node_names = [node.name for node in node_list] + # If cascade=true is set, we'll restore all nodes in this namespace and then + # subsequently restore this namespace + if cascade: + for node_name in node_names: + await activate_node( + name=node_name, + session=session, + message=f"Cascaded from restoring namespace `{namespace}`", + current_user=current_user, + ) + + message = ( + f"Namespace `{namespace}` has been restored. The following nodes" + f" have also been restored: {','.join(node_names)}" + ) + await mark_namespace_restored( + session=session, + namespace=node_namespace, + message=message, + current_user=current_user, + ) + + return JSONResponse( + status_code=HTTPStatus.CREATED, + content={ + "message": message, + }, + ) + + # Otherwise just restore this namespace + message = f"Namespace `{namespace}` has been restored." + await mark_namespace_restored( + session=session, + namespace=node_namespace, + message=message, + current_user=current_user, + ) + return JSONResponse( + status_code=HTTPStatus.CREATED, + content={"message": message}, + ) + + +@router.delete("/namespaces/{namespace}/hard/", name="Hard Delete a DJ Namespace") +async def hard_delete_node_namespace( + namespace: str, + *, + cascade: bool = False, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """ + Hard delete a namespace, which will completely remove the namespace. Additionally, + if any nodes are saved under this namespace, we'll hard delete the nodes if cascade + is set to true. If cascade is set to false, we'll raise an error. This should be used + with caution, as the impact may be large. + """ + impacts = await hard_delete_namespace( + session=session, + namespace=namespace, + cascade=cascade, + current_user=current_user, + ) + return JSONResponse( + status_code=HTTPStatus.OK, + content={ + "message": f"The namespace `{namespace}` has been completely removed.", + "impact": impacts, + }, + ) + + +@router.get( + "/namespaces/{namespace}/export/", + name="Export a namespace as a single project's metadata", +) +async def export_a_namespace( + namespace: str, + *, + session: AsyncSession = Depends(get_session), +) -> List[Dict]: + """ + Generates a zip of YAML files for the contents of the given namespace + as well as a project definition file. + """ + return await get_project_config( + session=session, + nodes=await get_nodes_in_namespace_detailed(session, namespace), + namespace_requested=namespace, + ) diff --git a/datajunction-server/datajunction_server/api/nodes.py b/datajunction-server/datajunction_server/api/nodes.py new file mode 100644 index 000000000..4585cb2d4 --- /dev/null +++ b/datajunction-server/datajunction_server/api/nodes.py @@ -0,0 +1,1684 @@ +""" +Node related APIs. +""" + +import logging +import os +from http import HTTPStatus +from typing import List, Optional + +from fastapi import BackgroundTasks, Depends, Query, Response +from fastapi.responses import JSONResponse +from fastapi_cache.decorator import cache +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload, selectinload +from sqlalchemy.sql.operators import is_ +from starlette.requests import Request + +from datajunction_server.api.helpers import ( + get_catalog_by_name, + get_column, + get_node_by_name, + get_node_namespace, + raise_if_node_exists, +) +from datajunction_server.api.namespaces import create_node_namespace +from datajunction_server.api.tags import get_tags_by_name +from datajunction_server.database import DimensionLink +from datajunction_server.database.attributetype import ColumnAttribute +from datajunction_server.database.column import Column +from datajunction_server.database.history import ActivityType, EntityType, History +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.partition import Partition +from datajunction_server.database.user import User +from datajunction_server.errors import ( + DJActionNotAllowedException, + DJAlreadyExistsException, + DJConfigurationException, + DJDoesNotExistException, + DJInvalidInputException, + ErrorCode, +) +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.access.authorization import ( + validate_access, + validate_access_requests, +) +from datajunction_server.internal.nodes import ( + activate_node, + copy_to_new_node, + create_cube_node_revision, + create_node_from_inactive, + create_node_revision, + deactivate_node, + get_column_level_lineage, + get_node_column, + hard_delete_node, + remove_dimension_link, + revalidate_node, + save_column_level_lineage, + save_node, + set_node_column_attributes, + update_any_node, + upsert_complex_dimension_link, +) +from datajunction_server.internal.validation import validate_node_data +from datajunction_server.models import access +from datajunction_server.models.attribute import ( + AttributeTypeIdentifier, + ColumnAttributes, +) +from datajunction_server.models.dimensionlink import ( + JoinLinkInput, + JoinType, + LinkDimensionIdentifier, +) +from datajunction_server.models.node import ( + ColumnOutput, + CreateCubeNode, + CreateNode, + CreateSourceNode, + DAGNodeOutput, + DimensionAttributeOutput, + LineageColumn, + NodeIndexItem, + NodeMode, + NodeOutput, + NodeRevisionBase, + NodeRevisionOutput, + NodeStatus, + NodeStatusDetails, + NodeValidation, + NodeValidationError, + UpdateNode, +) +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.partition import ( + Granularity, + PartitionInput, + PartitionType, +) +from datajunction_server.models.query import QueryCreate +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.sql.dag import ( + _node_output_options, + get_dimensions, + get_downstream_nodes, + get_filter_only_dimensions, + get_upstream_nodes, +) +from datajunction_server.sql.parsing.backends.antlr4 import parse, parse_rule +from datajunction_server.utils import ( + Version, + get_and_update_current_user, + get_namespace_from_name, + get_query_service_client, + get_session, + get_settings, +) + +_logger = logging.getLogger(__name__) +settings = get_settings() +router = SecureAPIRouter(tags=["nodes"]) + + +@router.post("/nodes/validate/", response_model=NodeValidation) +async def validate_node( + data: NodeRevisionBase, + response: Response, + session: AsyncSession = Depends(get_session), +) -> NodeValidation: + """ + Determines whether the provided node is valid and returns metadata from node validation. + """ + + if data.type == NodeType.SOURCE: + raise DJInvalidInputException(message="Source nodes cannot be validated") + + node_validator = await validate_node_data(data, session) + if node_validator.errors: + response.status_code = HTTPStatus.UNPROCESSABLE_ENTITY + else: + response.status_code = HTTPStatus.OK + + return NodeValidation( + message=f"Node `{data.name}` is {node_validator.status}.", + status=node_validator.status, + columns=node_validator.columns, + dependencies=set(node_validator.dependencies_map.keys()), + errors=node_validator.errors, + missing_parents=list(node_validator.missing_parents_map.keys()), + ) + + +@router.post("/nodes/{name}/validate/", response_model=NodeStatusDetails) +async def revalidate( + name: str, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), + *, + background_tasks: BackgroundTasks, +) -> NodeStatusDetails: + """ + Revalidate a single existing node and update its status appropriately + """ + node_validator = await revalidate_node( + name=name, + session=session, + current_user=current_user, + background_tasks=background_tasks, + ) + + return NodeStatusDetails( + status=node_validator.status, + errors=[ + *[ + NodeValidationError( + type=ErrorCode.TYPE_INFERENCE.name, + message=failure, + ) + for failure in node_validator.type_inference_failures + ], + *[ + NodeValidationError( + type=( + ErrorCode.TYPE_INFERENCE.name + if "Unable to infer type" in error.message + else error.code.name + ), + message=error.message, + ) + for error in node_validator.errors + ], + ], + ) + + +@router.post( + "/nodes/{node_name}/columns/{column_name}/attributes/", + response_model=List[ColumnOutput], + status_code=201, +) +async def set_column_attributes( + node_name: str, + column_name: str, + attributes: List[AttributeTypeIdentifier], + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> List[ColumnOutput]: + """ + Set column attributes for the node. + """ + node = await Node.get_by_name( + session, + node_name, + options=[ + joinedload(Node.current).options(*NodeRevision.default_load_options()), + ], + ) + columns = await set_node_column_attributes( + session, + node, # type: ignore + column_name, + attributes, + current_user=current_user, + ) + return columns # type: ignore + + +@router.get("/nodes/", response_model=List[str]) +async def list_nodes( + node_type: Optional[NodeType] = None, + prefix: Optional[str] = None, + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> List[str]: + """ + List the available nodes. + """ + nodes = await Node.find(session, prefix, node_type) # type: ignore + return [ + approval.access_object.name + for approval in validate_access_requests( + validate_access, + current_user, + [ + access.ResourceRequest( + verb=access.ResourceRequestVerb.BROWSE, + access_object=access.Resource.from_node(node), + ) + for node in nodes + ], + ) + ] + + +@router.get("/nodes/details/", response_model=List[NodeIndexItem]) +@cache(expire=settings.index_cache_expire) +async def list_all_nodes_with_details( + node_type: Optional[NodeType] = None, + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> List[NodeIndexItem]: + """ + List the available nodes. + """ + nodes_query = ( + select( + NodeRevision.name, + NodeRevision.display_name, + NodeRevision.description, + NodeRevision.type, + ) + .where( + Node.current_version == NodeRevision.version, + Node.name == NodeRevision.name, + Node.type == node_type if node_type else True, + is_(Node.deactivated_at, None), + ) + .order_by(NodeRevision.updated_at.desc()) + .limit(settings.node_list_max) + ) # Very high limit as a safeguard + results = [ + NodeIndexItem(name=row[0], display_name=row[1], description=row[2], type=row[3]) + for row in (await session.execute(nodes_query)).all() + ] + if len(results) == settings.node_list_max: # pragma: no cover + _logger.warning( + "%s limit reached when returning all nodes, all nodes may not be captured in results", + settings.node_list_max, + ) + approvals = [ + approval.access_object.name + for approval in validate_access_requests( + validate_access, + current_user, + [ + access.ResourceRequest( + verb=access.ResourceRequestVerb.BROWSE, + access_object=access.Resource( + name=row.name, + resource_type=access.ResourceType.NODE, + owner="", + ), + ) + for row in results + ], + ) + ] + return [row for row in results if row.name in approvals] + + +@router.get("/nodes/{name}/", response_model=NodeOutput) +async def get_node( + name: str, *, session: AsyncSession = Depends(get_session) +) -> NodeOutput: + """ + Show the active version of the specified node. + """ + node = await Node.get_by_name( + session, + name, + options=NodeOutput.load_options(), + raise_if_not_exists=True, + ) + return NodeOutput.from_orm(node) + + +@router.delete("/nodes/{name}/") +async def delete_node( + name: str, + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +): + """ + Delete (aka deactivate) the specified node. + """ + await deactivate_node(session=session, name=name, current_user=current_user) + return JSONResponse( + status_code=HTTPStatus.OK, + content={"message": f"Node `{name}` has been successfully deleted."}, + ) + + +@router.delete("/nodes/{name}/hard/", name="Hard Delete a DJ Node") +async def hard_delete( + name: str, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """ + Hard delete a node, destroying all links and invalidating all downstream nodes. + This should be used with caution, deactivating a node is preferred. + """ + impact = await hard_delete_node( + name=name, + session=session, + current_user=current_user, + ) + return JSONResponse( + status_code=HTTPStatus.OK, + content={ + "message": f"The node `{name}` has been completely removed.", + "impact": impact, + }, + ) + + +@router.post("/nodes/{name}/restore/") +async def restore_node( + name: str, + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +): + """ + Restore (aka re-activate) the specified node. + """ + await activate_node(session=session, name=name, current_user=current_user) + return JSONResponse( + status_code=HTTPStatus.OK, + content={"message": f"Node `{name}` has been successfully restored."}, + ) + + +@router.get("/nodes/{name}/revisions/", response_model=List[NodeRevisionOutput]) +async def list_node_revisions( + name: str, *, session: AsyncSession = Depends(get_session) +) -> List[NodeRevisionOutput]: + """ + List all revisions for the node. + """ + node = await Node.get_by_name( + session, + name, + options=[ + joinedload(Node.revisions).options(*NodeRevision.default_load_options()), + ], + raise_if_not_exists=True, + ) + return node.revisions # type: ignore + + +@router.post("/nodes/source/", response_model=NodeOutput, name="Create A Source Node") +async def create_source( + data: CreateSourceNode, + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), + background_tasks: BackgroundTasks, +) -> NodeOutput: + """ + Create a source node. If columns are not provided, the source node's schema + will be inferred using the configured query service. + """ + request_headers = dict(request.headers) + await raise_if_node_exists(session, data.name) + + # if the node previously existed and now is inactive + if recreated_node := await create_node_from_inactive( + new_node_type=NodeType.SOURCE, + data=data, + session=session, + current_user=current_user, + request_headers=request_headers, + query_service_client=query_service_client, + validate_access=validate_access, + background_tasks=background_tasks, + ): + return recreated_node + + namespace = get_namespace_from_name(data.name) + await get_node_namespace( + session=session, + namespace=namespace, + ) # Will return 404 if namespace doesn't exist + data.namespace = namespace + + node = Node( + name=data.name, + namespace=data.namespace, + display_name=data.display_name or f"{data.catalog}.{data.schema_}.{data.table}", + type=NodeType.SOURCE, + current_version=0, + created_by_id=current_user.id, + ) + catalog = await get_catalog_by_name(session=session, name=data.catalog) + + columns = [ + Column( + name=column_data.name, + type=column_data.type, + dimension=( + await get_node_by_name( + session, + name=column_data.dimension, + node_type=NodeType.DIMENSION, + raise_if_not_exists=False, + ) + ), + order=idx, + ) + for idx, column_data in enumerate(data.columns) + ] + node_revision = NodeRevision( + name=data.name, + display_name=data.display_name or f"{catalog.name}.{data.schema_}.{data.table}", + description=data.description, + type=NodeType.SOURCE, + status=NodeStatus.VALID, + catalog_id=catalog.id, + schema_=data.schema_, + table=data.table, + columns=columns, + parents=[], + created_by_id=current_user.id, + query=data.query, + ) + node.display_name = node_revision.display_name + + # Point the node to the new node revision. + await save_node(session, node_revision, node, data.mode, current_user=current_user) + node = await Node.get_by_name( # type: ignore + session, + node.name, + options=[ + joinedload(Node.current).options(*NodeRevision.default_load_options()), + joinedload(Node.tags), + ], + ) + return node + + +@router.post( + "/nodes/transform/", + response_model=NodeOutput, + status_code=201, + name="Create A Transform Node", +) +@router.post( + "/nodes/dimension/", + response_model=NodeOutput, + status_code=201, + name="Create A Dimension Node", +) +@router.post( + "/nodes/metric/", + response_model=NodeOutput, + status_code=201, + name="Create A Metric Node", +) +async def create_node( + data: CreateNode, + request: Request, + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), + query_service_client: QueryServiceClient = Depends(get_query_service_client), + background_tasks: BackgroundTasks, + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> NodeOutput: + """ + Create a node. + """ + request_headers = dict(request.headers) + node_type = NodeType(os.path.basename(os.path.normpath(request.url.path))) + + if node_type == NodeType.DIMENSION and not data.primary_key: + raise DJInvalidInputException("Dimension nodes must define a primary key!") + + await raise_if_node_exists(session, data.name) + + # if the node previously existed and now is inactive + if recreated_node := await create_node_from_inactive( + new_node_type=node_type, + data=data, + session=session, + current_user=current_user, + request_headers=request_headers, + query_service_client=query_service_client, + background_tasks=background_tasks, + validate_access=validate_access, + ): + return recreated_node # pragma: no cover + + namespace = get_namespace_from_name(data.name) + await get_node_namespace( + session=session, + namespace=namespace, + ) # Will return 404 if namespace doesn't exist + data.namespace = namespace + + node = Node( + name=data.name, + namespace=data.namespace, + type=NodeType(node_type), + current_version=0, + created_by_id=current_user.id, + ) + node_revision = await create_node_revision(data, node_type, session, current_user) + await save_node(session, node_revision, node, data.mode, current_user=current_user) + background_tasks.add_task( + save_column_level_lineage, + session=session, + node_revision=node_revision, + ) + + node = await Node.get_by_name( # type: ignore + session, + node.name, + options=[ + joinedload(Node.current).options(*NodeRevision.default_load_options()), + ], + ) + node_revision = node.current + column_names = {col.name for col in node_revision.columns} + if data.primary_key and any( + key_column not in column_names for key_column in data.primary_key + ): + raise DJInvalidInputException( + f"Some columns in the primary key [{','.join(data.primary_key)}] " + f"were not found in the list of available columns for the node {node.name}.", + ) + if data.primary_key: + for key_column in data.primary_key: + if key_column in column_names: # pragma: no cover + await set_node_column_attributes( + session, + node, + key_column, + [ + AttributeTypeIdentifier( + name=ColumnAttributes.PRIMARY_KEY.value, + namespace="system", + ), + ], + current_user=current_user, + ) + node = await Node.get_by_name( # type: ignore + session, + node.name, + options=[ + joinedload(Node.current).options(*NodeRevision.default_load_options()), + joinedload(Node.tags), + ], + ) + return node + + +@router.post( + "/nodes/cube/", + response_model=NodeOutput, + status_code=201, + name="Create A Cube", +) +async def create_cube( + data: CreateCubeNode, + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + current_user: User = Depends(get_and_update_current_user), + background_tasks: BackgroundTasks, + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> NodeOutput: + """ + Create a cube node. + """ + request_headers = dict(request.headers) + await raise_if_node_exists(session, data.name) + + # if the node previously existed and now is inactive + if recreated_node := await create_node_from_inactive( + new_node_type=NodeType.CUBE, + data=data, + session=session, + current_user=current_user, + request_headers=request_headers, + query_service_client=query_service_client, + background_tasks=background_tasks, + validate_access=validate_access, + ): + return recreated_node # pragma: no cover + + namespace = get_namespace_from_name(data.name) + await get_node_namespace( + session=session, + namespace=namespace, + ) + data.namespace = namespace + + node = Node( + name=data.name, + namespace=data.namespace, + type=NodeType.CUBE, + current_version=0, + created_by_id=current_user.id, + ) + node_revision = await create_cube_node_revision( + session=session, + data=data, + current_user=current_user, + ) + await save_node(session, node_revision, node, data.mode, current_user=current_user) + node = await Node.get_by_name(session, data.name) # type: ignore + return node + + +@router.post( + "/register/table/{catalog}/{schema_}/{table}/", + response_model=NodeOutput, + status_code=201, +) +async def register_table( + catalog: str, + schema_: str, + table: str, + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + current_user: User = Depends(get_and_update_current_user), + background_tasks: BackgroundTasks, +) -> NodeOutput: + """ + Register a table. This creates a source node in the SOURCE_NODE_NAMESPACE and + the source node's schema will be inferred using the configured query service. + """ + request_headers = dict(request.headers) + if not query_service_client: + raise DJConfigurationException( + message="Registering tables or views requires that a query " + "service is configured for columns inference", + ) + namespace = f"{settings.source_node_namespace}.{catalog}.{schema_}" + name = f"{namespace}.{table}" + await raise_if_node_exists(session, name) + + # Create the namespace if required (idempotent) + await create_node_namespace( + namespace=namespace, + session=session, + current_user=current_user, + ) + + # Use reflection to get column names and types + _catalog = await get_catalog_by_name(session=session, name=catalog) + columns = query_service_client.get_columns_for_table( + _catalog.name, + schema_, + table, + request_headers, + _catalog.engines[0] if len(_catalog.engines) >= 1 else None, + ) + + return await create_source( + data=CreateSourceNode( + catalog=catalog, + schema_=schema_, + table=table, + name=name, + display_name=name, + columns=[ColumnOutput.from_orm(col) for col in columns], + description="This source node was automatically created as a registered table.", + mode=NodeMode.PUBLISHED, + ), + session=session, + current_user=current_user, + background_tasks=background_tasks, + request=request, + ) + + +@router.post( + "/register/view/{catalog}/{schema_}/{view}/", + response_model=NodeOutput, + status_code=201, +) +async def register_view( + catalog: str, + schema_: str, + view: str, + query: str, + replace: bool = False, + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + current_user: User = Depends(get_and_update_current_user), + background_tasks: BackgroundTasks, +) -> NodeOutput: + """ + Register a view by creating the view in the database and adding a source node for it. + The source node is created in the SOURCE_NODE_NAMESPACE and + its schema will be inferred using the configured query service. + """ + request_headers = dict(request.headers) + if not query_service_client: + raise DJConfigurationException( + message="Registering tables or views requires that a query " + "service is configured for columns inference", + ) + namespace = f"{settings.source_node_namespace}.{catalog}.{schema_}" + node_name = f"{namespace}.{view}" + view_name = f"{schema_}.{view}" + await raise_if_node_exists(session, node_name) + + # Re-create the view in the database + _catalog = await get_catalog_by_name(session=session, name=catalog) + or_replace = "OR REPLACE" if replace else "" + query = f"CREATE {or_replace} VIEW {view_name} AS {query}" + query_create = QueryCreate( + engine_name=_catalog.engines[0].name, + catalog_name=_catalog.name, + engine_version=_catalog.engines[0].version, + submitted_query=query, + async_=False, + ) + query_service_client.create_view( + view_name, + query_create, + request_headers, + ) + + # Use reflection to get column names and types + columns = query_service_client.get_columns_for_table( + _catalog.name, + schema_, + view, + request_headers, + _catalog.engines[0] if len(_catalog.engines) >= 1 else None, + ) + + # Create the namespace if required (idempotent) + await create_node_namespace( + namespace=namespace, + session=session, + current_user=current_user, + ) + + return await create_source( + data=CreateSourceNode( + catalog=catalog, + schema_=schema_, + table=view, + name=node_name, + display_name=node_name, + columns=[ColumnOutput.from_orm(col) for col in columns], + description="This source node was automatically created as a registered view.", + mode=NodeMode.PUBLISHED, + query=query, + ), + session=session, + current_user=current_user, + background_tasks=background_tasks, + request=request, + ) + + +@router.post("/nodes/{name}/columns/{column}/", status_code=201) +async def link_dimension( + name: str, + column: str, + dimension: str, + dimension_column: Optional[str] = None, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """ + Add information to a node column + """ + node = await Node.get_by_name( + session, + name, + raise_if_not_exists=True, + ) + dimension_node = await Node.get_by_name( + session, + dimension, + raise_if_not_exists=True, + ) + if dimension_node.type != NodeType.DIMENSION: # type: ignore # pragma: no cover + # pragma: no cover + raise DJInvalidInputException(f"Node {node.name} is not of type dimension!") # type: ignore + primary_key_columns = dimension_node.current.primary_key() # type: ignore + if len(primary_key_columns) > 1: + raise DJActionNotAllowedException( # pragma: no cover + "Cannot use this endpoint to link a dimension with a compound primary key.", + ) + + target_column = await get_column(session, node.current, column) # type: ignore + if dimension_column: + # Check that the dimension column exists + column_from_dimension = await get_column( + session, + dimension_node.current, # type: ignore + dimension_column, + ) + + # Check the dimension column's type is compatible with the target column's type + if not column_from_dimension.type.is_compatible(target_column.type): + raise DJInvalidInputException( + f"The column {target_column.name} has type {target_column.type} " + f"and is being linked to the dimension {dimension} via the dimension" + f" column {dimension_column}, which has type {column_from_dimension.type}." + " These column types are incompatible and the dimension cannot be linked", + ) + + link_input = JoinLinkInput( + dimension_node=dimension, + join_type=JoinType.LEFT, + join_on=( + f"{name}.{column} = {dimension_node.name}.{primary_key_columns[0].name}" # type: ignore + ), + ) + activity_type = await upsert_complex_dimension_link( + session, + name, + link_input, + current_user, + ) + + node = await Node.get_by_name( + session, + name, + raise_if_not_exists=True, + ) + return JSONResponse( + status_code=201, + content={ + "message": ( + f"Dimension node {dimension} has been successfully " + f"linked to node {name} using column {column}." + ) + if activity_type == ActivityType.CREATE + else ( + f"The dimension link between {name} and {dimension} " + "has been successfully updated." + ), + }, + ) + + +@router.post("/nodes/{node_name}/columns/{node_column}/link", status_code=201) +async def add_reference_dimension_link( + node_name: str, + node_column: str, + dimension_node: str, + dimension_column: str, + role: Optional[str] = None, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """ + Add reference dimension link to a node column + """ + node = await Node.get_by_name(session, node_name, raise_if_not_exists=True) + dim_node = await Node.get_by_name(session, dimension_node, raise_if_not_exists=True) + if dim_node.type != NodeType.DIMENSION: # type: ignore + raise DJInvalidInputException( + message=f"Node {node.name} is not of type dimension!", # type: ignore + ) + + # The target and dimension columns should both exist + target_column = await get_column(session, node.current, node_column) # type: ignore + dim_column = await get_column(session, dim_node.current, dimension_column) # type: ignore + + # Check the dimension column's type is compatible with the target column's type + if not dim_column.type.is_compatible(target_column.type): + raise DJInvalidInputException( + f"The column {target_column.name} has type {target_column.type} " + f"and is being linked to the dimension {dimension_node} " + f"via the dimension column {dimension_column}, which has " + f"type {dim_column.type}. These column types are incompatible" + " and the dimension cannot be linked", + ) + + activity_type = ( + ActivityType.UPDATE if target_column.dimension_column else ActivityType.CREATE + ) + + # Create the reference link + target_column.dimension_id = dim_node.id # type: ignore + target_column.dimension_column = ( + f"{dimension_column}[{role}]" if role else dimension_column + ) + session.add(target_column) + session.add( + History( + entity_type=EntityType.LINK, + entity_name=node.name, # type: ignore + node=node.name, # type: ignore + activity_type=activity_type, + details={ + "node_name": node_name, # type: ignore + "node_column": node_column, + "dimension_node": dimension_node, + "dimension_column": dimension_column, + "role": role, + }, + user=current_user.username, + ), + ) + await session.commit() + return JSONResponse( + status_code=201, + content={ + "message": ( + f"{node_name}.{node_column} has been successfully " + f"linked to {dimension_node}.{dimension_column}" + ), + }, + ) + + +@router.delete("/nodes/{node_name}/columns/{node_column}/link", status_code=201) +async def remove_reference_dimension_link( + node_name: str, + node_column: str, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """ + Remove reference dimension link from a node column + """ + node = await Node.get_by_name(session, node_name, raise_if_not_exists=True) + target_column = await get_column(session, node.current, node_column) # type: ignore + if target_column.dimension_id or target_column.dimension_column: + target_column.dimension_id = None + target_column.dimension_column = None + session.add(target_column) + session.add( + History( + entity_type=EntityType.LINK, + entity_name=node.name, # type: ignore + node=node.name, # type: ignore + activity_type=ActivityType.DELETE, + details={ + "node_name": node_name, # type: ignore + "node_column": node_column, + }, + user=current_user.username, + ), + ) + session.commit() + return JSONResponse( + status_code=200, + content={ + "message": ( + f"The reference dimension link on {node_name}.{node_column} has been removed." + ), + }, + ) + return JSONResponse( + status_code=200, + content={ + "message": ( + f"There is no reference dimension link on {node_name}.{node_column}." + ), + }, + ) + + +@router.post("/nodes/{node_name}/link/", status_code=201) +async def add_complex_dimension_link( + node_name: str, + link_input: JoinLinkInput, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """ + Links a source, dimension, or transform node to a dimension with a custom join query. + If a link already exists, updates the link definition. + """ + activity_type = await upsert_complex_dimension_link( + session, + node_name, + link_input, + current_user, + ) + return JSONResponse( + status_code=201, + content={ + "message": ( + f"Dimension node {link_input.dimension_node} has been successfully " + f"linked to node {node_name}." + ) + if activity_type == ActivityType.CREATE + else ( + f"The dimension link between {node_name} and {link_input.dimension_node} " + "has been successfully updated." + ), + }, + ) + + +@router.delete("/nodes/{node_name}/link/", status_code=201) +async def remove_complex_dimension_link( + node_name: str, + link_identifier: LinkDimensionIdentifier, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """ + Removes a complex dimension link based on the dimension node and its role (if any). + """ + return await remove_dimension_link( + session, + node_name, + link_identifier, + current_user, + ) + + +@router.delete("/nodes/{name}/columns/{column}/", status_code=201) +async def delete_dimension_link( + name: str, + column: str, + dimension: str, + dimension_column: Optional[str] = None, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """ + Remove the link between a node column and a dimension node + """ + return await remove_dimension_link( + session, + name, + LinkDimensionIdentifier(dimension_node=dimension, role=None), + current_user, + ) + + +@router.post( + "/nodes/{name}/tags/", + status_code=200, + tags=["tags"], + name="Update Tags on Node", +) +async def tags_node( + name: str, + tag_names: Optional[List[str]] = Query(default=None), + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """ + Add a tag to a node + """ + node = await Node.get_by_name(session=session, name=name) + if not tag_names: + tag_names = [] # pragma: no cover + tags = await get_tags_by_name(session, names=tag_names) + node.tags = tags # type: ignore + + session.add(node) + session.add( + History( + entity_type=EntityType.NODE, + entity_name=node.name, # type: ignore + node=node.name, # type: ignore + activity_type=ActivityType.TAG, + details={ + "tags": tag_names, + }, + user=current_user.username, + ), + ) + await session.commit() + await session.refresh(node) + for tag in tags: + await session.refresh(tag) + + return JSONResponse( + status_code=200, + content={ + "message": ( + f"Node `{name}` has been successfully updated with " + f"the following tags: {', '.join(tag_names)}" + ), + }, + ) + + +@router.post( + "/nodes/{name}/refresh/", + response_model=NodeOutput, + status_code=201, +) +async def refresh_source_node( + name: str, + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + current_user: User = Depends(get_and_update_current_user), +) -> NodeOutput: + """ + Refresh a source node with the latest columns from the query service. + """ + request_headers = dict(request.headers) + source_node = await Node.get_by_name( + session, + name, + options=[ + joinedload(Node.current).options(*NodeRevision.default_load_options()), + joinedload(Node.tags), + ], + ) + current_revision = source_node.current # type: ignore + + # If this is a view-based source node, let's rerun the create view + new_query = None + if current_revision.query: + catalog = await get_catalog_by_name( + session=session, + name=current_revision.catalog.name, + ) + query_create = QueryCreate( + engine_name=catalog.engines[0].name, + catalog_name=catalog.name, + engine_version=catalog.engines[0].version, + submitted_query=current_revision.query, + async_=False, + ) + query_service_client.create_view( + view_name=current_revision.table, + query_create=query_create, + request_headers=request_headers, + ) + new_query = current_revision.query + + # Get the latest columns for the source node's table from the query service + new_columns = [] + try: + new_columns = query_service_client.get_columns_for_table( + current_revision.catalog.name, + current_revision.schema_, # type: ignore + current_revision.table, # type: ignore + request_headers, + current_revision.catalog.engines[0] + if len(current_revision.catalog.engines) >= 1 + else None, + ) + except DJDoesNotExistException: + # continue with the update, if the table was not found + pass + + refresh_details = {} + if new_columns: + # check if any of the columns have changed (only continue with update if they have) + column_changes = {col.identifier() for col in current_revision.columns} != { + (col.name, str(parse_rule(str(col.type), "dataType"))) + for col in new_columns + } + + # if the columns haven't changed and the node has a table, we can skip the update + if not column_changes: + if not source_node.missing_table: # type: ignore + return source_node # type: ignore + # if the columns haven't changed but the node has a missing table, we should fix it + source_node.missing_table = False # type: ignore + refresh_details["missing_table"] = "False" + else: + # since we don't see any columns, we assume the table is gone + if source_node.missing_table: # type: ignore + # but if the node already has a missing table, we can skip the update + return source_node # type: ignore + source_node.missing_table = True # type: ignore + new_columns = current_revision.columns + refresh_details["missing_table"] = "True" + + # Create a new node revision with the updated columns and bump the version + old_version = Version.parse(source_node.current_version) # type: ignore + new_revision = NodeRevision( + name=current_revision.name, + type=current_revision.type, + node_id=current_revision.node_id, + display_name=current_revision.display_name, + description=current_revision.description, + mode=current_revision.mode, + catalog_id=current_revision.catalog_id, + schema_=current_revision.schema_, + table=current_revision.table, + status=current_revision.status, + dimension_links=[ + DimensionLink( + dimension_id=link.dimension_id, + join_sql=link.join_sql, + join_type=link.join_type, + join_cardinality=link.join_cardinality, + materialization_conf=link.materialization_conf, + ) + for link in current_revision.dimension_links + ], + created_by_id=current_user.id, + query=new_query, + ) + new_revision.version = str(old_version.next_major_version()) + new_revision.columns = [ + Column( + name=column.name, + type=column.type, + node_revisions=[new_revision], + order=idx, + ) + for idx, column in enumerate(new_columns) + ] + + # Keep the dimension links and attributes on the columns from the node's + # last revision if any existed + new_revision.copy_dimension_links_from_revision(current_revision) + + # Point the source node to the new revision + source_node.current_version = new_revision.version # type: ignore + new_revision.extra_validation() + + session.add(new_revision) + session.add(source_node) + + refresh_details["version"] = new_revision.version + session.add( + History( + entity_type=EntityType.NODE, + entity_name=source_node.name, # type: ignore + node=source_node.name, # type: ignore + activity_type=ActivityType.REFRESH, + details=refresh_details, + user=current_user.username, + ), + ) + await session.commit() + + source_node = await Node.get_by_name( + session, + name, + options=[ + joinedload(Node.current).options(*NodeRevision.default_load_options()), + joinedload(Node.tags), + ], + ) + await session.refresh(source_node, ["current"]) + return source_node # type: ignore + + +@router.patch("/nodes/{name}/", response_model=NodeOutput) +async def update_node( + name: str, + data: UpdateNode, + *, + session: AsyncSession = Depends(get_session), + request: Request, + query_service_client: QueryServiceClient = Depends(get_query_service_client), + current_user: User = Depends(get_and_update_current_user), + background_tasks: BackgroundTasks, + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), +) -> NodeOutput: + """ + Update a node. + """ + request_headers = dict(request.headers) + await update_any_node( + name, + data, + session=session, + query_service_client=query_service_client, + current_user=current_user, + background_tasks=background_tasks, + validate_access=validate_access, + request_headers=request_headers, + ) + node = await Node.get_by_name( + session, + name, + options=[ + joinedload(Node.current).options(*NodeRevision.default_load_options()), + joinedload(Node.tags), + ], + ) + return node # type: ignore + + +@router.get("/nodes/similarity/{node1_name}/{node2_name}") +async def calculate_node_similarity( + node1_name: str, node2_name: str, *, session: AsyncSession = Depends(get_session) +) -> JSONResponse: + """ + Compare two nodes by how similar their queries are + """ + node1 = await Node.get_by_name( + session, + node1_name, + options=[joinedload(Node.current)], + raise_if_not_exists=True, + ) + node2 = await Node.get_by_name( + session, + node2_name, + options=[joinedload(Node.current)], + raise_if_not_exists=True, + ) + if NodeType.SOURCE in (node1.type, node2.type): # type: ignore + raise DJInvalidInputException( + message="Cannot determine similarity of source nodes", + http_status_code=HTTPStatus.CONFLICT, + ) + node1_ast = parse(node1.current.query) # type: ignore + node2_ast = parse(node2.current.query) # type: ignore + similarity = node1_ast.similarity_score(node2_ast) + return JSONResponse(status_code=200, content={"similarity": similarity}) + + +@router.get( + "/nodes/{name}/downstream/", + response_model=List[DAGNodeOutput], + name="List Downstream Nodes For A Node", +) +async def list_downstream_nodes( + name: str, + *, + node_type: NodeType = None, + depth: int = -1, + session: AsyncSession = Depends(get_session), +) -> List[DAGNodeOutput]: + """ + List all nodes that are downstream from the given node, filterable by type and max depth. + Setting a max depth of -1 will include all downstream nodes. + """ + return await get_downstream_nodes( + session=session, + node_name=name, + node_type=node_type, + include_deactivated=False, + depth=depth, + ) + + +@router.get( + "/nodes/{name}/upstream/", + response_model=List[DAGNodeOutput], + name="List Upstream Nodes For A Node", +) +async def list_upstream_nodes( + name: str, + *, + node_type: NodeType = None, + session: AsyncSession = Depends(get_session), +) -> List[DAGNodeOutput]: + """ + List all nodes that are upstream from the given node, filterable by type. + """ + return await get_upstream_nodes(session, name, node_type) + + +@router.get( + "/nodes/{name}/dag/", + name="List All Connected Nodes (Upstreams + Downstreams)", +) +async def list_node_dag( + name: str, *, session: AsyncSession = Depends(get_session) +) -> List[DAGNodeOutput]: + """ + List all nodes that are part of the DAG of the given node. This means getting all upstreams, + downstreams, and linked dimension nodes. + """ + node = await Node.get_by_name( + session, + name, + options=_node_output_options(), + raise_if_not_exists=True, + ) + dimension_nodes = await get_dimensions( + session, + node.current.parents[0] if node.type == NodeType.METRIC else node, # type: ignore + with_attributes=False, + ) + if node.type == NodeType.METRIC: # type: ignore # pragma: no cover + dimension_nodes = dimension_nodes + node.current.parents # type: ignore + dimension_nodes += [node] # type: ignore + downstreams = await get_downstream_nodes( + session, + name, + include_deactivated=False, + include_cubes=False, + ) + upstreams = await get_upstream_nodes(session, name, include_deactivated=False) + dag_nodes = set(dimension_nodes + downstreams + upstreams) + return list(dag_nodes) + + +@router.get( + "/nodes/{name}/dimensions/", + response_model=List[DimensionAttributeOutput], + name="List All Dimension Attributes", +) +async def list_all_dimension_attributes( + name: str, *, depth: int = 30, session: AsyncSession = Depends(get_session) +) -> List[DimensionAttributeOutput]: + """ + List all available dimension attributes for the given node. + """ + node = await Node.get_by_name( + session, + name, + options=[ + joinedload(Node.current).options( + joinedload(NodeRevision.parents).options(joinedload(Node.current)), + ), + ], + ) + dimensions = await get_dimensions( + session, + node, # type: ignore + with_attributes=True, + depth=depth, + ) + filter_only_dimensions = await get_filter_only_dimensions(session, name) + return dimensions + filter_only_dimensions + + +@router.get( + "/nodes/{name}/lineage/", + response_model=List[LineageColumn], + name="List column level lineage of node", +) +async def column_lineage( + name: str, *, session: AsyncSession = Depends(get_session) +) -> List[LineageColumn]: + """ + List column-level lineage of a node in a graph + """ + + node = await Node.get_by_name( + session, + name, + options=[ + selectinload(Node.current).options( + selectinload(NodeRevision.columns).options( + selectinload(Column.attributes).joinedload( + ColumnAttribute.attribute_type, + ), + selectinload(Column.dimension), + selectinload(Column.partition), + ), + ), + ], + ) + if node.current.lineage: # type: ignore + return node.current.lineage # type: ignore + return await get_column_level_lineage(session, node.current) # type: ignore # pragma: no cover + + +@router.patch( + "/nodes/{node_name}/columns/{column_name}/", + response_model=ColumnOutput, + status_code=201, +) +async def set_column_display_name( + node_name: str, + column_name: str, + display_name: str, + current_user: User = Depends(get_and_update_current_user), + *, + session: AsyncSession = Depends(get_session), +) -> ColumnOutput: + """ + Set column name for the node + """ + node = await Node.get_by_name( + session, + node_name, + options=[joinedload(Node.current)], + ) + column = await get_column(session, node.current, column_name) # type: ignore + column.display_name = display_name + session.add(column) + session.add( + History( + entity_type=EntityType.COLUMN_ATTRIBUTE, + node=node.name, # type: ignore + activity_type=ActivityType.UPDATE, + details={ + "column": column.name, + "display_name": display_name, + }, + user=current_user.username, + ), + ) + await session.commit() + return column + + +@router.post( + "/nodes/{node_name}/columns/{column_name}/partition", + response_model=ColumnOutput, + status_code=201, + name="Set Node Column as Partition", +) +async def set_column_partition( + node_name: str, + column_name: str, + input_partition: PartitionInput, + *, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> ColumnOutput: + """ + Add or update partition columns for the specified node. + """ + node = await Node.get_by_name( + session, + node_name, + options=[ + joinedload(Node.current).options( + *NodeRevision.default_load_options(), + joinedload(NodeRevision.cube_elements), + ), + ], + ) + column = get_node_column(node, column_name) # type: ignore + upsert_partition_event = History( + entity_type=EntityType.PARTITION, + node=node_name, + activity_type=ActivityType.CREATE, + details={ + "column": column_name, + "partition": input_partition.dict(), + }, + user=current_user.username, + ) + + if input_partition.type_ == PartitionType.TEMPORAL: + if input_partition.granularity is None: + raise DJInvalidInputException( + message=f"The granularity must be provided for temporal partitions. " + f"One of: {[val.name for val in Granularity]}", + ) + if input_partition.format is None: + raise DJInvalidInputException( + message="The temporal partition column's datetime format must be provided.", + ) + + if column.partition: + column.partition.type_ = input_partition.type_ + column.partition.granularity = input_partition.granularity + column.partition.format = input_partition.format + session.add(column) + upsert_partition_event.activity_type = ActivityType.UPDATE + session.add(upsert_partition_event) + else: + partition = Partition( + column=column, + type_=input_partition.type_, + granularity=input_partition.granularity, + format=input_partition.format, + ) + session.add(partition) + session.add(upsert_partition_event) + + await session.commit() + await session.refresh(column) + return column + + +@router.post( + "/nodes/{node_name}/copy", + response_model=DAGNodeOutput, + name="Copy A Node", +) +async def copy_node( + node_name: str, + *, + new_name: str, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> DAGNodeOutput: + """ + Copy this node to a new name. + """ + # Check to make sure that the new node's namespace exists + new_node_namespace = ".".join(new_name.split(".")[:-1]) + await get_node_namespace(session, new_node_namespace, raise_if_not_exists=True) + + # Check if there is already a node with the new name + existing_new_node = await get_node_by_name( + session, + new_name, + raise_if_not_exists=False, + include_inactive=True, + ) + if existing_new_node: + if existing_new_node.deactivated_at: + await hard_delete_node(new_name, session, current_user) + else: + raise DJAlreadyExistsException( + f"A node with name {new_name} already exists.", + ) + + # Copy existing node to the new name + await copy_to_new_node(session, node_name, new_name, current_user) + new_node = await Node.get_by_name(session, new_name) + return new_node # type: ignore diff --git a/datajunction-server/datajunction_server/api/notifications.py b/datajunction-server/datajunction_server/api/notifications.py new file mode 100644 index 000000000..02c590e6e --- /dev/null +++ b/datajunction-server/datajunction_server/api/notifications.py @@ -0,0 +1,124 @@ +"""Dependency for notifications""" + +import logging +from http import HTTPStatus +from typing import Annotated, Optional + +from fastapi import Body, Depends +from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from datajunction_server.database.history import ActivityType, EntityType, History +from datajunction_server.database.notification_preference import NotificationPreference +from datajunction_server.database.user import User +from datajunction_server.errors import DJDoesNotExistException +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.utils import get_and_update_current_user, get_session + +router = SecureAPIRouter(tags=["notifications"]) +_logger = logging.getLogger(__name__) + + +def get_notifier(): + """Returns a method for sending notifications for an event""" + + def notify(event: History): + """Send a notification for an event""" + _logger.debug("Sending notification for event %s", event) + + return notify + + +@router.post("/notifications/subscribe") +async def subscribe( + entity_type: Annotated[EntityType, Body()], + entity_name: Annotated[str, Body()], + activity_types: list[ActivityType], + alert_types: list[str], + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """Subscribes to notificaitons by upserting a notification preference""" + session.add( + NotificationPreference( + entity_type=entity_type, + entity_name=entity_name, + activity_types=activity_types, + alert_types=alert_types, + user=current_user, + ), + ) + await session.commit() + return JSONResponse( + status_code=201, + content={ + "message": ( + f"Notification preferences successfully saved for {entity_name}" + ), + }, + ) + + +@router.delete("/notifications/unsubscribe") +async def unsubscribe( + entity_type: EntityType, + entity_name: str, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """Unsubscribes from notifications by deleting a notification preference""" + result = await session.execute( + select(NotificationPreference).where( + NotificationPreference.entity_type == entity_type, + NotificationPreference.entity_name == entity_name, + NotificationPreference.user_id == current_user.id, + ), + ) + notification_preference = result.scalars().first() + + if not notification_preference: + raise DJDoesNotExistException( + message=f"No notification preference found for {entity_name}", + http_status_code=HTTPStatus.NOT_FOUND, + ) + await session.delete(notification_preference) + await session.commit() + return JSONResponse( + status_code=200, + content={ + "message": ( + f"Notification preferences successfully removed for {entity_name}" + ), + }, + ) + + +@router.get("/notifications/") +async def get_notification_preferences( + entity_name: Optional[str] = None, + entity_type: Optional[EntityType] = None, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> JSONResponse: + """Subscribes to notificaitons by upserting a notification preference""" + statement = select(NotificationPreference).where( + NotificationPreference.user == current_user, + ) + if entity_name: + statement = statement.where(NotificationPreference.entity_name == entity_name) + if entity_type: + statement = statement.where(NotificationPreference.entity_type == entity_type) + result = await session.execute(statement) + notification_preferences = [ + { + "entity_type": pref.entity_type, + "entity_name": pref.entity_name, + "activity_types": pref.activity_types, + "user_id": pref.user.id, + "username": pref.user.username, + "alert_types": pref.alert_types, + } + for pref in result.scalars().all() + ] + return JSONResponse(content=notification_preferences) diff --git a/datajunction-server/datajunction_server/api/sql.py b/datajunction-server/datajunction_server/api/sql.py new file mode 100644 index 000000000..c4151f4cc --- /dev/null +++ b/datajunction-server/datajunction_server/api/sql.py @@ -0,0 +1,500 @@ +""" +SQL related APIs. +""" + +import logging +from collections import OrderedDict +from http import HTTPStatus +from typing import List, Optional, Tuple, cast + +from fastapi import BackgroundTasks, Depends, Query +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.api.helpers import ( + assemble_column_metadata, + build_sql_for_multiple_metrics, + get_query, + validate_orderby, +) +from datajunction_server.database import Engine, Node +from datajunction_server.database.queryrequest import QueryBuildType, QueryRequest +from datajunction_server.database.user import User +from datajunction_server.errors import DJInvalidInputException +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.internal.access.authorization import validate_access +from datajunction_server.internal.engines import get_engine +from datajunction_server.models import access +from datajunction_server.models.access import AccessControlStore +from datajunction_server.models.metric import TranslatedSQL +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.sql import GeneratedSQL +from datajunction_server.models.user import UserOutput +from datajunction_server.utils import ( + Settings, + get_and_update_current_user, + get_session, + get_settings, +) + +_logger = logging.getLogger(__name__) +settings = get_settings() +router = SecureAPIRouter(tags=["sql"]) + + +@router.get( + "/sql/measures/v2/", + response_model=List[GeneratedSQL], + name="Get Measures SQL", +) +async def get_measures_sql_for_cube_v2( + metrics: List[str] = Query([]), + dimensions: List[str] = Query([]), + filters: List[str] = Query([]), + orderby: List[str] = Query([]), + preaggregate: bool = Query( + False, + description=( + "Whether to pre-aggregate to the requested dimensions so that " + "subsequent queries are more efficient." + ), + ), + *, + include_all_columns: bool = Query( + False, + description=( + "Whether to include all columns or only those necessary " + "for the metrics and dimensions in the cube" + ), + ), + settings: Settings = Depends(get_settings), + session: AsyncSession = Depends(get_session), + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + current_user: Optional[User] = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), + use_materialized: bool = True, +) -> List[GeneratedSQL]: + """ + Return measures SQL for a set of metrics with dimensions and filters. + + The measures query can be used to produce intermediate table(s) with all the measures + and dimensions needed prior to applying specific metric aggregations. + + This endpoint returns one SQL query per upstream node of the requested metrics. + For example, if some of your metrics are aggregations on measures in parent node A + and others are aggregations on measures in parent node B, this endpoint will generate + two measures queries, one for A and one for B. + """ + from datajunction_server.construction.build_v2 import ( + get_measures_query, + ) + + metrics = list(OrderedDict.fromkeys(metrics)) + measures_query = await get_measures_query( + session=session, + metrics=metrics, + dimensions=dimensions, + filters=filters, + orderby=orderby, + engine_name=engine_name, + engine_version=engine_version, + current_user=current_user, + validate_access=validate_access, + include_all_columns=include_all_columns, + sql_transpilation_library=settings.sql_transpilation_library, + use_materialized=use_materialized, + preagg_requested=preaggregate, + ) + return measures_query + + +async def build_and_save_node_sql( + node_name: str, + dimensions: List[str] = Query([]), + filters: List[str] = Query([]), + orderby: List[str] = Query([]), + limit: Optional[int] = None, + *, + session: AsyncSession = Depends(get_session), + engine: Engine, + access_control: AccessControlStore, + ignore_errors: bool = True, + use_materialized: bool = True, +) -> QueryRequest: + """ + Build node SQL and save it to query requests + """ + node = cast( + Node, + await Node.get_by_name(session, node_name, raise_if_not_exists=True), + ) + + # If it's a cube, we'll build SQL for the metrics in the cube, along with any additional + # dimensions or filters provided in the arguments + if node.type == NodeType.CUBE: + node = cast( + Node, + await Node.get_cube_by_name(session, node_name), + ) + dimensions = list( + OrderedDict.fromkeys(node.current.cube_node_dimensions + dimensions), + ) + translated_sql, engine, _ = await build_sql_for_multiple_metrics( + session=session, + metrics=node.current.cube_node_metrics, + dimensions=dimensions, + filters=filters, + orderby=orderby, + limit=limit, + engine_name=engine.name if engine else None, + engine_version=engine.version if engine else None, + access_control=access_control, + use_materialized=use_materialized, + ) + # We save the request for both the cube and the metrics, so that if someone makes either + # of these types of requests, they'll go to the cached query + requests_to_save = [ + (node.current.cube_node_metrics, QueryBuildType.METRICS), + ([node_name], QueryBuildType.NODE), + ] + for nodes, query_type in requests_to_save: + request = await QueryRequest.save_query_request( + session=session, + nodes=nodes, + dimensions=dimensions, + filters=filters, + orderby=orderby, + limit=limit, + engine_name=engine.name if engine else None, + engine_version=engine.version if engine else None, + query_type=query_type, + query=translated_sql.sql, + columns=[col.dict() for col in translated_sql.columns], # type: ignore + ) + return request + + # For all other nodes, build the node query + node = await Node.get_by_name(session, node_name, raise_if_not_exists=True) # type: ignore + if node.type == NodeType.METRIC: + translated_sql, engine, _ = await build_sql_for_multiple_metrics( + session, + [node_name], + dimensions, + filters, + orderby, + limit, + engine.name if engine else None, + engine.version if engine else None, + access_control=access_control, + ignore_errors=ignore_errors, + use_materialized=use_materialized, + ) + query = translated_sql.sql + columns = translated_sql.columns + else: + query_ast = await get_query( + session=session, + node_name=node_name, + dimensions=dimensions, + filters=filters, + orderby=orderby, + limit=limit, + engine=engine, + access_control=access_control, + use_materialized=use_materialized, + ) + columns = [ + assemble_column_metadata(col) # type: ignore + for col in query_ast.select.projection + ] + query = str(query_ast) + + query_request = await QueryRequest.save_query_request( + session=session, + nodes=[node_name], + dimensions=dimensions, + filters=filters, + orderby=orderby, + limit=limit, + engine_name=engine.name if engine else None, + engine_version=engine.version if engine else None, + query_type=QueryBuildType.NODE, + query=query, + columns=[col.dict() for col in columns or []], + ) + return query_request + + +async def get_node_sql( + node_name: str, + dimensions: List[str] = Query([]), + filters: List[str] = Query([]), + orderby: List[str] = Query([]), + limit: Optional[int] = None, + *, + session: AsyncSession = Depends(get_session), + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + current_user: User, + validate_access: access.ValidateAccessFn, + background_tasks: BackgroundTasks, + ignore_errors: bool = True, + use_materialized: bool = True, +) -> Tuple[TranslatedSQL, QueryRequest]: + """ + Return SQL for a node. + """ + dimensions = [dim for dim in dimensions if dim and dim != ""] + access_control = access.AccessControlStore( + validate_access=validate_access, + user=UserOutput.from_orm(current_user), + base_verb=access.ResourceRequestVerb.READ, + ) + + engine = ( + await get_engine(session, engine_name, engine_version) # type: ignore + if engine_name + else None + ) + validate_orderby(orderby, [node_name], dimensions) + + if query_request := await QueryRequest.get_query_request( + session, + nodes=[node_name], + dimensions=dimensions, + filters=filters, + orderby=orderby, + limit=limit, + engine_name=engine.name if engine else None, + engine_version=engine.version if engine else None, + query_type=QueryBuildType.NODE, + ): + # Update the node SQL in a background task to keep it up-to-date + background_tasks.add_task( + build_and_save_node_sql, + node_name=node_name, + dimensions=dimensions, + filters=filters, + orderby=orderby, + limit=limit, + session=session, + engine=engine, + access_control=access_control, + use_materialized=use_materialized, + ) + return ( + TranslatedSQL( + sql=query_request.query, + columns=query_request.columns, + dialect=engine.dialect if engine else None, + ), + query_request, + ) + + query_request = await build_and_save_node_sql( + node_name=node_name, + dimensions=dimensions, + filters=filters, + orderby=orderby, + limit=limit, + session=session, + engine=engine, # type: ignore + access_control=access_control, + ignore_errors=ignore_errors, + use_materialized=use_materialized, + ) + return ( + TranslatedSQL( + sql=query_request.query, + columns=query_request.columns, + dialect=engine.dialect if engine else None, + ), + query_request, + ) + + +@router.get( + "/sql/{node_name}/", + response_model=TranslatedSQL, + name="Get SQL For A Node", +) +async def get_sql( + node_name: str, + dimensions: List[str] = Query([]), + filters: List[str] = Query([]), + orderby: List[str] = Query([]), + limit: Optional[int] = None, + *, + session: AsyncSession = Depends(get_session), + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), + background_tasks: BackgroundTasks, + ignore_errors: Optional[bool] = True, + use_materialized: Optional[bool] = True, +) -> TranslatedSQL: + """ + Return SQL for a node. + """ + translated_sql, _ = await get_node_sql( + node_name, + dimensions, + filters, + orderby, + limit, + session=session, + engine_name=engine_name, + engine_version=engine_version, + current_user=current_user, + validate_access=validate_access, + background_tasks=background_tasks, + ignore_errors=ignore_errors, # type: ignore + use_materialized=use_materialized, # type: ignore + ) + return translated_sql + + +@router.get("/sql/", response_model=TranslatedSQL, name="Get SQL For Metrics") +async def get_sql_for_metrics( + metrics: List[str] = Query([]), + dimensions: List[str] = Query([]), + filters: List[str] = Query([]), + orderby: List[str] = Query([]), + limit: Optional[int] = None, + *, + session: AsyncSession = Depends(get_session), + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + current_user: User = Depends(get_and_update_current_user), + validate_access: access.ValidateAccessFn = Depends( + validate_access, + ), + ignore_errors: Optional[bool] = True, + use_materialized: Optional[bool] = True, + background_tasks: BackgroundTasks, +) -> TranslatedSQL: + """ + Return SQL for a set of metrics with dimensions and filters + """ + + access_control = access.AccessControlStore( + validate_access=validate_access, + user=current_user, + base_verb=access.ResourceRequestVerb.READ, + ) + + # make sure all metrics exist and have correct node type + nodes = [ + await Node.get_by_name(session, node, raise_if_not_exists=True) + for node in metrics + ] + non_metric_nodes = [node for node in nodes if node and node.type != NodeType.METRIC] + + if non_metric_nodes: + raise DJInvalidInputException( + message="All nodes must be of metric type, but some are not: " + f"{', '.join([f'{n.name} ({n.type})' for n in non_metric_nodes])} .", + http_status_code=HTTPStatus.UNPROCESSABLE_ENTITY, + ) + + if query_request := await QueryRequest.get_query_request( + session, + nodes=metrics, + dimensions=dimensions, + filters=filters, + orderby=orderby, + limit=limit, + engine_name=engine_name, + engine_version=engine_version, + query_type=QueryBuildType.METRICS, + ): + # Update the node SQL in a background task to keep it up-to-date + background_tasks.add_task( + build_and_save_sql_for_metrics, + session=session, + metrics=metrics, + dimensions=dimensions, + filters=filters, + orderby=orderby, + limit=limit, + engine_name=engine_name, + engine_version=engine_version, + access_control=access_control, + ignore_errors=ignore_errors, + use_materialized=use_materialized, + ) + engine = ( + await get_engine(session, engine_name, engine_version) # type: ignore + if engine_name + else None + ) + return TranslatedSQL( + sql=query_request.query, + columns=query_request.columns, + dialect=engine.dialect if engine else None, + ) + + return await build_and_save_sql_for_metrics( + session, + metrics, + dimensions, + filters, + orderby, + limit, + engine_name, + engine_version, + access_control, + ignore_errors=ignore_errors, # type: ignore + use_materialized=use_materialized, # type: ignore + ) + + +async def build_and_save_sql_for_metrics( + session: AsyncSession, + metrics: List[str], + dimensions: List[str], + filters: List[str] = None, + orderby: List[str] = None, + limit: Optional[int] = None, + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + access_control: Optional[access.AccessControlStore] = None, + ignore_errors: bool = True, + use_materialized: bool = True, +): + """ + Builds and saves SQL for metrics. + """ + translated_sql, _, _ = await build_sql_for_multiple_metrics( + session, + metrics, + dimensions, + filters, + orderby, + limit, + engine_name, + engine_version, + access_control, + ignore_errors=ignore_errors, # type: ignore + use_materialized=use_materialized, # type: ignore + ) + + await QueryRequest.save_query_request( + session=session, + nodes=metrics, + dimensions=dimensions, + filters=filters, # type: ignore + orderby=orderby, # type: ignore + limit=limit, + engine_name=engine_name, + engine_version=engine_version, + query_type=QueryBuildType.METRICS, + query=translated_sql.sql, + columns=[col.dict() for col in translated_sql.columns], # type: ignore + ) + return translated_sql diff --git a/datajunction-server/datajunction_server/api/tags.py b/datajunction-server/datajunction_server/api/tags.py new file mode 100644 index 000000000..b4ab05abb --- /dev/null +++ b/datajunction-server/datajunction_server/api/tags.py @@ -0,0 +1,204 @@ +""" +Tag related APIs. +""" + +from typing import List, Optional + +from fastapi import Depends +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload + +from datajunction_server.database import Node +from datajunction_server.database.history import ActivityType, EntityType, History +from datajunction_server.database.tag import Tag +from datajunction_server.database.user import User +from datajunction_server.errors import DJAlreadyExistsException, DJDoesNotExistException +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.models.node import NodeMinimumDetail +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.tag import CreateTag, TagOutput, UpdateTag +from datajunction_server.utils import ( + get_and_update_current_user, + get_session, + get_settings, +) + +settings = get_settings() +router = SecureAPIRouter(tags=["tags"]) + + +async def get_tags_by_name( + session: AsyncSession, + names: List[str], +) -> List[Tag]: + """ + Retrieves a list of tags by name + """ + statement = select(Tag).where(Tag.name.in_(names)) # type: ignore + tags = (await session.execute(statement)).scalars().all() + difference = set(names) - {tag.name for tag in tags} + if difference: + raise DJDoesNotExistException( + message=f"Tags not found: {', '.join(difference)}", + ) + return tags + + +async def get_tag_by_name( + session: AsyncSession, + name: str, + raise_if_not_exists: bool = False, + for_update: bool = False, +): + """ + Retrieves a tag by its name. + """ + statement = select(Tag).where(Tag.name == name) + if for_update: + statement = statement.with_for_update().execution_options( + populate_existing=True, + ) + tag = (await session.execute(statement)).scalars().one_or_none() + if not tag and raise_if_not_exists: + raise DJDoesNotExistException( # pragma: no cover + message=(f"A tag with name `{name}` does not exist."), + http_status_code=404, + ) + return tag + + +@router.get("/tags/", response_model=List[TagOutput]) +async def list_tags( + tag_type: Optional[str] = None, *, session: AsyncSession = Depends(get_session) +) -> List[TagOutput]: + """ + List all available tags. + """ + statement = select(Tag) + if tag_type: + statement = statement.where(Tag.tag_type == tag_type) + result = await session.execute(statement) + return result.scalars().all() + + +@router.get("/tags/{name}/", response_model=TagOutput) +async def get_a_tag( + name: str, *, session: AsyncSession = Depends(get_session) +) -> TagOutput: + """ + Return a tag by name. + """ + tag = await get_tag_by_name(session, name, raise_if_not_exists=True) + return tag + + +@router.post("/tags/", response_model=TagOutput, status_code=201) +async def create_a_tag( + data: CreateTag, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> TagOutput: + """ + Create a tag. + """ + tag = await get_tag_by_name(session, data.name, raise_if_not_exists=False) + if tag: + raise DJAlreadyExistsException( + message=f"A tag with name `{data.name}` already exists!", + http_status_code=500, + ) + tag = Tag( + name=data.name, + tag_type=data.tag_type, + description=data.description, + display_name=data.display_name, + tag_metadata=data.tag_metadata, + created_by_id=current_user.id, + ) + session.add(tag) + session.add( + History( + entity_type=EntityType.TAG, + entity_name=tag.name, + activity_type=ActivityType.CREATE, + user=current_user.username, + ), + ) + await session.commit() + await session.refresh(tag) + return tag + + +@router.patch("/tags/{name}/", response_model=TagOutput) +async def update_a_tag( + name: str, + data: UpdateTag, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_and_update_current_user), +) -> TagOutput: + """ + Update a tag. + """ + tag = await get_tag_by_name( + session, + name, + raise_if_not_exists=True, + for_update=True, + ) + + if data.description: + tag.description = data.description + if data.tag_metadata: + tag.tag_metadata = data.tag_metadata + if data.display_name: + tag.display_name = data.display_name + session.add(tag) + session.add( + History( + entity_type=EntityType.TAG, + entity_name=tag.name, + activity_type=ActivityType.UPDATE, + details=data.dict(), + user=current_user.username, + ), + ) + await session.commit() + await session.refresh(tag) + return tag + + +@router.get("/tags/{name}/nodes/", response_model=List[NodeMinimumDetail]) +async def list_nodes_for_a_tag( + name: str, + node_type: Optional[NodeType] = None, + *, + session: AsyncSession = Depends(get_session), +) -> List[NodeMinimumDetail]: + """ + Find nodes tagged with the tag, filterable by node type. + """ + statement = ( + select(Tag) + .where(Tag.name == name) + .options(joinedload(Tag.nodes).options(joinedload(Node.current))) + ) + tag = (await session.execute(statement)).unique().scalars().one_or_none() + if not tag: + raise DJDoesNotExistException( + message=f"A tag with name `{name}` does not exist.", + http_status_code=404, + ) + if not node_type: + return sorted( + [node.current for node in tag.nodes if not node.deactivated_at], + key=lambda x: x.name, + ) + return sorted( + [ + node.current + for node in tag.nodes + if node.type == node_type and not node.deactivated_at + ], + key=lambda x: x.name, + ) diff --git a/datajunction-server/datajunction_server/api/users.py b/datajunction-server/datajunction_server/api/users.py new file mode 100644 index 000000000..26a628c94 --- /dev/null +++ b/datajunction-server/datajunction_server/api/users.py @@ -0,0 +1,89 @@ +""" +User related APIs. +""" + +from typing import List, Union + +from fastapi import Depends, Query +from sqlalchemy import distinct, func, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload, selectinload + +from datajunction_server.database.column import Column +from datajunction_server.database.history import ActivityType, EntityType, History +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.user import User +from datajunction_server.internal.access.authentication.http import SecureAPIRouter +from datajunction_server.models.node import NodeMinimumDetail +from datajunction_server.models.user import UserActivity +from datajunction_server.utils import get_session, get_settings + +settings = get_settings() +router = SecureAPIRouter(tags=["users"]) + + +@router.get("/users/{username}", response_model=List[NodeMinimumDetail]) +async def list_nodes_by_username( + username: str, + *, + session: AsyncSession = Depends(get_session), + activity_types: List[str] = Query([ActivityType.CREATE, ActivityType.UPDATE]), +) -> List[NodeMinimumDetail]: + """ + List all nodes with the specified activity type(s) by the user + """ + statement = select(distinct(History.entity_name)).where( + (History.user == username) + & (History.entity_type == EntityType.NODE) + & (History.activity_type.in_(activity_types)), + ) + result = await session.execute(statement) + nodes = await Node.get_by_names( + session=session, + names=list(set(result.scalars().all())), + options=[ + joinedload(Node.current).options( + selectinload(NodeRevision.cube_elements) + .selectinload(Column.node_revisions) + .options( + selectinload(NodeRevision.node), + ), + ), + ], + ) + return [node.current for node in nodes] + + +@router.get("/users", response_model=List[Union[str, UserActivity]]) +async def list_users_with_activity( + session: AsyncSession = Depends(get_session), + *, + with_activity: bool = False, +) -> List[Union[str, UserActivity]]: + """ + Lists all users. The endpoint will include user activity counts if the + `with_activity` flag is set to true. + """ + if not with_activity: + statement = select(User.username) + result = await session.execute(statement) + return result.scalars().all() + + statement = ( + select( + User.username, + func.count(History.id).label("count"), + ) + .join( + History, + onclause=(User.username == History.user), + isouter=True, + ) + .group_by(User.username) + .order_by(func.count(History.id).desc()) + ) + result = await session.execute(statement) + return [ + UserActivity(username=user_activity[0], count=user_activity[1]) + for user_activity in result.all() + ] diff --git a/datajunction-server/datajunction_server/config.py b/datajunction-server/datajunction_server/config.py new file mode 100644 index 000000000..d92511e92 --- /dev/null +++ b/datajunction-server/datajunction_server/config.py @@ -0,0 +1,131 @@ +""" +Configuration for the datajunction server. +""" + +import urllib.parse +from datetime import timedelta +from pathlib import Path +from typing import TYPE_CHECKING, List, Optional + +from cachelib.base import BaseCache +from cachelib.file import FileSystemCache +from cachelib.redis import RedisCache +from celery import Celery +from pydantic import BaseSettings + +if TYPE_CHECKING: + pass + + +class Settings(BaseSettings): # pragma: no cover + """ + DataJunction configuration. + """ + + name: str = "DJ server" + description: str = "A DataJunction metrics layer" + url: str = "http://localhost:8000/" + + # A list of hostnames that are allowed to make cross-site HTTP requests + cors_origin_whitelist: List[str] = ["http://localhost:3000"] + + # SQLAlchemy URI for the metadata database. + index: str = "postgresql+psycopg://dj:dj@postgres_metadata:5432/dj" + + # Directory where the repository lives. This should have 2 subdirectories, "nodes" and + # "databases". + repository: Path = Path(".") + + # Where to store the results from queries. + results_backend: BaseCache = FileSystemCache("/tmp/dj", default_timeout=0) + + # Cache for paginating results and potentially other things. + redis_cache: Optional[str] = None + paginating_timeout: timedelta = timedelta(minutes=5) + + # Configure Celery for async requests. If not configured async queries will be + # executed using FastAPI's ``BackgroundTasks``. + celery_broker: Optional[str] = None + + # How long to wait when pinging databases to find out the fastest online database. + do_ping_timeout: timedelta = timedelta(seconds=5) + + # Query service + query_service: Optional[str] = None + + # The namespace where source nodes for registered tables should exist + source_node_namespace: Optional[str] = "source" + + # This specifies what the DJ_LOGICAL_TIMESTAMP() macro should be replaced with. + # This defaults to an Airflow compatible value, but other examples include: + # ${dj_logical_timestamp} + # {{ dj_logical_timestamp }} + # $dj_logical_timestamp + dj_logical_timestamp_format: Optional[str] = "${dj_logical_timestamp}" + + # DJ UI host, used for OAuth redirection + frontend_host: Optional[str] = "http://localhost:3000" + + # Library to use when transpiling SQL to other dialects + sql_transpilation_library: Optional[str] = None + + # 128 bit DJ secret, used to encrypt passwords and JSON web tokens + secret: str = "a-fake-secretkey" + + # GitHub OAuth application client ID + github_oauth_client_id: Optional[str] = None + + # GitHub OAuth application client secret + github_oauth_client_secret: Optional[str] = None + + # Google OAuth application client ID + google_oauth_client_id: Optional[str] = None + + # Google OAuth application client secret + google_oauth_client_secret: Optional[str] = None + + # Google OAuth application client secret file + google_oauth_client_secret_file: Optional[str] = None + + # Interval in seconds with which to expire caching of any indexes + index_cache_expire = 60 + + default_catalog_id: int = 0 + + # SQLAlchemy engine config + db_pool_size = 20 + db_max_overflow = 20 + db_pool_timeout = 10 + db_connect_timeout = 5 + db_pool_pre_ping = True + db_echo = False + db_keepalives = 1 + db_keepalives_idle = 30 + db_keepalives_interval = 10 + db_keepalives_count = 5 + + # Maximum amount of nodes to return for requests to list all nodes + node_list_max = 10000 + + @property + def celery(self) -> Celery: + """ + Return Celery app. + """ + return Celery(__name__, broker=self.celery_broker) + + @property + def cache(self) -> Optional[BaseCache]: + """ + Configure the Redis cache. + """ + if self.redis_cache is None: + return None + + parsed = urllib.parse.urlparse(self.redis_cache) + return RedisCache( + host=parsed.hostname, + port=parsed.port, + password=parsed.password, + db=parsed.path.strip("/"), + ) diff --git a/datajunction-server/datajunction_server/constants.py b/datajunction-server/datajunction_server/constants.py new file mode 100644 index 000000000..0ffdfb9be --- /dev/null +++ b/datajunction-server/datajunction_server/constants.py @@ -0,0 +1,20 @@ +""" +Useful constants. +""" + +from datetime import timedelta +from uuid import UUID + +DJ_DATABASE_ID = 0 +DJ_DATABASE_UUID = UUID("594804bf-47cb-426c-83c4-94a348e95972") +SQLITE_DATABASE_ID = -1 +SQLITE_DATABASE_UUID = UUID("3619eeba-d628-4ab1-9dd5-65738ab3c02f") + +DEFAULT_DIMENSION_COLUMN = "id" + +# used by the SQLAlchemy client +QUERY_EXECUTE_TIMEOUT = timedelta(seconds=60) +GET_COLUMNS_TIMEOUT = timedelta(seconds=60) + +AUTH_COOKIE = "__dj" +LOGGED_IN_FLAG_COOKIE = "__djlif" diff --git a/datajunction-server/datajunction_server/construction/__init__.py b/datajunction-server/datajunction_server/construction/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/datajunction-server/datajunction_server/construction/build.py b/datajunction-server/datajunction_server/construction/build.py new file mode 100755 index 000000000..dcfcff547 --- /dev/null +++ b/datajunction-server/datajunction_server/construction/build.py @@ -0,0 +1,367 @@ +"""Functions for building DJ node queries""" + +import collections +import logging +import os +from concurrent.futures import ThreadPoolExecutor +from typing import DefaultDict, List, Optional, Set, Tuple + +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database import Engine +from datajunction_server.database.column import Column +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.errors import DJError, DJInvalidInputException, ErrorCode +from datajunction_server.internal.engines import get_engine +from datajunction_server.models import access +from datajunction_server.models.cube_materialization import Measure +from datajunction_server.models.engine import Dialect +from datajunction_server.models.materialization import GenericCubeConfig +from datajunction_server.models.node import BuildCriteria +from datajunction_server.naming import LOOKUP_CHARS, amenable_name, from_amenable_name +from datajunction_server.sql.dag import get_shared_dimensions +from datajunction_server.sql.decompose import MeasureExtractor +from datajunction_server.sql.parsing.backends.antlr4 import ast, parse +from datajunction_server.sql.parsing.types import ColumnType +from datajunction_server.utils import SEPARATOR + +_logger = logging.getLogger(__name__) + + +def get_default_criteria( + node: NodeRevision, + engine: Optional[Engine] = None, +) -> BuildCriteria: + """ + Get the default build criteria for a node. + """ + # Set the dialect by using the provided engine, if any. If no engine is specified, + # set the dialect by finding available engines for this node, or default to Spark + dialect = ( + engine.dialect + if engine + else ( + node.catalog.engines[0].dialect + if node.catalog and node.catalog.engines and node.catalog.engines[0].dialect + else Dialect.SPARK + ) + ) + return BuildCriteria( + dialect=dialect, + target_node_name=node.name, + ) + + +def rename_columns( + built_ast: ast.Query, + node: NodeRevision, + preaggregate: bool = False, +): + """ + Rename columns in the built ast to fully qualified column names. + """ + projection = [] + node_columns = {col.name for col in node.columns} + for expression in built_ast.select.projection: + if ( + not isinstance(expression, ast.Alias) + and not isinstance( + expression, + ast.Wildcard, + ) + and not (hasattr(expression, "alias") and expression.alias) # type: ignore + ): + alias_name = expression.alias_or_name.identifier(False) # type: ignore + if expression.alias_or_name.name in node_columns: # type: ignore # pragma: no cover + alias_name = node.name + SEPARATOR + expression.alias_or_name.name # type: ignore + expression = expression.copy() + expression.set_semantic_entity(alias_name) # type: ignore + if not preaggregate: + expression.set_alias(ast.Name(amenable_name(alias_name))) + projection.append(expression) + else: + expression = expression.copy() + if isinstance( + expression, + ast.Aliasable, + ) and not isinstance( # pragma: no cover + expression, + ast.Wildcard, + ): + column_ref = expression.alias_or_name.identifier() + if column_ref in node_columns: # type: ignore + alias_name = f"{node.name}{SEPARATOR}{column_ref}" # type: ignore # pragma: no cover + expression.set_semantic_entity(alias_name) # pragma: no cover + else: + expression.set_semantic_entity(from_amenable_name(column_ref)) + projection.append(expression) # type: ignore + built_ast.select.projection = projection + + if built_ast.select.where: + for col in built_ast.select.where.find_all(ast.Column): # pragma: no cover + if hasattr(col, "alias"): # pragma: no cover + col.alias = None + + if built_ast.select.group_by: + for i in range( # pragma: no cover + len(built_ast.select.group_by), + ): + if hasattr(built_ast.select.group_by[i], "alias"): # pragma: no cover + built_ast.select.group_by[i] = ast.Column( + name=built_ast.select.group_by[i].name, # type: ignore + _type=built_ast.select.group_by[i].type, # type: ignore + _table=built_ast.select.group_by[i]._table, # type: ignore + ) + built_ast.select.group_by[i].alias = None + return built_ast + + +def group_metrics_by_parent( + metric_nodes: List[Node], +) -> DefaultDict[Node, List[NodeRevision]]: + """ + Group metrics by their parent node + """ + common_parents = collections.defaultdict(list) + for metric_node in metric_nodes: + immediate_parent = metric_node.current.parents[0] + common_parents[immediate_parent].append(metric_node.current) + return common_parents + + +async def validate_shared_dimensions( + session: AsyncSession, + metric_nodes: List[Node], + dimensions: List[str], +): + """ + Determine if dimensions are shared. + """ + shared_dimensions = [ + dim.name for dim in await get_shared_dimensions(session, metric_nodes) + ] + for dimension_attribute in dimensions: + if dimension_attribute not in shared_dimensions: + message = ( + f"The dimension attribute `{dimension_attribute}` is not " + "available on every metric and thus cannot be included." + ) + raise DJInvalidInputException( + message, + errors=[DJError(code=ErrorCode.INVALID_DIMENSION, message=message)], + ) + + +async def build_metric_nodes( + session: AsyncSession, + metric_nodes: List[Node], + filters: List[str], + dimensions: List[str], + orderby: List[str], + limit: Optional[int] = None, + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + build_criteria: Optional[BuildCriteria] = None, + access_control: Optional[access.AccessControlStore] = None, + ignore_errors: bool = True, +): + """ + Build a single query for all metrics in the list, including the specified + group bys (dimensions) and filters. The metric nodes should share the same set + of dimensional attributes. Then we can: + (a) Group metrics by their parent nodes. + (b) Build a query for each parent node with the dimensional attributes referenced joined in + (c) For all metrics that reference the parent node, insert the metric expression + into the parent node's select and build the parent query + (d) Set the rest of the parent query's attributes (filters, orderby, limit etc) + (e) Join together the transforms on the shared dimensions + (f) Select all the requested metrics and dimensions in the final SELECT + """ + from datajunction_server.construction.build_v2 import CubeQueryBuilder + + engine = ( + await get_engine(session, engine_name, engine_version) + if engine_name and engine_version + else None + ) + build_criteria = build_criteria or BuildCriteria( + dialect=engine.dialect if engine and engine.dialect else Dialect.SPARK, + ) + builder = await CubeQueryBuilder.create( + session, + metric_nodes, + use_materialized=False, + ) + builder = ( + builder.add_filters(filters) + .add_dimensions(dimensions) + .order_by(orderby) + .limit(limit) + .with_build_criteria(build_criteria) + .with_access_control(access_control) + ) + if ignore_errors: + builder = builder.ignore_errors() + return await builder.build() + + +def build_temp_select(temp_query: str): + """ + Builds an intermediate select ast used to build cube queries + """ + temp_select = parse(temp_query).select + + for col in temp_select.find_all(ast.Column): + dimension_column = col.identifier(False).replace( + SEPARATOR, + f"_{LOOKUP_CHARS.get(SEPARATOR)}_", + ) + col.name = ast.Name(dimension_column) + return temp_select + + +def build_materialized_cube_node( + selected_metrics: List[Column], + selected_dimensions: List[Column], + cube: NodeRevision, + filters: List[str] = None, + orderby: List[str] = None, + limit: Optional[int] = None, +) -> ast.Query: + """ + Build query for a materialized cube node + """ + combined_ast: ast.Query = ast.Query( + select=ast.Select(from_=ast.From(relations=[])), + ctes=[], + ) + materialization_config = cube.materializations[0] + cube_config = GenericCubeConfig.parse_obj(materialization_config.config) + + if materialization_config.name == "default": + # TODO: remove after we migrate old Druid materializations + selected_metric_keys = [ + col.name for col in selected_metrics + ] # pragma: no cover + else: + selected_metric_keys = [ + col.node_revision().name # type: ignore + for col in selected_metrics + ] + + # Assemble query for materialized cube based on the previously saved measures + # combiner expression for each metric + for metric_key in selected_metric_keys: + if ( + cube_config.measures and metric_key in cube_config.measures + ): # pragma: no cover + metric_measures = cube_config.measures[metric_key] + measures_combiner_ast = parse(f"SELECT {metric_measures.combiner}") + measures_type_lookup = { + ( + measure.name + if materialization_config.name == "default" + else measure.field_name + ): measure.type + for measure in metric_measures.measures + } + for col in measures_combiner_ast.find_all(ast.Column): + col.add_type( + ColumnType( + measures_type_lookup[col.alias_or_name.name], # type: ignore + ), + ) + combined_ast.select.projection.extend( + [ + proj.set_alias(ast.Name(amenable_name(metric_key))) + for proj in measures_combiner_ast.select.projection + ], + ) + else: + # This is the materialized metrics table case. We choose SUM for now, + # since there shouldn't be any other possible aggregation types on a + # metric (maybe MAX or MIN in some special cases). + combined_ast.select.projection.append( + ast.Function( # pragma: no cover + name=ast.Name("SUM"), + args=[ast.Column(name=ast.Name(amenable_name(metric_key)))], + ), + ) + + # Add in selected dimension attributes to the query + for selected_dim in selected_dimensions: + dimension_column = ast.Column( + name=ast.Name( + ( + selected_dim.node_revision().name # type: ignore + + SEPARATOR + + selected_dim.name + ).replace(SEPARATOR, f"_{LOOKUP_CHARS.get(SEPARATOR)}_"), + ), + ) + combined_ast.select.projection.append(dimension_column) + combined_ast.select.group_by.append(dimension_column) + + # Add in filters to the query + filter_asts = [] + for filter_ in filters: # type: ignore + temp_select = build_temp_select( + f"select * where {filter_}", + ) + filter_asts.append(temp_select.where) + + if filter_asts: # pragma: no cover + combined_ast.select.where = ast.BinaryOp.And(*filter_asts) + + # Add orderby + if orderby: # pragma: no cover + temp_select = build_temp_select( + f"select * order by {','.join(orderby)}", + ) + combined_ast.select.organization = temp_select.organization + + # Add limit + if limit: # pragma: no cover + combined_ast.select.limit = ast.Number(value=limit) + + # Set up FROM clause + combined_ast.select.from_.relations.append( # type: ignore + ast.Relation(primary=ast.Table(ast.Name(cube.availability.table))), # type: ignore + ) + return combined_ast + + +def metrics_to_measures( + metric_nodes: list[Node], +) -> Tuple[DefaultDict[str, Set[str]], dict[str, tuple[list[Measure], ast.Query]]]: + """ + For the given metric nodes, returns a mapping between the metrics' referenced parent nodes + and the list of necessary measures to extract from the parent node. + The structure is: + { + "parent_node_name1": ["measure_columnA", "measure_columnB"], + "parent_node_name2": ["measure_columnX"], + } + """ + metric_to_measures: dict[str, tuple[list[Measure], ast.Query]] = {} + parents_to_measures = collections.defaultdict(set) + + def _process_metric(metric_query: str): + extractor = MeasureExtractor.from_query_string(metric_query) + metric_ast = parse(metric_query) + return extractor.extract(), list(metric_ast.find_all(ast.Column)) + + max_workers = min(max(1, len(metric_nodes)), os.cpu_count()) # type: ignore + with ThreadPoolExecutor(max_workers=max_workers) as executor: + extracted_measures = executor.map( + _process_metric, + [metric_node.current.query for metric_node in metric_nodes], + ) + + for metric_node, (measures, columns) in zip(metric_nodes, extracted_measures): + metric_to_measures[metric_node.name] = measures + for col in columns: + parents_to_measures[metric_node.current.parents[0].name].add( # type: ignore + col.alias_or_name.name, + ) + return parents_to_measures, metric_to_measures diff --git a/datajunction-server/datajunction_server/construction/build_v2.py b/datajunction-server/datajunction_server/construction/build_v2.py new file mode 100644 index 000000000..7338796a4 --- /dev/null +++ b/datajunction-server/datajunction_server/construction/build_v2.py @@ -0,0 +1,1795 @@ +"""Building node SQL functions""" + +import collections +import logging +import re +from dataclasses import dataclass +from functools import cached_property +from typing import Any, DefaultDict, Dict, List, Optional, Tuple, Union, cast + +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.construction.utils import to_namespaced_name +from datajunction_server.database import Engine +from datajunction_server.database.dimensionlink import DimensionLink +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.user import User +from datajunction_server.errors import ( + DJException, + DJQueryBuildError, + DJQueryBuildException, + ErrorCode, +) +from datajunction_server.internal.engines import get_engine +from datajunction_server.models import access +from datajunction_server.models.column import SemanticType +from datajunction_server.models.cube_materialization import Aggregability, Measure +from datajunction_server.models.engine import Dialect +from datajunction_server.models.node import BuildCriteria +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.sql import GeneratedSQL +from datajunction_server.naming import amenable_name, from_amenable_name +from datajunction_server.sql.parsing.ast import CompileContext +from datajunction_server.sql.parsing.backends.antlr4 import ast, cached_parse, parse +from datajunction_server.utils import SEPARATOR, refresh_if_needed + +logger = logging.getLogger(__name__) + + +@dataclass +class FullColumnName: + """ + A fully qualified column name with the node name and the column. + """ + + name: str + + @cached_property + def node_name(self) -> str: + """ + Gets the node name part of the full column name. + """ + return SEPARATOR.join(self.name.split(SEPARATOR)[:-1]) + + @cached_property + def full_column_name(self) -> str: + """ + Gets the column name part of the full column name. + """ + return self.name.split(SEPARATOR)[-1] + + @cached_property + def column_name(self) -> str: + """ + Gets the column name part of the full column name. + """ + if self.role: + return self.full_column_name.replace(f"[{self.role}]", "") + return self.full_column_name + + @cached_property + def role(self) -> Optional[str]: + """ + Gets the column name part of the full column name. + """ + regex = r"\[([A-Za-z0-9_]*)\]" + match = re.search(regex, self.full_column_name) + if match: + return match.group(1) + return None + + +@dataclass +class DimensionJoin: + """ + Info on a dimension join + """ + + join_path: List[DimensionLink] + requested_dimensions: List[str] + node_query: Optional[ast.Query] = None + + +async def get_measures_query( + session: AsyncSession, + metrics: List[str], + dimensions: List[str], + filters: List[str], + orderby: List[str] = None, + engine_name: Optional[str] = None, + engine_version: Optional[str] = None, + current_user: Optional[User] = None, + validate_access: access.ValidateAccessFn = None, + include_all_columns: bool = False, + sql_transpilation_library: Optional[str] = None, + use_materialized: bool = True, + preagg_requested: bool = False, +) -> List[GeneratedSQL]: + """ + Builds the measures SQL for a set of metrics with dimensions and filters. + + Measures queries are generated at the grain of each of the metrics' upstream nodes. + For example, if some of your metrics are aggregations on measures in parent node A + and others are aggregations on measures in parent node B, this function will return a + dictionary that maps A to the measures query for A, and B to the measures query for B. + """ + from datajunction_server.api.helpers import ( + assemble_column_metadata, + check_dimension_attributes_exist, + check_metrics_exist, + ) + from datajunction_server.construction.build import ( + group_metrics_by_parent, + metrics_to_measures, + rename_columns, + ) + + engine = ( + await get_engine(session, engine_name, engine_version) + if engine_name and engine_version + else None + ) + build_criteria = BuildCriteria( + dialect=engine.dialect if engine and engine.dialect else Dialect.SPARK, + ) + access_control = ( + access.AccessControlStore( + validate_access=validate_access, + user=current_user, + base_verb=access.ResourceRequestVerb.READ, + ) + if validate_access + else None + ) + + if not filters: + filters = [] + + metrics_sorting_order = {val: idx for idx, val in enumerate(metrics)} + metric_nodes = await check_metrics_exist(session, metrics) + await check_dimension_attributes_exist(session, dimensions) + + common_parents = group_metrics_by_parent(metric_nodes) + parents_to_measures, metrics2measures = metrics_to_measures(metric_nodes) + + column_name_regex = r"([A-Za-z0-9_\.]+)(\[[A-Za-z0-9_]+\])?" + matcher = re.compile(column_name_regex) + dimensions_without_roles = [matcher.findall(dim)[0][0] for dim in dimensions] + + measures_queries = [] + context = CompileContext(session=session, exception=DJException()) + for parent_node, children in common_parents.items(): # type: ignore + children = sorted(children, key=lambda x: metrics_sorting_order.get(x.name, 0)) + + # Determine whether to pre-aggregate to the requested dimensions so that subsequent + # queries are more efficient by checking the measures on the requested metrics + preaggregate = preagg_requested and all( + len(metrics2measures[metric.name][0]) > 0 + and all( + measure.rule.type == Aggregability.FULL + for measure in metrics2measures[metric.name][0] + ) + for metric in children + ) + + measure_columns, dimensional_columns = [], [] + await refresh_if_needed(session, parent_node, ["current"]) + query_builder = await QueryBuilder.create( + session, + parent_node.current, + use_materialized=use_materialized, + ) + parent_ast = await ( + query_builder.ignore_errors() + .with_access_control(access_control) + .with_build_criteria(build_criteria) + .add_dimensions(dimensions) + .add_filters(filters) + .order_by(orderby) + .build() + ) + + # Select only columns that were one of the necessary measures + if not include_all_columns: + parent_ast.select.projection = [ + expr + for expr in parent_ast.select.projection + if from_amenable_name(expr.alias_or_name.identifier(False)).split( # type: ignore + SEPARATOR, + )[-1] + in parents_to_measures[parent_node.name] + or from_amenable_name(expr.alias_or_name.identifier(False)) # type: ignore + in dimensions_without_roles + ] + await refresh_if_needed(session, parent_node.current, ["columns"]) + parent_ast = rename_columns(parent_ast, parent_node.current, preaggregate) + + # Sort the selected columns into dimension vs measure columns and + # generate identifiers for them + for expr in parent_ast.select.projection: + column_identifier = expr.alias_or_name.identifier(False) # type: ignore + if from_amenable_name(column_identifier) in dimensions_without_roles: + dimensional_columns.append(expr) + expr.set_semantic_type(SemanticType.DIMENSION) # type: ignore + else: + measure_columns.append(expr) + expr.set_semantic_type(SemanticType.MEASURE) # type: ignore + await parent_ast.compile(context) + dependencies, _ = await parent_ast.extract_dependencies( + CompileContext(session, DJException()), + ) + + final_query = ( + build_preaggregate_query( + parent_ast, + parent_node, + dimensional_columns, + children, + metrics2measures, + ) + if preaggregate + else parent_ast + ) + + # Build translated SQL object + columns_metadata = [ + assemble_column_metadata( # pragma: no cover + cast(ast.Column, col), + ) + for col in final_query.select.projection + ] + measures_queries.append( + GeneratedSQL( + node=parent_node.current, + sql_transpilation_library=sql_transpilation_library, + sql=str(final_query), + columns=columns_metadata, + dialect=build_criteria.dialect, + upstream_tables=[ + f"{dep.catalog.name}.{dep.schema_}.{dep.table}" + for dep in dependencies + if dep.type == NodeType.SOURCE + ], + grain=( + [ + col.name + for col in columns_metadata + if col.semantic_type == SemanticType.DIMENSION + ] + if preaggregate + else [pk_col.name for pk_col in parent_node.current.primary_key()] + ), + errors=query_builder.errors, + metrics={ + metric.name: ( + metrics2measures[metric.name][0], + str(metrics2measures[metric.name][1]).replace("\n", "") + if preaggregate + else metric.query, + ) + for metric in children + }, + ), + ) + return measures_queries + + +def build_preaggregate_query( + parent_ast: ast.Query, + parent_node: Node, + dimensional_columns: list[ast.Column], + children: list[NodeRevision], + metrics2measures: dict[str, tuple[list[Measure], ast.Query]], +): + """ + Builds a measures query preaggregated to the chosen dimensions. + """ + existing_ctes = parent_ast.ctes + parent_ast.ctes = [] + built_parent_ref = parent_node.name + "_built" + parent_node_cte = parent_ast.to_cte(ast.Name(amenable_name(built_parent_ref))) + from_table = ast.Table(ast.Name(amenable_name(built_parent_ref))) + final_query = ast.Query( + ctes=existing_ctes + [parent_node_cte], + select=ast.Select( + projection=[ + ast.Column.from_existing(col, table=from_table) + for col in parent_ast.select.projection + if col.semantic_type == SemanticType.DIMENSION # type: ignore + ], + from_=ast.From(relations=[ast.Relation(primary=from_table)]), + group_by=[ + ast.Column(dim.alias_or_name, _table=from_table) + for dim in dimensional_columns + ], + ), + ) + + added_measures = set() + for metric in children: + for measure in metrics2measures[metric.name][0]: + if measure.name in added_measures: + continue + added_measures.add(measure.name) + temp_select = cached_parse( + f"SELECT {measure.aggregation}({measure.expression}) AS {measure.name}", + ).select + for col in temp_select.find_all(ast.Column): + # Realias based on canonical dimension name if needed + if col.alias_or_name.name not in parent_ast.select.column_mapping: + new_alias = amenable_name( + parent_node.name + SEPARATOR + col.alias_or_name.name, + ) + if new_alias in parent_ast.select.column_mapping: + col.name.name = new_alias + if ( # pragma: no cover + col.alias_or_name.name in parent_ast.select.column_mapping + ): + col.add_type( + parent_ast.select.column_mapping.get( # type: ignore + col.alias_or_name.name, + ).type, + ) + for proj in temp_select.projection: + proj.set_semantic_entity(parent_node.name + SEPARATOR + measure.name) # type: ignore + proj.set_semantic_type(SemanticType.MEASURE) # type: ignore + final_query.select.projection.extend(temp_select.projection) + return final_query + + +class QueryBuilder: + """ + This class allows users to configure building node SQL by incrementally building out + the build configuration, including adding filters, dimensions, ordering, and limit + clauses. The builder then handles the management of CTEs, dimension joins, and error + validation, allowing for dynamic node query generation based on runtime conditions. + """ + + def __init__( + self, + session: AsyncSession, + node_revision: NodeRevision, + use_materialized: bool = True, + ): + self.session = session + self.node_revision = node_revision + self.use_materialized = use_materialized + + self._filters: List[str] = [] + self._required_dimensions: List[str] = [ + required.name for required in self.node_revision.required_dimensions + ] + self._dimensions: List[str] = [] + self._orderby: List[str] = [] + self._limit: Optional[int] = None + self._build_criteria: Optional[BuildCriteria] = self.get_default_criteria() + self._access_control: Optional[access.AccessControlStore] = None + self._ignore_errors: bool = False + + # The following attributes will be modified as the query gets built. + # -- + # Track node query CTEs as they get built + self.cte_mapping: Dict[str, ast.Query] = {} # Maps node name to its CTE + # Keep a list of build errors + self.errors: List[DJQueryBuildError] = [] + # The final built query AST + self.final_ast: Optional[ast.Query] = None + + @classmethod + async def create( + cls, + session: AsyncSession, + node_revision: NodeRevision, + use_materialized: bool = True, + ) -> "QueryBuilder": + """ + Create a QueryBuilder instance for the node revision. + """ + await refresh_if_needed( + session, + node_revision, + ["required_dimensions", "dimension_links"], + ) + instance = cls(session, node_revision, use_materialized=use_materialized) + return instance + + def ignore_errors(self): + """Do not raise on errors in query build.""" + self._ignore_errors = True + return self + + def raise_errors(self): + """Raise on errors in query build.""" + self._ignore_errors = False + return self + + def filter_by(self, filter_: str): + """Add filter to the query builder.""" + if filter_ not in self._filters: + self._filters.append(filter_) + return self + + def add_filters(self, filters: Optional[List[str]] = None): + """Add filters to the query builder.""" + for filter_ in filters or []: + self.filter_by(filter_) + return self + + def add_dimension(self, dimension: str): + """Add dimension to the query builder.""" + if ( + dimension not in self._dimensions + and dimension not in self._required_dimensions + ): + self._dimensions.append(dimension) + return self + + def add_dimensions(self, dimensions: Optional[List[str]] = None): + """Add dimensions to the query builder.""" + for dimension in dimensions or []: + self.add_dimension(dimension) + return self + + def order_by(self, orderby: Optional[Union[str, List[str]]] = None): + """Set order by for the query builder.""" + if isinstance(orderby, str): + if orderby not in self._orderby: + self._orderby.append(orderby) + else: + for order in orderby or []: + if order not in self._orderby: # pragma: no cover + self._orderby.append(order) + return self + + def limit(self, limit: Optional[int] = None): + """Set limit for the query builder.""" + if limit: # pragma: no cover + self._limit = limit + return self + + def with_build_criteria(self, build_criteria: Optional[BuildCriteria] = None): + """Set build criteria for the query builder.""" + if build_criteria: # pragma: no cover + self._build_criteria = build_criteria + return self + + def with_access_control( + self, + access_control: Optional[access.AccessControlStore] = None, + ): + """ + Set access control for the query builder. + """ + if access_control: # pragma: no cover + access_control.add_request_by_node(self.node_revision) + self._access_control = access_control + return self + + @property + def dimensions(self) -> List[str]: + """All dimensions""" + return self._dimensions + self._required_dimensions + + @property + def filters(self) -> List[str]: + """All filters""" + return self._filters + + @property + def filter_asts(self) -> List[ast.Expression]: + """ + Returns a list of filter expressions rendered as ASTs + """ + return [filter_ast for filter_ast in to_filter_asts(self.filters) if filter_ast] + + @property + def include_dimensions_in_groupby(self) -> bool: + """ + Whether to include the requested dimensions in the query's GROUP BY clause. + Defaults to true for metrics. + """ + return self.node_revision.type == NodeType.METRIC + + @cached_property + def physical_table(self) -> Optional[ast.Table]: + """ + A physical table for the node, if one exists + """ + return get_table_for_node( + self.node_revision, + build_criteria=self._build_criteria, + ) + + @property + def context(self) -> Dict[str, Any]: + """ + Debug context + """ + return { + "node_revision": self.node_revision.name, + "filters": self._filters, + "required_dimensions": self._required_dimensions, + "dimensions": self._dimensions, + "orderby": self._orderby, + "limit": self._limit, + "ignore_errors": self._ignore_errors, + "build_criteria": self._build_criteria, + } + + async def build(self) -> ast.Query: + """ + Builds the node SQL with the requested set of dimensions, filter expressions, + order by, and limit clauses. + + Build Strategy + --------------- + 1. Recursively turn node references into query ASTs + apply any filters that can + be pushed down. If the node query has CTEs, unwind them into subqueries. + 2. Initialize the final query with the node's query AST added to it as a CTE. + 3. For any dimensions or filters requested for the node, determine if a join is + needed to bring in the dimension or filter. Keep track of all the necessary dimension + joins in a dict that maps dimension nodes to join paths. + 4. For each of the necessary dimension joins, build the dimension node's query in the + same manner as above, recursively replacing any node references and pushing down requested + filters where possible. + 5. Add each dimension node's query AST to the final query as a CTE. + 6. Build the final query using the various CTEs. This does all the joins between the node + query AST and the dimension nodes' ASTs using the join logic from the dimension links. + 7. Add all requested dimensions to the final select. + 8. Add order by and limit to the final select (TODO) + """ + await refresh_if_needed( + self.session, + self.node_revision, + ["availability", "columns", "query_ast"], + ) + if self.node_revision.query_ast: + node_ast = self.node_revision.query_ast # pragma: no cover + else: + node_ast = ( + await compile_node_ast(self.session, self.node_revision) + if not self.physical_table + else self.create_query_from_physical_table(self.physical_table) + ) + + if self.physical_table and not self._filters and not self.dimensions: + self.final_ast = node_ast + else: + node_alias, node_ast = await self.build_current_node_ast(node_ast) + ctx = CompileContext(self.session, DJException()) + await node_ast.compile(ctx) + self.final_ast = self.initialize_final_query_ast(node_ast, node_alias) + await self.build_dimension_node_joins(node_ast, node_alias) + self.set_dimension_aliases() + + self.final_ast.select.limit = self._limit # type: ignore + if self._orderby: + if order := self.build_order_bys(): + self.final_ast.select.organization = ast.Organization( # type: ignore + order=order, + ) + + # Error validation + self.validate_access() + if self.errors and not self._ignore_errors: + raise DJQueryBuildException(errors=self.errors) + return self.final_ast # type: ignore + + def build_order_bys(self): + """ + Build the ORDER BY clause from the provided order expressions + """ + temp_orderbys = cached_parse( + f"SELECT 1 ORDER BY {','.join(self._orderby)}", + ).select.organization.order + valid_sort_items = [ + sortitem + for sortitem in temp_orderbys + if amenable_name(sortitem.expr.identifier()) + in self.final_ast.select.column_mapping + ] + if len(valid_sort_items) < len(temp_orderbys): + self.errors.append( + DJQueryBuildError( + code=ErrorCode.INVALID_ORDER_BY, + message=f"{self._orderby} is not a valid ORDER BY request", + debug=self.context, + ), + ) + return [ + ast.SortItem( + expr=self.final_ast.select.column_mapping.get( + amenable_name(sortitem.expr.identifier()), + ) + .copy() + .set_alias(None), + asc=sortitem.asc, + nulls=sortitem.nulls, + ) + for sortitem in valid_sort_items + ] + + def get_default_criteria( + self, + engine: Optional[Engine] = None, + ) -> BuildCriteria: + """ + Get the default build criteria for a node. + Set the dialect by using the provided engine, if any. If no engine is specified, + set the dialect by finding available engines for this node, or default to Spark + """ + dialect = ( + engine.dialect + if engine + else ( + self.node_revision.catalog.engines[0].dialect + if self.node_revision.catalog + and self.node_revision.catalog.engines + and self.node_revision.catalog.engines[0].dialect + else Dialect.SPARK + ) + ) + return BuildCriteria( + dialect=dialect, + target_node_name=self.node_revision.name, + ) + + async def build_current_node_ast(self, node_ast): + """ + Build the node AST into a CTE + """ + ctx = CompileContext(self.session, DJException()) + await node_ast.compile(ctx) + self.errors.extend(ctx.exception.errors) + node_alias = ast.Name(amenable_name(self.node_revision.name)) + return node_alias, await build_ast( + self.session, + self.node_revision, + node_ast, + filters=self._filters, + build_criteria=self._build_criteria, + ctes_mapping=self.cte_mapping, + use_materialized=self.use_materialized, + ) + + def initialize_final_query_ast(self, node_ast, node_alias): + """ + Initialize the final query AST structure + """ + node_ctes = remove_duplicates( # pragma: no cover + node_ast.ctes, + lambda cte: cte.alias_or_name.identifier(), + ) + return ast.Query( + select=ast.Select( + projection=[ + ast.Column( + ast.Name(col.alias_or_name.name), # type: ignore + _table=node_ast, + _type=col.type, # type: ignore + ) + for col in node_ast.select.projection + ], + from_=ast.From(relations=[ast.Relation(node_alias)]), # type: ignore + ), + ctes=[*node_ctes, node_ast], + ) + + async def build_dimension_node_joins(self, node_ast, node_alias): + """ + Builds the dimension joins and adding them to the CTEs + """ + # Add node ast to CTE tracker + node_ast.ctes = [] + node_ast = node_ast.to_cte(node_alias) + self.cte_mapping[self.node_revision.name] = node_ast + + # Find all dimension node joins necessary for the requested dimensions and filters + dimension_node_joins = await self.find_dimension_node_joins() + for _, dimension_join in dimension_node_joins.items(): + join_path = dimension_join.join_path + requested_dimensions = list( + dict.fromkeys(dimension_join.requested_dimensions), + ) + + for link in join_path: + link = cast(DimensionLink, link) + if all( + dim in link.foreign_keys_reversed for dim in requested_dimensions + ): # pragma: no cover + continue # pragma: no cover + + if link.dimension.name in self.cte_mapping: + dimension_join.node_query = self.cte_mapping[link.dimension.name] + continue + + dimension_node_query = await build_dimension_node_query( + self.session, + self._build_criteria, + link, + self._filters, + self.cte_mapping, + use_materialized=self.use_materialized, + ) + dimension_join.node_query = convert_to_cte( + dimension_node_query, + self.final_ast, + link.dimension.name, + ) + # Add it to the list of CTEs + self.cte_mapping[link.dimension.name] = dimension_join.node_query # type: ignore + self.final_ast.ctes.append(dimension_join.node_query) # type: ignore + + # Build the join statement + join_ast = build_join_for_link( + link, + self.cte_mapping, + dimension_node_query, + ) + self.final_ast.select.from_.relations[-1].extensions.append(join_ast) # type: ignore + + # Add the requested dimensions to the final SELECT + if join_path: # pragma: no cover + dimensions_columns, errors = build_requested_dimensions_columns( + requested_dimensions, + join_path[-1], + dimension_node_joins, + ) + self.final_ast.select.projection.extend(dimensions_columns) + self.errors.extend(errors) + + def create_query_from_physical_table(self, physical_table) -> ast.Query: + """ + Initial scaffolding for a query from a physical table. + """ + return ast.Query( + select=ast.Select( + projection=physical_table.columns, # type: ignore + from_=ast.From(relations=[ast.Relation(physical_table)]), + ), + ) + + def set_dimension_aliases(self): + """ + Mark any remaining requested dimensions that don't need a join with + their canonical dimension names + """ + for dim_name in self.dimensions: + column_name = get_column_from_canonical_dimension( + dim_name, + self.node_revision, + ) + node_col = ( + self.final_ast.select.column_mapping.get(column_name) + if column_name + else None + ) + # Realias based on canonical dimension name + new_alias = amenable_name(dim_name) + if node_col and new_alias not in self.final_ast.select.column_mapping: + node_col.set_alias(ast.Name(amenable_name(dim_name))) + + async def add_request_by_node_name(self, node_name): + """Add a node request to the access control validator.""" + if self._access_control: # pragma: no cover + await self._access_control.add_request_by_node_name( # pragma: no cover + self.session, + node_name, + ) + + def validate_access(self): + """Validates access""" + if self._access_control: + self._access_control.validate_and_raise() + + async def find_dimension_node_joins( + self, + ) -> Dict[str, DimensionJoin]: + """ + Returns a list of dimension node joins that are necessary based on + the requested dimensions and filters + """ + dimension_node_joins = {} + + # Combine necessary dimensions from filters and requested dimensions + necessary_dimensions = self.dimensions.copy() + for filter_ast in self.filter_asts: + for filter_dim in filter_ast.find_all(ast.Column): + necessary_dimensions.append(filter_dim.identifier()) + + # For dimensions that need a join, build metadata on the join path + for dim in necessary_dimensions: + dimension_attr = FullColumnName(dim) + dim_node = dimension_attr.node_name + if dim_node == self.node_revision.name: + continue + await self.add_request_by_node_name(dim_node) + if dim_node not in dimension_node_joins: + join_path = await dimension_join_path( + self.session, + self.node_revision, + dimension_attr.name, + ) + if not join_path and join_path != []: + self.errors.append( + DJQueryBuildError( + code=ErrorCode.INVALID_DIMENSION_JOIN, + message=( + f"This dimension attribute cannot be joined in: {dim}. " + f"Please make sure that {dimension_attr.node_name} is " + f"linked to {self.node_revision.name}" + ), + context=str(self), + ), + ) + if join_path and await needs_dimension_join( + self.session, + dimension_attr.name, + join_path, + ): + dimension_node_joins[dim_node] = DimensionJoin( + join_path=join_path, # type: ignore + requested_dimensions=[dimension_attr.name], + ) + else: + if dim not in dimension_node_joins[dim_node].requested_dimensions: + dimension_node_joins[dim_node].requested_dimensions.append(dim) + return dimension_node_joins + + +class CubeQueryBuilder: + """ + This class allows users to configure building cube SQL (retrieving SQL for multiple + metrics + dimensions) through settings like adding filters, dimensions, ordering, and limit + clauses. The builder then handles the management of CTEs, dimension joins, and error + validation, allowing for dynamic node query generation based on runtime conditions. + """ + + def __init__( + self, + session: AsyncSession, + metric_nodes: List[Node], + use_materialized: bool = True, + ): + self.session = session + self.metric_nodes = metric_nodes + self.use_materialized = use_materialized + + self._filters: List[str] = [] + self._required_dimensions: List[str] = [ + required.name + for metric_node in self.metric_nodes + for required in metric_node.current.required_dimensions + ] + self._dimensions: List[str] = [] + self._orderby: List[str] = [] + self._limit: Optional[int] = None + self._build_criteria: Optional[BuildCriteria] = self.get_default_criteria() + self._access_control: Optional[access.AccessControlStore] = None + self._ignore_errors: bool = False + + # The following attributes will be modified as the query gets built. + # -- + # Track node query CTEs as they get built + self.cte_mapping: Dict[str, ast.Query] = {} # Maps node name to its CTE + # Keep a list of build errors + self.errors: List[DJQueryBuildError] = [] + # The final built query AST + self.final_ast: Optional[ast.Query] = None + + def get_default_criteria( + self, + engine: Optional[Engine] = None, + ) -> BuildCriteria: + """ + Get the default build criteria for a node. + Set the dialect by using the provided engine, if any. If no engine is specified, + set the dialect by finding available engines for this node, or default to Spark + """ + return BuildCriteria( + dialect=engine.dialect if engine and engine.dialect else Dialect.SPARK, + ) + + @classmethod + async def create( + cls, + session: AsyncSession, + metric_nodes: List[Node], + use_materialized: bool = True, + ) -> "CubeQueryBuilder": + """ + Create a QueryBuilder instance for the node revision. + """ + for node in metric_nodes: + await refresh_if_needed(session, node, ["current"]) + await refresh_if_needed(session, node.current, ["required_dimensions"]) + + instance = cls(session, metric_nodes, use_materialized=use_materialized) + return instance + + def ignore_errors(self): + """Do not raise on errors in query build.""" + self._ignore_errors = True + return self + + def raise_errors(self): + """Raise on errors in query build.""" + self._ignore_errors = False # pragma: no cover + return self # pragma: no cover + + def filter_by(self, filter_: str): + """Add filter to the query builder.""" + if filter_ not in self._filters: # pragma: no cover + self._filters.append(filter_) + return self + + def add_filters(self, filters: Optional[List[str]] = None): + """Add filters to the query builder.""" + for filter_ in filters or []: + self.filter_by(filter_) + return self + + def add_dimension(self, dimension: str): + """Add dimension to the query builder.""" + if ( # pragma: no cover + dimension not in self._dimensions + and dimension not in self._required_dimensions + ): + self._dimensions.append(dimension) + return self + + def add_dimensions(self, dimensions: Optional[List[str]] = None): + """Add dimensions to the query builder.""" + for dimension in dimensions or []: + self.add_dimension(dimension) + return self + + def order_by(self, orderby: Optional[Union[str, List[str]]] = None): + """Set order by for the query builder.""" + if isinstance(orderby, str): + if orderby not in self._orderby: # pragma: no cover + self._orderby.append(orderby) # pragma: no cover + else: + for order in orderby or []: + if order not in self._orderby: # pragma: no cover + self._orderby.append(order) + return self + + def limit(self, limit: Optional[int] = None): + """Set limit for the query builder.""" + if limit: # pragma: no cover + self._limit = limit + return self + + def with_build_criteria(self, build_criteria: Optional[BuildCriteria] = None): + """Set build criteria for the query builder.""" + if build_criteria: # pragma: no cover + self._build_criteria = build_criteria + return self + + def with_access_control( + self, + access_control: Optional[access.AccessControlStore] = None, + ): + """ + Set access control for the query builder. + """ + if access_control: # pragma: no cover + access_control.add_request_by_nodes(self.metric_nodes) + self._access_control = access_control + return self + + @property + def dimensions(self) -> List[str]: + """All dimensions""" + return self._dimensions # TO DO: add self._required_dimensions + + @property + def filters(self) -> List[str]: + """All filters""" + return self._filters + + async def build(self) -> ast.Query: + """ + Builds SQL for multiple metrics with the requested set of dimensions, + filter expressions, order by, and limit clauses. + """ + measures_queries = await self.build_measures_queries() + + # Join together the transforms on the shared dimensions and select all + # requested metrics and dimensions in the final select projection + parent_ctes, metric_ctes = self.extract_ctes(measures_queries) + initial_cte = metric_ctes[0] + self.final_ast = ast.Query( + ctes=parent_ctes + metric_ctes, + select=ast.Select( + projection=[ + ast.Column( + name=ast.Name(proj.alias, namespace=initial_cte.alias), # type: ignore + _type=proj.type, # type: ignore + semantic_entity=proj.semantic_entity, # type: ignore + semantic_type=proj.semantic_type, # type: ignore + ) + for proj in initial_cte.select.projection + ], + from_=ast.From( + relations=[ast.Relation(primary=ast.Table(initial_cte.alias))], # type: ignore + ), + ), + ) + # Add metrics + for metric_cte in metric_ctes[1:]: + self.final_ast.select.projection.extend( + [ + ast.Column( + name=ast.Name(proj.alias, namespace=metric_cte.alias), # type: ignore + _type=proj.type, # type: ignore + semantic_entity=proj.semantic_entity, # type: ignore + semantic_type=proj.semantic_type, # type: ignore + ) + for proj in metric_cte.select.projection + if from_amenable_name(proj.alias_or_name.identifier()) # type: ignore + not in self.dimensions + ], + ) + join_on = [ + ast.BinaryOp( + op=ast.BinaryOpKind.Eq, + left=ast.Column( + name=ast.Name(proj.alias, namespace=initial_cte.alias), # type: ignore + _type=proj.type, # type: ignore + ), + right=ast.Column( + name=ast.Name(proj.alias, namespace=metric_cte.alias), # type: ignore + _type=proj.type, # type: ignore + ), + ) + for proj in metric_cte.select.projection # type: ignore + if from_amenable_name(proj.alias_or_name.identifier()) # type: ignore + in self.dimensions + ] + self.final_ast.select.from_.relations[0].extensions.append( # type: ignore + ast.Join( + join_type="full", + right=ast.Table(metric_cte.alias), # type: ignore + criteria=ast.JoinCriteria( + on=ast.BinaryOp.And(*join_on), + ), + ), + ) + + if self._orderby: + self.final_ast.select.organization = self.build_orderby() + + if self._limit: + self.final_ast.select.limit = ast.Number(value=self._limit) + + # Error validation + self.validate_access() + if self.errors and not self._ignore_errors: + raise DJQueryBuildException(errors=self.errors) # pragma: no cover + return self.final_ast + + def validate_access(self): + """Validates access""" + if self._access_control: + self._access_control.validate_and_raise() + + async def build_measures_queries(self): + """ + Build the metrics' queries grouped by parent + """ + from datajunction_server.construction.build import ( + group_metrics_by_parent, + ) + + common_parents = group_metrics_by_parent(self.metric_nodes) + measures_queries = {} + for parent_node, metrics in common_parents.items(): # type: ignore + await refresh_if_needed(self.session, parent_node, ["current"]) + query_builder = await QueryBuilder.create(self.session, parent_node.current) + if self._ignore_errors: + query_builder = query_builder.ignore_errors() + parent_ast = await ( + query_builder.with_access_control(self._access_control) + .with_build_criteria(self._build_criteria) + .add_dimensions(self.dimensions) + .add_filters(self.filters) + .build() + ) + self.errors.extend(query_builder.errors) + + dimension_columns = [ + expr + for expr in parent_ast.select.projection + if from_amenable_name(expr.alias_or_name.identifier(False)) # type: ignore + in self.dimensions + ] + parent_ast.select.projection = dimension_columns + for col in dimension_columns: + group_by_col = col.copy() + group_by_col.alias = None + parent_ast.select.group_by.append(group_by_col) + + # Add metric aggregations to select + for metric_node in metrics: + metric_proj = await self.build_metric_agg(metric_node, parent_node) + parent_ast.select.projection.extend(metric_proj) + + await refresh_if_needed(self.session, parent_node.current, ["columns"]) + + # Generate semantic types for each + for expr in parent_ast.select.projection: + column_identifier = expr.alias_or_name.identifier(False) # type: ignore + semantic_entity = from_amenable_name(column_identifier) + if semantic_entity in self.dimensions: + expr.set_semantic_entity(semantic_entity) # type: ignore + expr.set_semantic_type(SemanticType.DIMENSION) # type: ignore + + ctx = CompileContext(self.session, DJException()) + await parent_ast.compile(ctx) + measures_queries[parent_node.name] = parent_ast + return measures_queries + + async def build_metric_agg(self, metric_node, parent_node): + """ + Build the metric's aggregate expression. + """ + if self._access_control: + self._access_control.add_request_by_node(metric_node) # type: ignore + metric_query_builder = await QueryBuilder.create(self.session, metric_node) + if self._ignore_errors: + metric_query_builder = ( # pragma: no cover + metric_query_builder.ignore_errors() + ) + metric_query = await ( + metric_query_builder.with_access_control(self._access_control) + .with_build_criteria(self._build_criteria) + .build() + ) + self.errors.extend(metric_query_builder.errors) + metric_query.ctes[-1].select.projection[0].set_semantic_entity( # type: ignore + f"{metric_node.name}.{amenable_name(metric_node.name)}", + ) + metric_query.ctes[-1].select.projection[0].set_alias( # type: ignore + ast.Name(amenable_name(metric_node.name)), + ) + metric_query.ctes[-1].select.projection[0].set_semantic_type( # type: ignore + SemanticType.METRIC, + ) + for col in metric_query.ctes[-1].select.find_all(ast.Column): + col._table = ast.Table( + name=ast.Name(name=amenable_name(parent_node.name)), + ) + return metric_query.ctes[-1].select.projection + + def extract_ctes(self, measures_queries) -> Tuple[List[ast.Query], List[ast.Query]]: + """ + Extracts the parent CTEs and the metric CTEs from the queries + """ + parent_ctes: List[ast.Query] = [] + metric_ctes: List[ast.Query] = [] + for parent_name, parent_query in measures_queries.items(): + existing_cte_aliases = { + cte.alias_or_name.identifier() for cte in parent_ctes + } + parent_ctes += [ + cte + for cte in parent_query.ctes + if cte.alias_or_name.identifier() not in existing_cte_aliases + ] + parent_query.ctes = [] + metric_ctes += [ + parent_query.to_cte(ast.Name(amenable_name(parent_name + "_metrics"))), + ] + return parent_ctes, metric_ctes + + def build_orderby(self): + """ + Creates an order by ast from the requested order bys + """ + temp_orderbys = cached_parse( # type: ignore + f"SELECT 1 ORDER BY {','.join(self._orderby)}", + ).select.organization.order + valid_sort_items = [ + sortitem + for sortitem in temp_orderbys + if amenable_name(sortitem.expr.identifier()) # type: ignore + in self.final_ast.select.column_mapping + ] + if len(valid_sort_items) < len(temp_orderbys): + self.errors.append( # pragma: no cover + DJQueryBuildError( + code=ErrorCode.INVALID_ORDER_BY, + message=f"{self._orderby} is not a valid ORDER BY request", + ), + ) + return ast.Organization( + order=[ + ast.SortItem( + expr=self.final_ast.select.column_mapping.get( # type: ignore + amenable_name(sortitem.expr.identifier()), # type: ignore + ) + .copy() + .set_alias(None), + asc=sortitem.asc, + nulls=sortitem.nulls, + ) + for sortitem in valid_sort_items + ], + ) + + +def get_column_from_canonical_dimension( + dimension_name: str, + node: NodeRevision, +) -> Optional[str]: + """ + Gets a column based on a dimension request on a node. + """ + column_name = None + dimension_attr = FullColumnName(dimension_name) + # Dimension requested was on node + if dimension_attr.node_name == node.name: + column_name = dimension_attr.column_name + + # Dimension requested has reference link on node + dimension_columns = { + (col.dimension.name, col.dimension_column): col.name + for col in node.columns + if col.dimension + } + key = (dimension_attr.node_name, dimension_attr.column_name) + if key in dimension_columns: + return dimension_columns[key] + + # Dimension referenced was foreign key of dimension link + for link in node.dimension_links: + foreign_key_column = link.foreign_keys_reversed.get(dimension_attr.name) + if foreign_key_column: + return FullColumnName(foreign_key_column).column_name + return column_name + + +def to_filter_asts(filters: Optional[List[str]] = None): + """ + Converts a list of filter expresisons to ASTs + """ + return [ + parse(f"select * where {filter_}").select.where for filter_ in filters or [] + ] + + +def remove_duplicates(input_list, key_func=lambda x: x): # pragma: no cover + """ + Remove duplicates from the list by using the key_func on each element + to determine the "key" used for identifying duplicates. + """ + return list( + collections.OrderedDict((key_func(item), item) for item in input_list).values(), + ) + + +async def dimension_join_path( + session: AsyncSession, + node: NodeRevision, + dimension: str, +) -> Optional[List[DimensionLink]]: + """ + Find a join path between this node and the dimension attribute. + * If there is no possible join path, returns None + * If it is a local dimension on this node, return [] + * If it is in one of the dimension nodes on the dimensions graph, return a + list of dimension links that represent the join path + """ + # Check if it is a local dimension + for col in node.columns: # pragma: no cover + # Decide if we should restrict this to only columns marked as dimensional + # await session.refresh(col, ["attributes"]) TODO + # if col.is_dimensional(): + # ... + if f"{node.name}.{col.name}" == dimension: + return [] + if ( + col.dimension + and f"{col.dimension.name}.{col.dimension_column}" == dimension + ): + return [] + + dimension_attr = FullColumnName(dimension) + + # If it's not a local dimension, traverse the node's dimensions graph + # This queue tracks the dimension link being processed and the path to that link + await refresh_if_needed(session, node, ["dimension_links"]) + + # Start with first layer of linked dims + processing_queue = collections.deque( + [(link, [link]) for link in node.dimension_links], + ) + while processing_queue: + current_link, join_path = processing_queue.popleft() + await refresh_if_needed(session, current_link, ["dimension"]) + if current_link.dimension.name == dimension_attr.node_name: + return join_path + + await refresh_if_needed(session, current_link.dimension, ["current"]) + await refresh_if_needed( + session, + current_link.dimension.current, + ["dimension_links"], + ) + processing_queue.extend( + [ + (link, join_path + [link]) + for link in current_link.dimension.current.dimension_links + ], + ) + return None + + +async def build_dimension_node_query( + session: AsyncSession, + build_criteria: Optional[BuildCriteria], + link: DimensionLink, + filters: List[str], + cte_mapping: Dict[str, ast.Query], + use_materialized: bool = True, +): + """ + Builds a dimension node query with the requested filters + """ + await refresh_if_needed(session, link.dimension, ["current"]) + await refresh_if_needed( + session, + link.dimension.current, + ["availability", "columns"], + ) + physical_table = get_table_for_node( + link.dimension.current, + build_criteria=build_criteria, + ) + dimension_node_ast = ( + await compile_node_ast(session, link.dimension.current) + if not physical_table + else ast.Query( + select=ast.Select( + projection=physical_table.columns, # type: ignore + from_=ast.From(relations=[ast.Relation(physical_table)]), + ), + ) + ) + dimension_node_query = await build_ast( + session, + link.dimension.current, + dimension_node_ast, + filters=filters, # type: ignore + build_criteria=build_criteria, + ctes_mapping=cte_mapping, + use_materialized=use_materialized, + use_pickled=not physical_table, + ) + return dimension_node_query + + +def convert_to_cte( + inner_query: ast.Query, + outer_query: ast.Query, + cte_name: str, +): + """ + Convert the query to a CTE that can be used by the outer query + """ + # Move all the CTEs used by the inner query to the outer query + for cte in inner_query.ctes: + cte.set_parent(outer_query, parent_key="ctes") + outer_query.ctes.extend(inner_query.ctes) + inner_query.ctes = [] + + # Convert the dimension node query to a CTE + inner_query = inner_query.to_cte( + ast.Name(amenable_name(cte_name)), + outer_query, + ) + return inner_query + + +def build_requested_dimensions_columns( + requested_dimensions, + link, + dimension_node_joins, +) -> Tuple[list[ast.Column], list[DJQueryBuildError]]: + """ + Builds the requested dimension columns for the final select layer. + """ + dimensions_columns = [] + errors = [] + for dim in requested_dimensions: + replacement = build_dimension_attribute( + dim, + dimension_node_joins, + link, + alias=amenable_name(dim), + ) + if replacement: # pragma: no cover + dimensions_columns.append(replacement) + else: + errors.append( + DJQueryBuildError( + code=ErrorCode.INVALID_DIMENSION, + message=f"Dimension attribute {dim} does not exist!", + ), + ) + return dimensions_columns, errors + + +async def compile_node_ast(session, node_revision: NodeRevision) -> ast.Query: + """ + Parses the node's query into an AST and compiles it. + """ + node_ast = parse(node_revision.query) + ctx = CompileContext(session, DJException()) + await node_ast.compile(ctx) + return node_ast + + +def build_dimension_attribute( + full_column_name: str, + dimension_node_joins: Dict[str, DimensionJoin], + link: DimensionLink, + alias: Optional[str] = None, +) -> Optional[ast.Column]: + """ + Turn the canonical dimension attribute into a column on the query AST + """ + dimension_attr = FullColumnName(full_column_name) + dim_node = dimension_attr.node_name + node_query = ( + dimension_node_joins[dim_node].node_query + if dim_node in dimension_node_joins + else None + ) + + if node_query: + foreign_key_column_name = ( + FullColumnName( + link.foreign_keys_reversed.get(dimension_attr.name), + ).column_name + if dimension_attr.name in link.foreign_keys_reversed + else None + ) + for col in node_query.select.projection: + if col.alias_or_name.name == dimension_attr.column_name or ( # type: ignore + foreign_key_column_name + and col.alias_or_name.identifier() == foreign_key_column_name # type: ignore + ): + return ast.Column( + name=ast.Name(col.alias_or_name.name), # type: ignore + alias=ast.Name(alias) if alias else None, + _table=node_query, + _type=col.type, # type: ignore + ) + return None # pragma: no cover + + +async def needs_dimension_join( + session: AsyncSession, + dimension_attribute: str, + join_path: List["DimensionLink"], +) -> bool: + """ + Checks if the requested dimension attribute needs a dimension join or + if it can be pulled from an existing column on the node. + """ + if len(join_path) == 1: + link = join_path[0] + await refresh_if_needed(session, link.dimension, ["current"]) + await refresh_if_needed(session, link.dimension.current, ["columns"]) + if dimension_attribute in link.foreign_keys_reversed: + return False + return True + + +def combine_filter_conditions( + existing_condition, *new_conditions +) -> Optional[Union[ast.BinaryOp, ast.Expression]]: + """ + Combines the existing where clause with new filter conditions. + """ + if not existing_condition and not new_conditions: + return None + if not existing_condition: + return ast.BinaryOp.And(*new_conditions) + return ast.BinaryOp.And(existing_condition, *new_conditions) + + +def build_join_for_link( + link: "DimensionLink", + cte_mapping: Dict[str, ast.Query], + join_right: ast.Query, +): + """ + Build a join for the dimension link using the provided query table expression + on the left and the provided query table expression on the right. + """ + join_ast = link.joins()[0] + join_ast.right = join_right.alias # type: ignore + dimension_node_columns = join_right.select.column_mapping + join_left = cte_mapping.get(link.node_revision.name) + node_columns = join_left.select.column_mapping # type: ignore + if not join_ast.criteria: + return join_ast + for col in join_ast.criteria.find_all(ast.Column): # type: ignore + full_column = FullColumnName(col.identifier()) + is_dimension_node = full_column.node_name == link.dimension.name + if full_column.column_name not in ( + dimension_node_columns if is_dimension_node else node_columns + ): + raise DJQueryBuildException( # pragma: no cover + f"The requested column {full_column.column_name} does not exist" + f" on {full_column.node_name}", + ) + replacement = ast.Column( + name=ast.Name(full_column.column_name), + _table=join_right if is_dimension_node else join_left, + _type=(dimension_node_columns if is_dimension_node else node_columns) + # type: ignore + .get(full_column.column_name) + .type, + ) + col.parent.replace(col, replacement) # type: ignore + return join_ast + + +async def build_ast( + session: AsyncSession, + node: NodeRevision, + query: ast.Query, + filters: Optional[List[str]], + build_criteria: Optional[BuildCriteria] = None, + access_control=None, + ctes_mapping: Dict[str, ast.Query] = None, + use_materialized: bool = True, + use_pickled: bool = True, +) -> ast.Query: + """ + Recursively replaces DJ node references with query ASTs. These are replaced with + materialized tables where possible (i.e., source nodes will always be replaced with a + materialized table), but otherwise we generate the SQL of each upstream node reference. + + This function will apply any filters that can be pushed down to each referenced node's AST + (filters are only applied if they don't require dimension node joins). + """ + context = CompileContext(session=session, exception=DJException()) + cached_query_ast = node.query_ast + if use_pickled and cached_query_ast: + try: # pragma: no cover + query = cached_query_ast # pragma: no cover + except TypeError as exc: # pragma: no cover + logger.error( + "Error loading query AST pickle for %s@%s: %s", + node.name, + node.version, + exc, + ) + await query.compile(context) + else: + await query.compile(context) + + query.bake_ctes() + await refresh_if_needed(session, node, ["dimension_links"]) + + new_cte_mapping: Dict[str, ast.Query] = {} + if ctes_mapping is None: + ctes_mapping = new_cte_mapping # pragma: no cover + + node_to_tables_mapping = get_dj_node_references_from_select(query.select) + for referenced_node, reference_expressions in node_to_tables_mapping.items(): + await refresh_if_needed(session, referenced_node, ["dimension_links"]) + + for ref_expr in reference_expressions: + # Try to find a materialized table attached to this node, if one exists. + physical_table = None + if use_materialized: + logger.debug("Checking for physical node: %s", referenced_node.name) + physical_table = cast( + Optional[ast.Table], + get_table_for_node( + referenced_node, + build_criteria=build_criteria, + ), + ) + + if not physical_table: + logger.debug("Didn't find physical node: %s", referenced_node.name) + # Build a new CTE with the query AST if there is no materialized table + if referenced_node.name not in ctes_mapping: + node_query = parse(cast(str, referenced_node.query)) + query_ast = await build_ast( # type: ignore + session, + referenced_node, + node_query, + filters=filters, + build_criteria=build_criteria, + access_control=access_control, + ctes_mapping=ctes_mapping, + use_materialized=use_materialized, + ) + cte_name = ast.Name(amenable_name(referenced_node.name)) + query_ast = query_ast.to_cte(cte_name, parent_ast=query) + if referenced_node.name not in new_cte_mapping: # pragma: no cover + new_cte_mapping[referenced_node.name] = query_ast + + reference_cte = ( + ctes_mapping[referenced_node.name] + if referenced_node.name in ctes_mapping + else new_cte_mapping[referenced_node.name] + ) + query_ast = ast.Table( # type: ignore + reference_cte.alias, # type: ignore + _columns=reference_cte._columns, + _dj_node=referenced_node, + ) + else: + # Otherwise use the materialized table and apply filters where possible + alias = amenable_name(referenced_node.name) + query_ast = ast.Query( + select=ast.Select( + projection=physical_table.columns, # type: ignore + from_=ast.From(relations=[ast.Relation(physical_table)]), + ), + alias=ast.Name(alias), + ) + query_ast.parenthesized = True + apply_filters_to_node( + referenced_node, + query_ast, + to_filter_asts(filters), + ) + if not query_ast.select.where: + query_ast = ast.Alias( # type: ignore + ast.Name(alias), + child=physical_table, + as_=True, + ) + + # If the user has set an alias for the node reference, reuse the + # same alias for the built query + if ref_expr.alias and hasattr(query_ast, "alias"): + query_ast.alias = ref_expr.alias + query.select.replace( + ref_expr, + query_ast, + copy=False, + ) + await query.select.compile(context) + for col in query.select.find_all(ast.Column): + if ( + col.table + and not col.table.alias + and isinstance(col.table, ast.Table) + and col.table.dj_node + and col.table.dj_node.name == referenced_node.name + ): + col._table = query_ast + + # Apply pushdown filters if possible + apply_filters_to_node(node, query, to_filter_asts(filters)) + + for cte in new_cte_mapping.values(): + query.ctes.extend(cte.ctes) + cte.ctes = [] + query.ctes.append(cte) + query.select.add_aliases_to_unnamed_columns() + ctes_mapping.update(new_cte_mapping) + return query + + +def apply_filters_to_node( + node: NodeRevision, + query: ast.Query, + filters: List[ast.Expression], +): + """ + Apply pushdown filters if possible to the node's query AST. + + A pushdown filter is defined as a filter with references to dimensions that + already exist on the node query without any additional dimension joins. We can + apply these filters directly to the query AST by renaming the dimension ref in + the filter expression. + """ + for filter_ast in filters: + all_referenced_dimensions_can_pushdown = True + if not filter_ast: + continue + for filter_dim in filter_ast.find_all(ast.Column): + column_name = get_column_from_canonical_dimension( + filter_dim.identifier(), + node, + ) + node_col = ( + query.select.column_mapping.get(column_name) if column_name else None + ) + if node_col: + replacement = ( + node_col.child if isinstance(node_col, ast.Alias) else node_col # type: ignore + ).copy() + replacement.set_alias(None) + filter_ast.replace(filter_dim, replacement) + else: + all_referenced_dimensions_can_pushdown = False + + if all_referenced_dimensions_can_pushdown: + query.select.where = combine_filter_conditions( + query.select.where, + filter_ast, + ) + return query + + +def get_dj_node_references_from_select( + select: ast.SelectExpression, +) -> DefaultDict[NodeRevision, List[ast.Table]]: + """ + Extract all DJ node references (source, transform, dimensions) from the select + expression. DJ node references are represented in the AST as table expressions + and have an attached DJ node. + """ + tables: DefaultDict[NodeRevision, List[ast.Table]] = collections.defaultdict(list) + + for table in select.find_all(ast.Table): + if node := table.dj_node: # pragma: no cover + tables[node].append(table) + return tables + + +def get_table_for_node( + node: NodeRevision, + build_criteria: Optional[BuildCriteria] = None, +) -> Optional[ast.Table]: + """ + If a node has a materialized table available, return the materialized table. + Source nodes should always have an associated table, whereas for all other nodes + we can check the materialization type. + """ + table = None + can_use_materialization = ( + build_criteria and node.name != build_criteria.target_node_name + ) + if node.type == NodeType.SOURCE: + table_name = ( + f"{node.catalog.name}.{node.schema_}.{node.table}" + if node.schema_ == "iceberg" + else f"{node.schema_}.{node.table}" + ) + name = to_namespaced_name(table_name if node.table else node.name) + table = ast.Table( + name, + _columns=[ + ast.Column(name=ast.Name(col.name), _type=col.type) + for col in node.columns + ], + _dj_node=node, + ) + elif ( + can_use_materialization + and node.availability + and node.availability.is_available( + criteria=build_criteria, + ) + ): # pragma: no cover + table = ast.Table( + ast.Name( + node.availability.table, + namespace=( + ast.Name(node.availability.schema_) + if node.availability.schema_ + else None + ), + ), + _columns=[ + ast.Column(name=ast.Name(col.name), _type=col.type) + for col in node.columns + ], + _dj_node=node, + ) + return table diff --git a/datajunction-server/datajunction_server/construction/dimensions.py b/datajunction-server/datajunction_server/construction/dimensions.py new file mode 100644 index 000000000..e92f27fda --- /dev/null +++ b/datajunction-server/datajunction_server/construction/dimensions.py @@ -0,0 +1,131 @@ +""" +Dimensions-related query building +""" + +from typing import List, Optional + +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.api.helpers import get_catalog_by_name +from datajunction_server.construction.build_v2 import get_measures_query +from datajunction_server.database.node import NodeRevision +from datajunction_server.database.user import User +from datajunction_server.errors import DJInvalidInputException +from datajunction_server.models import access +from datajunction_server.models.column import SemanticType +from datajunction_server.models.metric import TranslatedSQL +from datajunction_server.models.query import ColumnMetadata +from datajunction_server.naming import amenable_name, from_amenable_name +from datajunction_server.sql.parsing import ast +from datajunction_server.sql.parsing.backends.antlr4 import parse +from datajunction_server.sql.parsing.types import IntegerType +from datajunction_server.utils import SEPARATOR + + +async def build_dimensions_from_cube_query( + session: AsyncSession, + cube: NodeRevision, + dimensions: List[str], + current_user: User, + validate_access: access.ValidateAccessFn, + filters: Optional[str] = None, + limit: Optional[int] = 50000, + include_counts: bool = False, +) -> TranslatedSQL: + """ + Builds a query for retrieving unique values of a dimension for the given cube. + The filters provided here are additional filters layered on top of any existing cube filters. + Setting `include_counts` to true will also provide associated counts for each dimension value. + """ + unavailable_dimensions = set(dimensions) - set(cube.cube_dimensions()) + if unavailable_dimensions: + raise DJInvalidInputException( + f"The following dimensions {unavailable_dimensions} are not " + f"available in the cube {cube.name}.", + ) + query_ast: ast.Query = ast.Query( + select=ast.Select(from_=ast.From(relations=[])), + ctes=[], + ) + for dimension in dimensions or cube.cube_dimensions(): + dimension_column = ast.Column( + name=ast.Name(amenable_name(dimension)), + ) + query_ast.select.projection.append(dimension_column) + query_ast.select.group_by.append(dimension_column) + if include_counts: + query_ast.select.projection.append( + ast.Function( + name=ast.Name("COUNT"), + args=[ast.Column(name=ast.Name("*"))], + ), + ) + query_ast.select.organization = ast.Organization( + order=[ + ast.SortItem( + expr=ast.Column( + name=ast.Name(str(len(query_ast.select.projection))), + ), + asc="DESC", + nulls="", + ), + ], + ) + query_ast.select.where = None + + if limit is not None: + query_ast.select.limit = ast.Number(limit) + + # Build the FROM clause. The source table on the FROM clause depends on whether + # the cube is available as a materialized datasource or if it needs to be built up + # from the measures query. + if cube.availability: + catalog = await get_catalog_by_name(session, cube.availability.catalog) # type: ignore + query_ast.select.from_.relations.append( # type: ignore + ast.Relation(primary=ast.Table(ast.Name(cube.availability.table))), # type: ignore + ) + if filters: + temp_filters_select = parse(f"select * where {filters}") + for col in temp_filters_select.find_all(ast.Column): + if ( # pragma: no cover + col.alias_or_name.identifier() in cube.cube_dimensions() + ): + col.name = ast.Name( + name=amenable_name(col.alias_or_name.identifier()), + ) + query_ast.select.where = temp_filters_select.select.where + else: + catalog = cube.catalog + measures_query = await get_measures_query( + session=session, + metrics=[metric.name for metric in cube.cube_metrics()], + dimensions=dimensions, + filters=[filters] if filters else [], + current_user=current_user, + validate_access=validate_access, + ) + measures_query_ast = parse(measures_query[0].sql) + measures_query_ast.bake_ctes() + measures_query_ast.parenthesized = True + query_ast.select.from_.relations.append( # type: ignore + ast.Relation(primary=measures_query_ast), + ) + types_lookup = { + amenable_name(elem.node_revision().name + SEPARATOR + elem.name): elem.type # type: ignore + for elem in cube.cube_elements + } + return TranslatedSQL( + sql=str(query_ast), + columns=[ + ColumnMetadata( + name=col.name.name, # type: ignore + type=str(types_lookup.get(col.name.name)), # type: ignore + semantic_entity=from_amenable_name(col.name.name), # type: ignore + semantic_type=SemanticType.DIMENSION, + ) + if col.name.name in types_lookup # type: ignore + else ColumnMetadata(name="count", type=str(IntegerType())) + for col in query_ast.select.projection + ], + dialect=catalog.engines[0].dialect if catalog else None, + ) diff --git a/datajunction-server/datajunction_server/construction/dj_query.py b/datajunction-server/datajunction_server/construction/dj_query.py new file mode 100644 index 000000000..01d64084a --- /dev/null +++ b/datajunction-server/datajunction_server/construction/dj_query.py @@ -0,0 +1,263 @@ +# pragma: no cover +""" +Functions for making queries directly against DJ +""" + +from typing import Dict, List, Set, Tuple + +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.construction.build import build_metric_nodes +from datajunction_server.construction.build_v2 import QueryBuilder +from datajunction_server.construction.utils import try_get_dj_node +from datajunction_server.database.node import Node +from datajunction_server.models.node_type import NodeType +from datajunction_server.naming import amenable_name +from datajunction_server.sql.parsing import ast +from datajunction_server.sql.parsing.backends.antlr4 import parse + + +def selects_only_metrics(select: ast.Select) -> bool: + """ + Checks that a Select only has FROM metrics + """ + + return ( + select.from_ is not None + and len(select.from_.relations) == 1 + and len(select.from_.relations[0].extensions) == 0 + and str(select.from_.relations[0].primary) == "metrics" + ) + + +async def resolve_metric_queries( + session: AsyncSession, + tree: ast.Query, + ctx: ast.CompileContext, + touched_nodes: Set[int], + metrics: List[Node], +): + """ + Find all Metric Node references + ensure they belong to a query sourcing `metrics` + build the queries as if they were API queries + replace them in the original query + """ + for col in tree.find_all(ast.Column): + curr_cols = [] + metric_nodes = [] + if id(col) in touched_nodes: + continue + ident = col.identifier(False) + if metric_node := await try_get_dj_node(session, ident, {NodeType.METRIC}): + curr_cols.append((True, col)) + # if we found a metric node we need to check where it came from + parent_select = col.get_nearest_parent_of_type(ast.Select) + if not parent_select or not selects_only_metrics(parent_select): + raise ast.DJParseException( + "Any SELECT referencing a Metric must source " + "from a single unaliased Table named `metrics`.", + ) + metric_nodes.append(metric_node) + metrics.append(metric_node) + dimensions = [str(exp) for exp in parent_select.group_by] + + if any( + ( + parent_select.having, + parent_select.lateral_views, + parent_select.set_op, + ), + ): + raise ast.DJParseException( + "HAVING, LATERAL VIEWS, and SET OPERATIONS " + "are not allowed on `metrics` queries.", + ) + + for sibling_col in parent_select.projection: + if sibling_col is col: + continue + if not isinstance(sibling_col, ast.Column): + raise ast.DJParseException( + "Only direct Columns are allowed in " + f"`metrics` queries, found `{sibling_col}`.", + ) + + sibling_ident = sibling_col.identifier(False) + + sibling_node = await try_get_dj_node( + session, + sibling_ident, + {NodeType.METRIC}, + ) + + if sibling_ident in dimensions: + curr_cols.append((False, sibling_col)) + elif sibling_node is not None: + curr_cols.append((True, sibling_col)) + else: + raise ast.DJParseException( + "You can only select direct METRIC nodes or a column " + f"from your GROUP BY on `metrics` queries, found `{sibling_col}`", + ) + + if sibling_node: + metric_nodes.append(sibling_node) + touched_nodes.add(id(sibling_col)) + + filters = [str(parent_select.where)] if parent_select.where else [] + + orderby = [ + str(sort) + for sort in ( + parent_select.organization.order + parent_select.organization.sort + if parent_select.organization + else [] + ) + ] + limit = None + if limit_exp := parent_select.limit: + try: + limit = int(str(limit_exp)) # type: ignore + except ValueError as exc: + raise ast.DJParseException( + f"LIMITs on `metrics` queries can only be integers not `{limit_exp}`.", + ) from exc + touched_nodes.add(id(col)) + + cte_name = ast.Name(f"metric_query_{len(tree.ctes)}") + + built = ( + ( + await build_metric_nodes( + session, + metric_nodes, + filters, + dimensions, + orderby, + limit, + ) + ) + .bake_ctes() + .set_alias(cte_name) + .set_as(True) + ) + built.parenthesized = True + built.compile(ctx) + for built_col in built.columns: # pragma: no cover + built_col.alias_or_name.namespace = None # pragma: no cover + for _, cur_col in curr_cols: + name = amenable_name(cur_col.identifier(False)) + ref_type = [ + col + for col in built.select.projection + if col.alias_or_name.name == name + ][0].type + + swap_col = ( + ast.Column(ast.Name(name), _type=ref_type, _table=built) + .set_alias(cur_col.alias and cur_col.alias.copy()) + .set_as(True) + ) + cur_col.swap(swap_col) + + swap_select = ast.Select( + projection=parent_select.projection, + from_=ast.From( + relations=[ast.Relation(primary=ast.Table(built.alias_or_name))], + ), + ) + parent_select.swap(swap_select) + + tree.ctes = tree.ctes + [built] + touched_nodes.add(id(built)) + touched_nodes.add(id(swap_select)) + return tree + + +def find_all_other( + node: ast.Node, + touched_nodes: Set[int], + node_map: Dict[str, List[ast.Column]], +): + """ + Find all non-Metric DJ Node references + """ + if id(node) in touched_nodes: + return + touched_nodes.add(id(node)) + if isinstance(node, ast.Column) and node.table is None: + if namespace_name := node.name.namespace: + namespace = namespace_name.identifier(False) + if namespace in node_map: + node_map[namespace].append(node) + else: + node_map[namespace] = [node] + else: + node.apply(lambda n: find_all_other(n, touched_nodes, node_map)) + + +async def resolve_all( + session: AsyncSession, + ctx: ast.CompileContext, + tree: ast.Query, + dj_nodes: List[Node], +): + """ + Resolve all references to DJ Nodes + """ + touched_nodes: Set[int] = set() + tree = await resolve_metric_queries(session, tree, ctx, touched_nodes, dj_nodes) + node_map: Dict[str, List[ast.Column]] = {} + find_all_other(tree, touched_nodes, node_map) + for namespace, cols in node_map.items(): + if dj_node := await try_get_dj_node( # pragma: no cover + session, + namespace, + {NodeType.SOURCE, NodeType.TRANSFORM, NodeType.DIMENSION}, + ): + dj_nodes.append(dj_node) + cte_name = ast.Name(f"node_query_{len(tree.ctes)}") + current_dj_node = dj_node.current + query_builder = await QueryBuilder.create(session, current_dj_node) + built = (await query_builder.build()).bake_ctes() + built.alias = cte_name + built.set_as(True) + built.parenthesized = True + await built.compile(ctx) + for cur_col in cols: + name = cur_col.name.name + ref_col = [col for col in current_dj_node.columns if col.name == name][ + 0 + ] + swap_col = ( + ast.Column(ast.Name(name), _table=built, _type=ref_col.type) + .set_alias(cur_col.alias and cur_col.alias.copy()) + .set_as(True) + ) + cur_col.swap(swap_col) + for tbl in tree.filter( + lambda node: isinstance(node, ast.Table) + and node.identifier(False) == dj_node.name, # type: ignore + ): + tbl.name = cte_name + tree.ctes = tree.ctes + [built] + built.parent = tree + return tree + + +async def build_dj_query( + session: AsyncSession, + query: str, +) -> Tuple[ast.Query, List[Node]]: + """ + Build a sql query that refers to DJ Nodes + """ + dj_nodes: List[Node] = [] # metrics first if any + ctx = ast.CompileContext(session, ast.DJException()) + tree = parse(query).bake_ctes() + tree = await resolve_all(session, ctx, tree, dj_nodes) + await tree.compile(ctx) + if not dj_nodes: + raise ast.DJParseException(f"Found no dj nodes in query `{query}`.") + return tree, dj_nodes diff --git a/datajunction-server/datajunction_server/construction/exceptions.py b/datajunction-server/datajunction_server/construction/exceptions.py new file mode 100755 index 000000000..2bf9eae39 --- /dev/null +++ b/datajunction-server/datajunction_server/construction/exceptions.py @@ -0,0 +1,57 @@ +""" +Exceptions used in construction +""" + +from typing import List, Optional + +from datajunction_server.errors import DJError, DJQueryBuildException + + +class CompoundBuildException: + """ + Exception singleton to optionally build up exceptions or raise + """ + + errors: List[DJError] + _instance: Optional["CompoundBuildException"] = None + _raise: bool = True + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super(CompoundBuildException, cls).__new__( + cls, *args, **kwargs + ) + cls.errors = [] + return cls._instance + + def reset(self): + """ + Resets the singleton + """ + self._raise = True + self.errors = [] + + def set_raise(self, raise_: bool): + """ + Set whether to raise caught exceptions or accumulate them + """ + self._raise = raise_ + + def append(self, error: DJError, message: Optional[str] = None): + """ + Accumulate DJ exceptions + """ + if self._raise: + raise DJQueryBuildException( + message=message or error.message, + errors=[error], + ) + self.errors.append(error) + + def __str__(self) -> str: + plural = "s" if len(self.errors) > 1 else "" + error = f"Found {len(self.errors)} issue{plural}:\n" + return error + "\n\n".join( + "\t" + str(type(exc).__name__) + ": " + str(exc) + "\n" + "=" * 50 + for exc in self.errors + ) diff --git a/datajunction-server/datajunction_server/construction/utils.py b/datajunction-server/datajunction_server/construction/utils.py new file mode 100755 index 000000000..63c873329 --- /dev/null +++ b/datajunction-server/datajunction_server/construction/utils.py @@ -0,0 +1,92 @@ +""" +Utilities used around construction +""" + +from typing import TYPE_CHECKING, Optional, Set, Union + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload +from sqlalchemy.orm.exc import NoResultFound + +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.errors import DJError, DJErrorException, ErrorCode +from datajunction_server.models.node_type import NodeType + +if TYPE_CHECKING: + from datajunction_server.sql.parsing.ast import Column, Name + + +async def get_dj_node( + session: AsyncSession, + node_name: str, + kinds: Optional[Set[NodeType]] = None, + current: bool = True, +) -> NodeRevision: + """Return the DJ Node with a given name from a set of node types""" + query = select(Node).filter(Node.name == node_name) + if kinds: + query = query.filter(Node.type.in_(kinds)) # type: ignore + match = None + try: + match = ( + ( + await session.execute( + query.options( + joinedload(Node.current).options( + *NodeRevision.default_load_options() + ), + ), + ) + ) + .unique() + .scalar_one() + ) + except NoResultFound as no_result_exc: + kind_msg = " or ".join(str(k) for k in kinds) if kinds else "" + raise DJErrorException( + DJError( + code=ErrorCode.UNKNOWN_NODE, + message=f"No node `{node_name}` exists of kind {kind_msg}.", + ), + ) from no_result_exc + return match.current if match and current else match + + +async def try_get_dj_node( + session: AsyncSession, + name: Union[str, "Column"], + kinds: Optional[Set[NodeType]] = None, +) -> Optional[Node]: + "wraps get dj node to return None if no node is found" + from datajunction_server.sql.parsing.ast import Column + + if isinstance(name, Column): + if name.name.namespace is not None: # pragma: no cover + name = name.name.namespace.identifier(False) # pragma: no cover + else: # pragma: no cover + return None # pragma: no cover + try: + return await get_dj_node(session, name, kinds, current=False) + except DJErrorException: + return None + + +def to_namespaced_name(name: str) -> "Name": + """ + Builds a namespaced name from a string + """ + from datajunction_server.sql.parsing.ast import Name + + chunked = name.split(".") + chunked.reverse() + current_name = None + full_name = None + for chunk in chunked: + if not current_name: + current_name = Name(chunk) + full_name = current_name + else: + current_name.namespace = Name(chunk) + current_name = current_name.namespace + return full_name # type: ignore diff --git a/datajunction-server/datajunction_server/database/__init__.py b/datajunction-server/datajunction_server/database/__init__.py new file mode 100644 index 000000000..00e43e473 --- /dev/null +++ b/datajunction-server/datajunction_server/database/__init__.py @@ -0,0 +1,38 @@ +"""All database schemas.""" + +__all__ = [ + "AttributeType", + "ColumnAttribute", + "Catalog", + "Collection", + "Database", + "DimensionLink", + "Engine", + "History", + "Node", + "NodeNamespace", + "NodeRevision", + "NotificationPreference", + "Partition", + "QueryRequest", + "Table", + "Tag", + "User", + "Measure", +] + +from datajunction_server.database.attributetype import AttributeType, ColumnAttribute +from datajunction_server.database.catalog import Catalog +from datajunction_server.database.collection import Collection +from datajunction_server.database.database import Database, Table +from datajunction_server.database.dimensionlink import DimensionLink +from datajunction_server.database.engine import Engine +from datajunction_server.database.measure import Measure +from datajunction_server.database.namespace import NodeNamespace +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.notification_preference import NotificationPreference +from datajunction_server.database.partition import Partition +from datajunction_server.database.queryrequest import QueryRequest +from datajunction_server.database.tag import Tag +from datajunction_server.database.user import User +from datajunction_server.models.history import History diff --git a/datajunction-server/datajunction_server/database/attributetype.py b/datajunction-server/datajunction_server/database/attributetype.py new file mode 100644 index 000000000..008b41d62 --- /dev/null +++ b/datajunction-server/datajunction_server/database/attributetype.py @@ -0,0 +1,114 @@ +"""Attribute type database schema.""" + +from typing import TYPE_CHECKING, List, Optional + +import sqlalchemy as sa +from sqlalchemy import UniqueConstraint, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from datajunction_server.database.base import Base +from datajunction_server.models.attribute import MutableAttributeTypeFields + +if TYPE_CHECKING: + from datajunction_server.database.column import Column + + +class AttributeType(Base): + """ + Available attribute types for column metadata. + """ + + __tablename__ = "attributetype" + __table_args__ = (UniqueConstraint("namespace", "name"),) + + id: Mapped[int] = mapped_column( + sa.BigInteger().with_variant(sa.Integer, "sqlite"), + primary_key=True, + ) + namespace: Mapped[str] = mapped_column(nullable=False, default="system") + name: Mapped[str] = mapped_column(nullable=False) + description: Mapped[str] = mapped_column(nullable=False) + allowed_node_types: Mapped[List[str]] = mapped_column(sa.JSON, nullable=True) + uniqueness_scope: Mapped[List[str]] = mapped_column(sa.JSON, nullable=True) + + def __hash__(self): + return hash(self.id) + + @classmethod + async def get_all(cls, session: AsyncSession): + """ + Get all AttributeTypes. + """ + stmt = select(cls) + result = await session.execute(stmt) + return result.scalars().all() + + @classmethod + async def get_by_name( + cls, + session: AsyncSession, + name: str, + ) -> Optional["AttributeType"]: + """ + Get an AttributeType by name. + """ + stmt = select(cls).where(cls.name == name) + result = await session.execute(stmt) + return result.unique().one_or_none() + + @classmethod + async def create( + cls, + session: AsyncSession, + new_obj: MutableAttributeTypeFields, + ) -> Optional["AttributeType"]: + """ + Get an AttributeType by name. + """ + attribute_type = AttributeType( + namespace=new_obj.namespace, + name=new_obj.name, + description=new_obj.description, + allowed_node_types=new_obj.allowed_node_types, + uniqueness_scope=new_obj.uniqueness_scope + if new_obj.uniqueness_scope + else [], + ) + session.add(attribute_type) + await session.commit() + await session.refresh(attribute_type) + return attribute_type + + +class ColumnAttribute(Base): + """ + Column attributes. + """ + + __tablename__ = "columnattribute" + __table_args__ = (UniqueConstraint("attribute_type_id", "column_id"),) + + id: Mapped[int] = mapped_column( + sa.BigInteger().with_variant(sa.Integer, "sqlite"), + primary_key=True, + ) + + attribute_type_id: Mapped[int] = mapped_column( + sa.ForeignKey( + "attributetype.id", + name="fk_columnattribute_attribute_type_id_attributetype", + ), + ) + attribute_type: Mapped[AttributeType] = relationship( + foreign_keys=[attribute_type_id], + lazy="joined", + ) + + column_id: Mapped[Optional[int]] = mapped_column( + sa.ForeignKey("column.id", name="fk_columnattribute_column_id_column"), + ) + column: Mapped[Optional["Column"]] = relationship( + back_populates="attributes", + foreign_keys=[column_id], + ) diff --git a/datajunction-server/datajunction_server/database/availabilitystate.py b/datajunction-server/datajunction_server/database/availabilitystate.py new file mode 100644 index 000000000..c38e5a044 --- /dev/null +++ b/datajunction-server/datajunction_server/database/availabilitystate.py @@ -0,0 +1,123 @@ +"""Availability state database schema.""" + +from datetime import datetime, timezone +from functools import partial +from typing import TYPE_CHECKING, Dict, List, Optional + +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy import JSON, DateTime, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from datajunction_server.database.base import Base +from datajunction_server.models.node import BuildCriteria, PartitionAvailability +from datajunction_server.typing import UTCDatetime + +if TYPE_CHECKING: + from datajunction_server.database.materialization import Materialization + + +class AvailabilityState(Base): + """ + The availability of materialized data for a node + """ + + __tablename__ = "availabilitystate" + + id: Mapped[int] = mapped_column( + sa.BigInteger().with_variant(sa.Integer, "sqlite"), + primary_key=True, + ) + + # Identifying where the dataset lives + catalog: Mapped[str] + schema_: Mapped[Optional[str]] = mapped_column(nullable=True) + table: Mapped[str] + + # Indicates data freshness + valid_through_ts: Mapped[int] = mapped_column(sa.BigInteger()) + + # Arbitrary JSON metadata. This can encompass any URLs associated with the materialized dataset + custom_metadata: Mapped[Optional[Dict]] = mapped_column( + JSONB, + default=dict, + ) + + # The materialization that this availability is associated with, if any + materialization_id: Mapped[Optional[int]] = mapped_column( + ForeignKey( + "materialization.id", + name="fk_availability_materialization_id_materialization", + ), + ) + materialization: Mapped[Optional["Materialization"]] = relationship( + back_populates="availability", + primaryjoin="Materialization.id==AvailabilityState.materialization_id", + ) + + # An ordered list of categorical partitions like ["country", "group_id"] + # or ["region_id", "age_group"] + categorical_partitions: Mapped[Optional[List[str]]] = mapped_column( + JSON, + default=[], + ) + + # An ordered list of temporal partitions like ["date", "hour"] or ["date"] + temporal_partitions: Mapped[Optional[List[str]]] = mapped_column( + JSON, + default=[], + ) + + # Node-level temporal ranges + min_temporal_partition: Mapped[Optional[List[str]]] = mapped_column( + JSON, + default=[], + ) + max_temporal_partition: Mapped[Optional[List[str]]] = mapped_column( + JSON, + default=[], + ) + + # Partition-level availabilities + partitions: Mapped[Optional[List[PartitionAvailability]]] = mapped_column( + JSON, + default=[], + ) + updated_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + default=partial(datetime.now, timezone.utc), + ) + + def is_available( + self, + criteria: Optional[BuildCriteria] = None, + ) -> bool: # pragma: no cover + """ + Determine whether an availability state is useable given criteria + """ + # TODO: we should evaluate this availability state against the criteria. + # Remember that VTTS can be also evaluated at runtime dependency. + return True + + +class NodeAvailabilityState(Base): + """ + Join table for availability state + """ + + __tablename__ = "nodeavailabilitystate" + + availability_id: Mapped[int] = mapped_column( + ForeignKey( + "availabilitystate.id", + name="fk_nodeavailabilitystate_availability_id_availabilitystate", + ), + primary_key=True, + ) + node_id: Mapped[int] = mapped_column( + ForeignKey( + "noderevision.id", + name="fk_nodeavailabilitystate_node_id_noderevision", + ), + primary_key=True, + ) diff --git a/datajunction-server/datajunction_server/database/backfill.py b/datajunction-server/datajunction_server/database/backfill.py new file mode 100644 index 000000000..ed8a3d18f --- /dev/null +++ b/datajunction-server/datajunction_server/database/backfill.py @@ -0,0 +1,49 @@ +"""Backfill database schema.""" + +from typing import TYPE_CHECKING, List, Optional + +from sqlalchemy import JSON, BigInteger, ForeignKey, Integer +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from datajunction_server.database.base import Base +from datajunction_server.models.partition import PartitionBackfill + +if TYPE_CHECKING: + from datajunction_server.database.materialization import Materialization + + +class Backfill(Base): # type: ignore + """ + A backfill run is linked to a materialization config, where users provide the range + (of a temporal partition) to backfill for the node. + """ + + __tablename__ = "backfill" + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + ) + + # The column reference that this backfill is defined on + materialization_id: Mapped[int] = mapped_column( + ForeignKey( + "materialization.id", + name="fk_backfill_materialization_id_materialization", + ), + ) + materialization: Mapped["Materialization"] = relationship( + back_populates="backfills", + primaryjoin="Materialization.id==Backfill.materialization_id", + ) + + # Backfilled values and range + spec: Mapped[List[PartitionBackfill]] = mapped_column( + JSON, + default=[], + ) + + urls: Mapped[Optional[List[str]]] = mapped_column( + JSON, + default=[], + ) diff --git a/datajunction-server/datajunction_server/database/base.py b/datajunction-server/datajunction_server/database/base.py new file mode 100644 index 000000000..95eb4b496 --- /dev/null +++ b/datajunction-server/datajunction_server/database/base.py @@ -0,0 +1,7 @@ +""" +SQLAlchemy base model. +""" + +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() diff --git a/datajunction-server/datajunction_server/database/catalog.py b/datajunction-server/datajunction_server/database/catalog.py new file mode 100644 index 000000000..381085a93 --- /dev/null +++ b/datajunction-server/datajunction_server/database/catalog.py @@ -0,0 +1,73 @@ +"""Catalog database schema.""" + +from datetime import datetime, timezone +from functools import partial +from typing import TYPE_CHECKING, Dict, List, Optional +from uuid import UUID, uuid4 + +from sqlalchemy import JSON, BigInteger, DateTime, ForeignKey, Integer +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy_utils import UUIDType + +from datajunction_server.database.base import Base +from datajunction_server.database.engine import Engine +from datajunction_server.typing import UTCDatetime + +if TYPE_CHECKING: + from datajunction_server.database.node import NodeRevision + + +class Catalog(Base): + """ + A catalog. + """ + + __tablename__ = "catalog" + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + ) + uuid: Mapped[UUID] = mapped_column(UUIDType(), default=uuid4) + name: Mapped[str] + engines: Mapped[List[Engine]] = relationship( + secondary="catalogengines", + primaryjoin="Catalog.id==CatalogEngines.catalog_id", + secondaryjoin="Engine.id==CatalogEngines.engine_id", + lazy="selectin", + ) + node_revisions: Mapped[List["NodeRevision"]] = relationship( + back_populates="catalog", + ) + created_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + insert_default=partial(datetime.now, timezone.utc), + ) + updated_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + insert_default=partial(datetime.now, timezone.utc), + ) + extra_params: Mapped[Optional[Dict]] = mapped_column(JSON, default={}) + + def __str__(self) -> str: + return self.name + + def __hash__(self) -> int: + return hash(self.id) + + +class CatalogEngines(Base): # type: ignore + """ + Join table for catalogs and engines. + """ + + __tablename__ = "catalogengines" + + catalog_id: Mapped[int] = mapped_column( + ForeignKey("catalog.id", name="fk_catalogengines_catalog_id_catalog"), + primary_key=True, + ) + engine_id: Mapped[int] = mapped_column( + ForeignKey("engine.id", name="fk_catalogengines_engine_id_engine"), + primary_key=True, + ) diff --git a/datajunction-server/datajunction_server/database/collection.py b/datajunction-server/datajunction_server/database/collection.py new file mode 100644 index 000000000..577219440 --- /dev/null +++ b/datajunction-server/datajunction_server/database/collection.py @@ -0,0 +1,88 @@ +"""Collection database schema.""" + +from datetime import datetime, timezone +from functools import partial +from typing import List, Optional + +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from datajunction_server.database.base import Base +from datajunction_server.database.node import Node +from datajunction_server.database.user import User +from datajunction_server.errors import DJDoesNotExistException +from datajunction_server.typing import UTCDatetime + + +class Collection(Base): + """ + A collection of nodes + """ + + __tablename__ = "collection" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[Optional[str]] = mapped_column(String, default=None, unique=True) + description: Mapped[Optional[str]] = mapped_column(String, default=None) + created_by_id: Mapped[int] = Column(Integer, ForeignKey("users.id"), nullable=False) + created_by: Mapped[User] = relationship( + "User", + back_populates="created_collections", + foreign_keys=[created_by_id], + lazy="selectin", + ) + nodes: Mapped[List[Node]] = relationship( + secondary="collectionnodes", + primaryjoin="Collection.id==CollectionNodes.collection_id", + secondaryjoin="Node.id==CollectionNodes.node_id", + lazy="selectin", + ) + created_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + insert_default=partial(datetime.now, timezone.utc), + ) + deactivated_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + nullable=True, + default=None, + ) + + def __hash__(self) -> int: + return hash(self.id) # pragma: no cover + + @classmethod + async def get_by_name( + cls, + session: AsyncSession, + name: str, + raise_if_not_exists: bool = False, + ) -> Optional["Collection"]: + """ + Get a collection by name + """ + statement = select(Collection).where(Collection.name == name) + collection = (await session.execute(statement)).scalar() + if not collection and raise_if_not_exists: + raise DJDoesNotExistException( + message=f"Collection with name `{name}` does not exist.", + http_status_code=404, + ) + return collection + + +class CollectionNodes(Base): # type: ignore + """ + Join table for collections and nodes. + """ + + __tablename__ = "collectionnodes" + + collection_id: Mapped[int] = mapped_column( + ForeignKey("collection.id", name="fk_collectionnodes_collection_id_collection"), + primary_key=True, + ) + node_id: Mapped[int] = mapped_column( + ForeignKey("node.id", name="fk_collectionnodes_node_id_node"), + primary_key=True, + ) diff --git a/datajunction-server/datajunction_server/database/column.py b/datajunction-server/datajunction_server/database/column.py new file mode 100644 index 000000000..8ea5b9a38 --- /dev/null +++ b/datajunction-server/datajunction_server/database/column.py @@ -0,0 +1,167 @@ +"""Column database schema.""" + +from typing import TYPE_CHECKING, List, Optional, Tuple + +from sqlalchemy import BigInteger, ForeignKey, Integer, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from datajunction_server.database.attributetype import ColumnAttribute +from datajunction_server.database.base import Base +from datajunction_server.models.attribute import ColumnAttributes +from datajunction_server.models.base import labelize +from datajunction_server.models.column import ColumnTypeDecorator +from datajunction_server.sql.parsing.types import ColumnType + +if TYPE_CHECKING: + from datajunction_server.database.measure import Measure + from datajunction_server.database.node import Node, NodeRevision + from datajunction_server.database.partition import Partition + + +class Column(Base): # type: ignore + """ + A column. + + Columns can be physical (associated with ``Table`` objects) or abstract (associated + with ``Node`` objects). + """ + + __tablename__ = "column" + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + ) + order: Mapped[Optional[int]] + name: Mapped[str] = mapped_column() + display_name: Mapped[Optional[str]] = mapped_column( + String, + insert_default=lambda context: labelize(context.current_parameters.get("name")), + ) + type: Mapped[Optional[ColumnType]] = mapped_column(ColumnTypeDecorator) + + dimension_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("node.id", ondelete="SET NULL", name="fk_column_dimension_id_node"), + ) + dimension: Mapped[Optional["Node"]] = relationship( + "Node", + lazy="joined", + ) + dimension_column: Mapped[Optional[str]] = mapped_column() + node_revisions: Mapped[List["NodeRevision"]] = relationship( + back_populates="columns", + secondary="nodecolumns", + lazy="selectin", + ) + attributes: Mapped[List["ColumnAttribute"]] = relationship( + back_populates="column", + lazy="joined", + cascade="all,delete", + ) + measure_id: Mapped[Optional[int]] = mapped_column( + ForeignKey( + "measures.id", + name="fk_column_measure_id_measures", + ondelete="SET NULL", + ), + ) + measure: Mapped["Measure"] = relationship(back_populates="columns") + + partition_id: Mapped[Optional[int]] = mapped_column( + ForeignKey( + "partition.id", + name="fk_column_partition_id_partition", + ondelete="SET NULL", + ), + ) + partition: Mapped["Partition"] = relationship( + lazy="joined", + primaryjoin="Column.id==Partition.column_id", + uselist=False, + cascade="all,delete", + ) + + def identifier(self) -> Tuple[str, ColumnType]: + """ + Unique identifier for this column. + """ + return self.name, self.type + + def is_dimensional(self) -> bool: + """ + Whether this column is considered dimensional + """ + return ( # pragma: no cover + self.has_dimension_attribute() + or self.has_primary_key_attribute() + or self.dimension + ) + + def has_dimension_attribute(self) -> bool: + """ + Whether the dimension attribute is set on this column. + """ + return self.has_attribute("dimension") # pragma: no cover + + def has_primary_key_attribute(self) -> bool: + """ + Whether the primary key attribute is set on this column. + """ + return self.has_attribute(ColumnAttributes.PRIMARY_KEY.value) + + def has_attribute(self, attribute_name: str) -> bool: + """ + Whether the given attribute is set on this column. + """ + return any( + attr.attribute_type.name == attribute_name for attr in self.attributes + ) + + def attribute_names(self) -> list[str]: + """ + A list of column attribute names + """ + return [attr.attribute_type.name for attr in self.attributes] + + def has_attributes_besides(self, attribute_name: str) -> bool: + """ + Whether the column has any attribute besides the one specified. + """ + return any( + attr.attribute_type.name != attribute_name for attr in self.attributes + ) + + def __hash__(self) -> int: + return hash(self.id) + + def node_revision(self) -> Optional["NodeRevision"]: + """ + Returns the most recent node revision associated with this column + """ + available_revisions = sorted(self.node_revisions, key=lambda n: n.updated_at) + return available_revisions[-1] if available_revisions else None + + def full_name(self) -> str: + """ + Full column name that includes the node it belongs to, i.e., default.hard_hat.first_name + """ + return f"{self.node_revision().name}.{self.name}" # type: ignore # pragma: no cover + + def copy(self) -> "Column": + """ + Returns a full copy of the column + """ + return Column( + order=self.order, + name=self.name, + display_name=self.display_name, + type=self.type, + dimension_id=self.dimension_id, + dimension_column=self.dimension_column, + attributes=[ + ColumnAttribute(attribute_type_id=attr.attribute_type_id) + for attr in self.attributes + ], + measure_id=self.measure_id, + partition_id=self.partition_id, + ) diff --git a/datajunction-server/datajunction_server/database/database.py b/datajunction-server/datajunction_server/database/database.py new file mode 100644 index 000000000..4f2008d6f --- /dev/null +++ b/datajunction-server/datajunction_server/database/database.py @@ -0,0 +1,129 @@ +"""Database schema""" + +from datetime import datetime, timezone +from functools import partial +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple +from uuid import UUID, uuid4 + +from sqlalchemy import JSON, BigInteger, DateTime, ForeignKey, Integer, String +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy_utils import UUIDType + +from datajunction_server.database.base import Base +from datajunction_server.typing import UTCDatetime + +if TYPE_CHECKING: + from datajunction_server.database.column import Column + + +class Database(Base): + """ + A database. + + A simple example: + + name: druid + description: An Apache Druid database + URI: druid://localhost:8082/druid/v2/sql/ + read-only: true + async_: false + cost: 1.0 + + """ + + __tablename__ = "database" + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + ) + uuid: Mapped[UUID] = mapped_column(UUIDType(), default=uuid4) + + name: Mapped[str] = mapped_column(String, unique=True) + description: Mapped[Optional[str]] = mapped_column(String, default="") + URI: Mapped[str] + extra_params: Mapped[Optional[Dict]] = mapped_column(JSON, default={}) + read_only: Mapped[bool] = mapped_column(default=True) + async_: Mapped[bool] = mapped_column(default=False, name="async") + cost: Mapped[float] = mapped_column(default=1.0) + + created_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + default=partial(datetime.now, timezone.utc), + ) + updated_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + default=partial(datetime.now, timezone.utc), + ) + + tables: Mapped[List["Table"]] = relationship( + back_populates="database", + cascade="all, delete", + ) + + def __hash__(self) -> int: + return hash(self.id) + + +class Table(Base): + """ + A table with data. + + Nodes can have data in multiple tables, in different databases. + """ + + __tablename__ = "table" + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + ) + + schema: Mapped[Optional[str]] = mapped_column(default=None, name="schema_") + table: Mapped[str] + cost: Mapped[float] = mapped_column(default=1.0) + + database_id: Mapped[int] = mapped_column( + ForeignKey("database.id", name="fk_table_database_id_database"), + ) + database: Mapped["Database"] = relationship("Database", back_populates="tables") + + columns: Mapped[List["Column"]] = relationship( + secondary="tablecolumns", + primaryjoin="Table.id==TableColumns.table_id", + secondaryjoin="Column.id==TableColumns.column_id", + cascade="all, delete", + ) + + def identifier( + self, + ) -> Tuple[Optional[str], Optional[str], str]: # pragma: no cover + """ + Unique identifier for this table. + """ + # Catalogs will soon be required and this return can be simplified + return ( + self.catalog.name if self.catalog else None, + self.schema_, + self.table, + ) + + def __hash__(self): + return hash(self.id) + + +class TableColumns(Base): + """ + Join table for table columns. + """ + + __tablename__ = "tablecolumns" + + table_id: Mapped[int] = mapped_column( + ForeignKey("table.id", name="fk_tablecolumns_table_id_table"), + primary_key=True, + ) + column_id: Mapped[int] = mapped_column( + ForeignKey("column.id", name="fk_tablecolumns_column_id_column"), + primary_key=True, + ) diff --git a/datajunction-server/datajunction_server/database/dimensionlink.py b/datajunction-server/datajunction_server/database/dimensionlink.py new file mode 100644 index 000000000..d8eb5ba28 --- /dev/null +++ b/datajunction-server/datajunction_server/database/dimensionlink.py @@ -0,0 +1,186 @@ +"""Dimension links table.""" + +from functools import cached_property +from typing import TYPE_CHECKING, Dict, List, Optional, Set + +from sqlalchemy import JSON, BigInteger, Enum, ForeignKey, Integer +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from datajunction_server.database.base import Base +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.models.dimensionlink import JoinCardinality, JoinType +from datajunction_server.utils import SEPARATOR + +if TYPE_CHECKING: + from datajunction_server.sql.parsing.backends.antlr4 import ast + + +class DimensionLink(Base): + """ + The join definition between a given node (source, dimension, or transform) + and a dimension node. + """ + + __tablename__ = "dimensionlink" + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + ) + + # A dimension node may be linked in multiple times to a given source, dimension, + # or transform node, with each link referencing a different conceptual role. + # One such example is a dimension node "default.users" that has "birth_date" and + # "registration_date" as fields. "default.users" will be linked to the "default.date" + # dimension twice, once per field, but each dimension link will have different roles. + role: Mapped[Optional[str]] + + node_revision_id: Mapped[int] = mapped_column( + ForeignKey( + "noderevision.id", + name="fk_dimensionlink_node_revision_id_noderevision", + ondelete="CASCADE", + ), + ) + node_revision: Mapped[NodeRevision] = relationship( + "NodeRevision", + foreign_keys=[node_revision_id], + back_populates="dimension_links", + ) + dimension_id: Mapped[int] = mapped_column( + ForeignKey( + "node.id", + name="fk_dimensionlink_dimension_id_node", + ondelete="CASCADE", + ), + ) + dimension: Mapped[Node] = relationship( + "Node", + foreign_keys=[dimension_id], + lazy="joined", + ) + + # SQL used to join the two nodes + join_sql: Mapped[str] + + # Metadata about the join + join_type: Mapped[Optional[JoinType]] + join_cardinality: Mapped[JoinCardinality] = mapped_column( + Enum(JoinCardinality), + default=JoinCardinality.MANY_TO_ONE, + ) + + # Additional materialization settings that are needed in order to do this join + materialization_conf: Mapped[Optional[Dict]] = mapped_column(JSON, default={}) + + @classmethod + def parse_join_type(cls, join_type: str) -> Optional[JoinType]: + """ + Parse a join type string into an enum value. + """ + join_type = join_type.strip().upper() + join_mapping = {e.name: e for e in JoinType} + for key, value in join_mapping.items(): + if key in join_type: + return value + return JoinType.LEFT # pragma: no cover + + def join_sql_ast(self) -> "ast.Query": + """ + The join query AST for this dimension link + """ + from datajunction_server.sql.parsing.backends.antlr4 import parse + + return parse( + f"select 1 from {self.node_revision.name} " + f"{self.join_type} join {self.dimension.name} " + + (f"on {self.join_sql}" if self.join_sql else ""), + ) + + def joins(self) -> List["ast.Join"]: + """ + The join ASTs for this dimension link + """ + join_sql = self.join_sql_ast() + return join_sql.select.from_.relations[-1].extensions # type: ignore + + def foreign_key_mapping(self) -> Dict["ast.Column", "ast.Column"]: + """ + If the dimension link was configured with an equality operation on the + dimension's primary key columns to a set of foreign key columns, this method + returns a mapping between the foreign keys on the node and the primary keys of + the dimension based on the join SQL. + """ + from datajunction_server.sql.parsing.backends.antlr4 import ast + + # Find equality comparions (i.e., fact.order_id = dim.order_id) + join_asts = self.joins() + equality_comparisons = ( + [ + expr + for expr in join_asts[0].criteria.on.find_all(ast.BinaryOp) # type: ignore + if expr.op == ast.BinaryOpKind.Eq + ] + if join_asts[0].criteria + else [] + ) + mapping = {} + for comp in equality_comparisons: + if isinstance(comp.left, ast.Column) and isinstance( + comp.right, + ast.Column, + ): # pragma: no cover + node_left = comp.left.name.namespace.identifier() # type: ignore + node_right = comp.right.name.namespace.identifier() # type: ignore + if node_left == self.node_revision.name: # pragma: no cover + mapping[comp.right] = comp.left + if node_right == self.node_revision.name: # pragma: no cover + mapping[comp.left] = comp.right # pragma: no cover + return mapping + + @hybrid_property + def foreign_keys(self) -> Dict[str, str | None]: + """ + Returns a mapping from the foreign key column(s) on the origin node to + the primary key column(s) on the dimension node. The dict values are column names. + """ + from datajunction_server.sql.parsing.backends.antlr4 import ast + + # Build equality operator-based mappings + join_asts = self.joins() + mapping: dict[str, str | None] = { + right.identifier(): left.identifier() + for left, right in self.foreign_key_mapping().items() + } + + # Add remaining foreign key references without an equality comparison + columns = [col.identifier() for col in join_asts[0].find_all(ast.Column)] + foreign_key_refs = [ + col for col in columns if col.startswith(self.node_revision.name) + ] + for foreign_key in foreign_key_refs: + if foreign_key not in mapping: + mapping[foreign_key] = None + return mapping + + @hybrid_property + def foreign_key_column_names(self) -> Set[str]: + """ + Returns a set of foreign key column names + """ + return { + fk.replace(f"{self.node_revision.name}{SEPARATOR}", "") + for fk in self.foreign_keys.keys() + } + + @cached_property + def foreign_keys_reversed(self): + """ + Returns a mapping from the primary key column(s) on the dimension node to the + foreign key column(s) on the origin node. The dict values are column names. + """ + return { + left.identifier(): right.identifier() + for left, right in self.foreign_key_mapping().items() + } diff --git a/datajunction-server/datajunction_server/database/engine.py b/datajunction-server/datajunction_server/database/engine.py new file mode 100644 index 000000000..5cc11bd21 --- /dev/null +++ b/datajunction-server/datajunction_server/database/engine.py @@ -0,0 +1,29 @@ +"""Engine database schema.""" + +from typing import Optional + +import sqlalchemy as sa +from sqlalchemy import Enum +from sqlalchemy.orm import Mapped, mapped_column + +from datajunction_server.database.base import Base +from datajunction_server.models.engine import Dialect + + +class Engine(Base): + """ + A query engine. + """ + + __tablename__ = "engine" + + id: Mapped[int] = mapped_column( + sa.BigInteger().with_variant(sa.Integer, "sqlite"), + primary_key=True, + ) + name: Mapped[str] + version: Mapped[str] + uri: Mapped[Optional[str]] + dialect: Mapped[Optional[Dialect]] = mapped_column( + Enum(Dialect), + ) diff --git a/datajunction-server/datajunction_server/database/history.py b/datajunction-server/datajunction_server/database/history.py new file mode 100644 index 000000000..6a5901e2d --- /dev/null +++ b/datajunction-server/datajunction_server/database/history.py @@ -0,0 +1,86 @@ +"""History database schema.""" + +from datetime import datetime, timezone +from functools import partial +from typing import Any, Dict, Optional + +from sqlalchemy import JSON, BigInteger, DateTime, Enum, Index, Integer, String +from sqlalchemy.orm import Mapped, mapped_column + +from datajunction_server.database.base import Base +from datajunction_server.enum import StrEnum +from datajunction_server.typing import UTCDatetime + + +class ActivityType(StrEnum): + """ + An activity type + """ + + CREATE = "create" + DELETE = "delete" + RESTORE = "restore" + UPDATE = "update" + REFRESH = "refresh" + TAG = "tag" + SET_ATTRIBUTE = "set_attribute" + STATUS_CHANGE = "status_change" + + +class EntityType(StrEnum): + """ + An entity type for which activity can occur + """ + + ATTRIBUTE = "attribute" + AVAILABILITY = "availability" + BACKFILL = "backfill" + CATALOG = "catalog" + COLUMN_ATTRIBUTE = "column_attribute" + DEPENDENCY = "dependency" + ENGINE = "engine" + LINK = "link" + MATERIALIZATION = "materialization" + NAMESPACE = "namespace" + NODE = "node" + PARTITION = "partition" + QUERY = "query" + TAG = "tag" + + +class History(Base): + """ + An event to store as part of the server's activity history + """ + + __tablename__ = "history" + __table_args__ = ( + Index("ix_history_entity_name", "entity_name", postgresql_using="btree"), + Index("ix_history_user", "user", postgresql_using="btree"), + ) + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + ) + entity_type: Mapped[Optional[EntityType]] = mapped_column( + Enum(EntityType), + default=None, + ) + entity_name: Mapped[Optional[str]] = mapped_column(String, default=None) + node: Mapped[Optional[str]] = mapped_column(String, default=None) + activity_type: Mapped[Optional[ActivityType]] = mapped_column( + Enum(ActivityType), + default=None, + ) + user: Mapped[Optional[str]] = mapped_column(String, default=None) + pre: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, default=dict) + post: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, default=dict) + details: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, default=dict) + created_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + insert_default=partial(datetime.now, timezone.utc), + ) + + def __hash__(self) -> int: + return hash(self.id) diff --git a/datajunction-server/datajunction_server/database/materialization.py b/datajunction-server/datajunction_server/database/materialization.py new file mode 100644 index 000000000..87268200c --- /dev/null +++ b/datajunction-server/datajunction_server/database/materialization.py @@ -0,0 +1,135 @@ +"""Materialization database schema.""" + +from typing import TYPE_CHECKING, List, Optional, Union + +import sqlalchemy as sa +from sqlalchemy import ( + JSON, + DateTime, + Enum, + ForeignKey, + String, + UniqueConstraint, + and_, + select, +) +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Mapped, joinedload, mapped_column, relationship + +from datajunction_server.database.backfill import Backfill +from datajunction_server.database.base import Base +from datajunction_server.database.column import Column +from datajunction_server.database.availabilitystate import AvailabilityState +from datajunction_server.models.materialization import ( + DruidMeasuresCubeConfig, + GenericMaterializationConfig, + MaterializationStrategy, +) +from datajunction_server.typing import UTCDatetime + +if TYPE_CHECKING: + from datajunction_server.database.node import NodeRevision + + +class Materialization(Base): + """ + Materialization configured for a node. + """ + + __tablename__ = "materialization" + __table_args__ = ( + UniqueConstraint( + "name", + "node_revision_id", + name="name_node_revision_uniq", + ), + ) + + id: Mapped[int] = mapped_column( + sa.BigInteger().with_variant(sa.Integer, "sqlite"), + primary_key=True, + autoincrement=True, + ) + + node_revision_id: Mapped[int] = mapped_column( + ForeignKey( + "noderevision.id", + name="fk_materialization_node_revision_id_noderevision", + ), + ) + node_revision: Mapped["NodeRevision"] = relationship( + "NodeRevision", + back_populates="materializations", + ) + + name: Mapped[str] + + strategy: Mapped[Optional[MaterializationStrategy]] = mapped_column( + Enum(MaterializationStrategy), + ) + + # A cron schedule to materialize this node by + schedule: Mapped[str] + + # Arbitrary config relevant to the materialization job + config: Mapped[Union[GenericMaterializationConfig, DruidMeasuresCubeConfig]] = ( + mapped_column( + JSON, + default={}, + ) + ) + + # The name of the plugin that handles materialization, if any + job: Mapped[str] = mapped_column( + String, + default="MaterializationJob", + ) + + deactivated_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + nullable=True, + default=None, + ) + + backfills: Mapped[List[Backfill]] = relationship( + back_populates="materialization", + primaryjoin="Materialization.id==Backfill.materialization_id", + cascade="all, delete", + lazy="selectin", + ) + + availability: Mapped[List[AvailabilityState]] = relationship( + back_populates="materialization", + primaryjoin="Materialization.id==AvailabilityState.materialization_id", + cascade="all, delete", + lazy="selectin", + ) + + @classmethod + async def get_by_names( + cls, + session: AsyncSession, + node_revision_id: int, + materialization_names: List[str], + ) -> List["Materialization"]: + """ + Get materializations by name and node revision id. + """ + from datajunction_server.database.node import NodeRevision + + statement = ( + select(cls) + .where( + and_( + cls.name.in_(materialization_names), + cls.node_revision_id == node_revision_id, + ), + ) + .options( + joinedload(cls.node_revision).options( + joinedload(NodeRevision.columns).joinedload(Column.partition), + ), + ) + ) + result = await session.execute(statement) + return result.unique().scalars().all() diff --git a/datajunction-server/datajunction_server/database/measure.py b/datajunction-server/datajunction_server/database/measure.py new file mode 100644 index 000000000..7c99f48be --- /dev/null +++ b/datajunction-server/datajunction_server/database/measure.py @@ -0,0 +1,44 @@ +"""Measure database schema.""" + +from typing import List, Optional + +from sqlalchemy import BigInteger, Enum, Integer, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from datajunction_server.database.base import Base +from datajunction_server.database.column import Column +from datajunction_server.models.base import labelize +from datajunction_server.models.measure import AggregationRule + + +class Measure(Base): # type: ignore + """ + Measure class. + + Measure is a basic data modelling concept that helps with making Metric nodes portable, + that is, so they can be computed on various DJ nodes using the same Metric definitions. + + By default, if a node column is not a Dimension or Dimension attribute then it should + be a Measure. + """ + + __tablename__ = "measures" + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + ) + name: Mapped[str] = mapped_column(unique=True) + display_name: Mapped[Optional[str]] = mapped_column( + String, + insert_default=lambda context: labelize(context.current_parameters.get("name")), + ) + description: Mapped[Optional[str]] + columns: Mapped[List["Column"]] = relationship( + back_populates="measure", + lazy="joined", + ) + additive: Mapped[AggregationRule] = mapped_column( + Enum(AggregationRule), + default=AggregationRule.NON_ADDITIVE, + ) diff --git a/datajunction-server/datajunction_server/database/metricmetadata.py b/datajunction-server/datajunction_server/database/metricmetadata.py new file mode 100644 index 000000000..4d70fda2e --- /dev/null +++ b/datajunction-server/datajunction_server/database/metricmetadata.py @@ -0,0 +1,48 @@ +"""Metric metadata database schema.""" + +from typing import Optional + +import sqlalchemy as sa +from sqlalchemy import Enum +from sqlalchemy.orm import Mapped, mapped_column + +from datajunction_server.database.base import Base +from datajunction_server.models.node import ( + MetricDirection, + MetricMetadataInput, + MetricUnit, +) + + +class MetricMetadata(Base): + """ + Additional metric metadata + """ + + __tablename__ = "metricmetadata" + + id: Mapped[int] = mapped_column( + sa.BigInteger().with_variant(sa.Integer, "sqlite"), + primary_key=True, + ) + + direction: Mapped[Optional[MetricDirection]] = mapped_column( + Enum(MetricDirection), + default=MetricDirection.NEUTRAL, + nullable=True, + ) + unit: Mapped[Optional[MetricUnit]] = mapped_column( + Enum(MetricUnit), + default=MetricUnit.UNKNOWN, + nullable=True, + ) + + @classmethod + def from_input(cls, input_data: "MetricMetadataInput") -> "MetricMetadata": + """ + Parses a MetricMetadataInput object to a MetricMetadata object + """ + return MetricMetadata( + direction=input_data.direction, + unit=MetricUnit[input_data.unit.upper()] if input_data.unit else None, + ) diff --git a/datajunction-server/datajunction_server/database/namespace.py b/datajunction-server/datajunction_server/database/namespace.py new file mode 100644 index 000000000..9282dc7cf --- /dev/null +++ b/datajunction-server/datajunction_server/database/namespace.py @@ -0,0 +1,176 @@ +"""Namespace database schema.""" + +from typing import List, Optional + +from sqlalchemy import DateTime, func, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Mapped, joinedload, load_only, mapped_column, selectinload +from sqlalchemy.sql.operators import is_, or_ + +from datajunction_server.database.base import Base +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.errors import DJDoesNotExistException +from datajunction_server.models.node import NodeMinimumDetail +from datajunction_server.models.node_type import NodeType +from datajunction_server.sql.dag import _node_output_options +from datajunction_server.typing import UTCDatetime + + +class NodeNamespace(Base): + """ + A node namespace + """ + + __tablename__ = "nodenamespace" + + namespace: Mapped[str] = mapped_column( + nullable=False, + unique=True, + primary_key=True, + ) + deactivated_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + nullable=True, + default=None, + ) + + @classmethod + async def get_all_with_node_count(cls, session: AsyncSession): + """ + Get all namespaces with the number of nodes in that namespaces. + """ + statement = ( + select( + NodeNamespace.namespace, + func.count(Node.id).label("num_nodes"), + ) + .join( + Node, + onclause=NodeNamespace.namespace == Node.namespace, + isouter=True, + ) + .where( + is_(NodeNamespace.deactivated_at, None), + ) + .group_by(NodeNamespace.namespace) + ) + result = await session.execute(statement) + return result.all() + + @classmethod + async def get( + cls, + session: AsyncSession, + namespace: str, + raise_if_not_exists: bool = True, + ) -> Optional["NodeNamespace"]: + """ + List node names in namespace. + """ + statement = select(cls).where(cls.namespace == namespace) + results = await session.execute(statement) + node_namespace = results.scalar_one_or_none() + if raise_if_not_exists: # pragma: no cover + if not node_namespace: + raise DJDoesNotExistException( + message=(f"node namespace `{namespace}` does not exist."), + http_status_code=404, + ) + return node_namespace + + @classmethod + async def list_nodes( + cls, + session: AsyncSession, + namespace: str, + node_type: Optional[NodeType] = None, + include_deactivated: bool = False, + with_edited_by: bool = False, + ) -> List["NodeMinimumDetail"]: + """ + List node names in namespace. + """ + await cls.get(session, namespace) + + list_nodes_query = ( + select(Node) + .where( + or_( + Node.namespace.like(f"{namespace}.%"), + Node.namespace == namespace, + ), + Node.type == node_type if node_type else True, + ) + .options( + load_only( + Node.name, + Node.type, + Node.current_version, + ), + joinedload(Node.current).options( + load_only( + NodeRevision.display_name, + NodeRevision.description, + NodeRevision.status, + NodeRevision.mode, + NodeRevision.updated_at, + ), + ), + selectinload(Node.tags), + *([selectinload(Node.history)] if with_edited_by else []), + ) + ) + if include_deactivated is False: + list_nodes_query = list_nodes_query.where(is_(Node.deactivated_at, None)) + + result = await session.execute(list_nodes_query) + return [ + NodeMinimumDetail( + name=row.name, + display_name=row.current.display_name, + description=row.current.description, + version=row.current_version, + type=row.type, + status=row.current.status, + mode=row.current.mode, + updated_at=row.current.updated_at, + tags=row.tags, + edited_by=( + None + if not with_edited_by + else list({entry.user for entry in row.history if entry.user}) + ), + ) + for row in result.unique().scalars().all() + ] + + @classmethod + async def list_all_nodes( + cls, + session: AsyncSession, + namespace: str, + include_deactivated: bool = False, + ) -> List["Node"]: + """ + List all nodes in the namespace. + """ + await cls.get(session, namespace) + + list_nodes_query = ( + select(Node) + .where( + or_( + Node.namespace.like(f"{namespace}.%"), + Node.namespace == namespace, + ), + ) + .options( + *_node_output_options(), + ) + ) + if include_deactivated is False: # pragma: no cover + list_nodes_query = list_nodes_query.where(is_(Node.deactivated_at, None)) + + result = await session.execute(list_nodes_query) + nodes = result.unique().scalars().all() + return nodes diff --git a/datajunction-server/datajunction_server/database/node.py b/datajunction-server/datajunction_server/database/node.py new file mode 100644 index 000000000..20babae0e --- /dev/null +++ b/datajunction-server/datajunction_server/database/node.py @@ -0,0 +1,987 @@ +"""Node database schema.""" + +import pickle +import zlib +from datetime import datetime, timezone +from functools import partial +from http import HTTPStatus +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple + +import sqlalchemy as sa +from pydantic import Extra +from sqlalchemy import JSON +from sqlalchemy import Column as SqlalchemyColumn +from sqlalchemy import ( + DateTime, + Enum, + ForeignKey, + Index, + Integer, + LargeBinary, + String, + TypeDecorator, + UniqueConstraint, + select, +) +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import Mapped, joinedload, mapped_column, relationship, selectinload +from sqlalchemy.sql.base import ExecutableOption +from sqlalchemy.sql.operators import is_ + +from datajunction_server.database.attributetype import ColumnAttribute +from datajunction_server.database.availabilitystate import AvailabilityState +from datajunction_server.database.base import Base +from datajunction_server.database.catalog import Catalog +from datajunction_server.database.column import Column +from datajunction_server.database.history import History +from datajunction_server.database.materialization import Materialization +from datajunction_server.database.metricmetadata import MetricMetadata +from datajunction_server.database.tag import Tag +from datajunction_server.database.user import User +from datajunction_server.errors import ( + DJInvalidInputException, + DJInvalidMetricQueryException, + DJNodeNotFound, +) +from datajunction_server.models.base import labelize +from datajunction_server.models.node import ( + DEFAULT_DRAFT_VERSION, + BuildCriteria, + NodeCursor, + NodeMode, + NodeStatus, +) +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.partition import PartitionType +from datajunction_server.naming import amenable_name +from datajunction_server.typing import UTCDatetime +from datajunction_server.utils import SEPARATOR + +if TYPE_CHECKING: + from datajunction_server.database.dimensionlink import DimensionLink + + +class NodeRelationship(Base): + """ + Join table for self-referential many-to-many relationships between nodes. + """ + + __tablename__ = "noderelationship" + + parent_id: Mapped[int] = mapped_column( + ForeignKey("node.id", name="fk_noderelationship_parent_id_node"), + primary_key=True, + ) + + # This will default to `latest`, which points to the current version of the node, + # or it can be a specific version. + parent_version: Mapped[Optional[str]] = mapped_column(default="latest") + + child_id: Mapped[int] = mapped_column( + ForeignKey("noderevision.id", name="fk_noderelationship_child_id_noderevision"), + primary_key=True, + ) + + +class CubeRelationship(Base): + """ + Join table for many-to-many relationships between cube nodes and metric/dimension nodes. + """ + + __tablename__ = "cube" + + cube_id: Mapped[int] = mapped_column( + ForeignKey("noderevision.id", name="fk_cube_cube_id_noderevision"), + primary_key=True, + ) + + cube_element_id: Mapped[int] = mapped_column( + ForeignKey("column.id", name="fk_cube_cube_element_id_column"), + primary_key=True, + ) + + +class BoundDimensionsRelationship(Base): + """ + Join table for many-to-many relationships between metric nodes + and parent nodes for dimensions that are required. + """ + + __tablename__ = "metric_required_dimensions" + + metric_id: Mapped[int] = mapped_column( + ForeignKey( + "noderevision.id", + name="fk_metric_required_dimensions_metric_id_noderevision", + ), + primary_key=True, + ) + + bound_dimension_id: Mapped[int] = mapped_column( + ForeignKey( + "column.id", + name="fk_metric_required_dimensions_bound_dimension_id_column", + ), + primary_key=True, + ) + + +class MissingParent(Base): + """ + A missing parent node + """ + + __tablename__ = "missingparent" + + id: Mapped[int] = mapped_column( + sa.BigInteger().with_variant(sa.Integer, "sqlite"), + primary_key=True, + ) + name: Mapped[str] = mapped_column(String) + created_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + default=partial(datetime.now, timezone.utc), + ) + + +class NodeMissingParents(Base): + """ + Join table for missing parents + """ + + __tablename__ = "nodemissingparents" + + missing_parent_id: Mapped[int] = mapped_column( + ForeignKey( + "missingparent.id", + name="fk_nodemissingparents_missing_parent_id_missingparent", + ), + primary_key=True, + ) + referencing_node_id: Mapped[int] = mapped_column( + ForeignKey( + "noderevision.id", + name="fk_nodemissingparents_referencing_node_id_noderevision", + ), + primary_key=True, + ) + + +class Node(Base): + """ + Node that acts as an umbrella for all node revisions + """ + + __tablename__ = "node" + __table_args__ = ( + UniqueConstraint("name", "namespace", name="unique_node_namespace_name"), + Index("cursor_index", "created_at", "id", postgresql_using="btree"), + Index( + "namespace_index", + "namespace", + postgresql_using="btree", + postgresql_ops={"identifier": "varchar_pattern_ops"}, + ), + ) + + id: Mapped[int] = mapped_column( + sa.BigInteger().with_variant(sa.Integer, "sqlite"), + primary_key=True, + ) + name: Mapped[str] = mapped_column(String, unique=True) + type: Mapped[NodeType] = mapped_column(Enum(NodeType)) + display_name: Mapped[Optional[str]] + created_by_id: int = SqlalchemyColumn( + Integer, + ForeignKey("users.id"), + nullable=False, + ) + + created_by: Mapped[User] = relationship( + "User", + back_populates="created_nodes", + foreign_keys=[created_by_id], + lazy="selectin", + ) + namespace: Mapped[str] = mapped_column(String, default="default") + current_version: Mapped[str] = mapped_column( + String, + default=str(DEFAULT_DRAFT_VERSION), + ) + created_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + insert_default=partial(datetime.now, timezone.utc), + ) + deactivated_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + nullable=True, + default=None, + ) + + revisions: Mapped[List["NodeRevision"]] = relationship( + "NodeRevision", + back_populates="node", + primaryjoin="Node.id==NodeRevision.node_id", + cascade="all,delete", + order_by="NodeRevision.updated_at", + ) + current: Mapped["NodeRevision"] = relationship( + "NodeRevision", + primaryjoin=( + "and_(Node.id==NodeRevision.node_id, " + "Node.current_version == NodeRevision.version)" + ), + viewonly=True, + uselist=False, + ) + + children: Mapped[List["NodeRevision"]] = relationship( + back_populates="parents", + secondary="noderelationship", + primaryjoin="Node.id==NodeRelationship.parent_id", + secondaryjoin="NodeRevision.id==NodeRelationship.child_id", + order_by="NodeRevision.id", + ) + + tags: Mapped[List["Tag"]] = relationship( + back_populates="nodes", + secondary="tagnoderelationship", + primaryjoin="TagNodeRelationship.node_id==Node.id", + secondaryjoin="TagNodeRelationship.tag_id==Tag.id", + ) + + missing_table: Mapped[bool] = mapped_column(sa.Boolean, default=False) + + history: Mapped[List[History]] = relationship( + primaryjoin="History.entity_name==Node.name", + order_by="History.created_at", + foreign_keys="History.entity_name", + ) + + def __hash__(self) -> int: + return hash(self.id) + + @hybrid_property + def edited_by(self) -> List[str]: + """ + Editors of the node + """ + return list( # pragma: no cover + {entry.user for entry in self.history if entry.user}, + ) + + @classmethod + async def get_by_name( + cls, + session: AsyncSession, + name: str, + options: List[ExecutableOption] = None, + raise_if_not_exists: bool = False, + include_inactive: bool = False, + for_update: bool = False, + ) -> Optional["Node"]: + """ + Get a node by name + """ + statement = select(Node).where(Node.name == name) + options = options or [ + joinedload(Node.current).options( + *NodeRevision.default_load_options(), + ), + selectinload(Node.tags), + selectinload(Node.created_by), + ] + statement = statement.options(*options) + if not include_inactive: + statement = statement.where(is_(Node.deactivated_at, None)) + if for_update: + statement = statement.with_for_update().execution_options( + populate_existing=True, + ) + result = await session.execute(statement) + node = result.unique().scalar_one_or_none() + if not node and raise_if_not_exists: + raise DJNodeNotFound( + message=(f"A node with name `{name}` does not exist."), + http_status_code=404, + ) + return node + + @classmethod + async def get_by_names( + cls, + session: AsyncSession, + names: List[str], + options: List[ExecutableOption] = None, + include_inactive: bool = False, + ) -> List["Node"]: + """ + Get a node by name + """ + statement = select(Node).where(Node.name.in_(names)) + options = options or [ + joinedload(Node.current).options( + *NodeRevision.default_load_options(), + ), + selectinload(Node.tags), + ] + statement = statement.options(*options) + if not include_inactive: # pragma: no cover + statement = statement.where(is_(Node.deactivated_at, None)) + result = await session.execute(statement) + nodes = result.unique().scalars().all() + return nodes + + @classmethod + async def get_cube_by_name( + cls, + session: AsyncSession, + name: str, + ) -> Optional["Node"]: + """ + Get a cube by name + """ + statement = ( + select(Node) + .where(Node.name == name) + .options( + joinedload(Node.current).options( + selectinload(NodeRevision.availability), + selectinload(NodeRevision.columns), + selectinload(NodeRevision.catalog).selectinload(Catalog.engines), + selectinload(NodeRevision.materializations).joinedload( + Materialization.backfills, + ), + selectinload(NodeRevision.cube_elements) + .selectinload(Column.node_revisions) + .options( + selectinload(NodeRevision.node), + ), + ), + joinedload(Node.tags), + ) + ) + result = await session.execute(statement) + node = result.unique().scalar_one_or_none() + return node + + @classmethod + async def get_by_id( + cls, + session: AsyncSession, + node_id: int, + *options: ExecutableOption, + ) -> Optional["Node"]: + """ + Get a node by id + """ + statement = ( + select(Node).where(Node.id == node_id).options(*options) + ) # pragma: no cover + result = await session.execute(statement) # pragma: no cover + node = result.unique().scalar_one_or_none() # pragma: no cover + return node # pragma: no cover + + @classmethod + async def find( + cls, + session: AsyncSession, + prefix: Optional[str] = None, + node_type: Optional[NodeType] = None, + *options: ExecutableOption, + ) -> List["Node"]: + """ + Finds a list of nodes by prefix + """ + statement = select(Node).where(is_(Node.deactivated_at, None)) + if prefix: + statement = statement.where( + Node.name.like(f"{prefix}%"), # type: ignore + ) + if node_type: + statement = statement.where(Node.type == node_type) + result = await session.execute(statement.options(*options)) + return result.unique().scalars().all() + + @classmethod + async def find_by( + cls, + session: AsyncSession, + names: Optional[List[str]] = None, + fragment: Optional[str] = None, + node_types: Optional[List[NodeType]] = None, + tags: Optional[List[str]] = None, + edited_by: Optional[str] = None, + namespace: Optional[str] = None, + limit: Optional[int] = 100, + before: Optional[str] = None, + after: Optional[str] = None, + options: list[ExecutableOption] = None, + ) -> List["Node"]: + """ + Finds a list of nodes by prefix + """ + nodes_with_tags = [] + if tags: + statement = ( + select(Tag).where(Tag.name.in_(tags)).options(joinedload(Tag.nodes)) + ) + nodes_with_tags = [ + node.id + for tag in (await session.execute(statement)).unique().scalars().all() + for node in tag.nodes + ] + if not nodes_with_tags: # pragma: no cover + return [] + + statement = select(Node).where(is_(Node.deactivated_at, None)) + if namespace: + statement = statement.where( + (Node.namespace.like(f"{namespace}.%")) | (Node.namespace == namespace), + ) + if nodes_with_tags: + statement = statement.where( + Node.id.in_(nodes_with_tags), + ) # pragma: no cover + if names: + statement = statement.where( + Node.name.in_(names), # type: ignore + ) + if fragment: + statement = statement.where( + Node.name.like(f"%{fragment}%"), # type: ignore + ) + if node_types: + statement = statement.where(Node.type.in_(node_types)) + if edited_by: + edited_node_subquery = ( + select(History.entity_name) + .where((History.user == edited_by)) + .distinct() + .subquery() + ) + + statement = statement.join( + edited_node_subquery, + onclause=(edited_node_subquery.c.entity_name == Node.name), + ).distinct() + + if after: + cursor = NodeCursor.decode(after) + statement = statement.where( + (Node.created_at, Node.id) <= (cursor.created_at, cursor.id), + ).order_by(Node.created_at.desc(), Node.id.desc()) + elif before: + cursor = NodeCursor.decode(before) + statement = statement.where( + (Node.created_at, Node.id) >= (cursor.created_at, cursor.id), + ) + statement = statement.order_by(Node.created_at.asc(), Node.id.asc()) + else: + statement = statement.order_by(Node.created_at.desc(), Node.id.desc()) + + limit = limit if limit and limit > 0 else 100 + statement = statement.limit(limit) + result = await session.execute(statement.options(*(options or []))) + nodes = result.unique().scalars().all() + + # Reverse for backward pagination + if before: + nodes.reverse() + return nodes + + +class CompressedPickleType(TypeDecorator): + """ + A SQLAlchemy type for storing zlib-compressed pickled objects. + """ + + impl = LargeBinary + python_type = object + + def __init__(self, *args, protocol=pickle.HIGHEST_PROTOCOL, **kwargs): + super().__init__(*args, **kwargs) + self.protocol = protocol + + def process_bind_param(self, value, dialect): + """ + Serialize and compress the Python object before storing it in the database. + """ + if value is None: + return None + return zlib.compress( # pragma: no cover + pickle.dumps(value, protocol=self.protocol), + ) + + def process_result_value(self, value, dialect): + """ + Decompress and deserialize the stored value into a Python object. + """ + if value is None: + return None + try: # pragma: no cover + return pickle.loads(zlib.decompress(value)) # pragma: no cover + except TypeError: # pragma: no cover + return None + + def process_literal_param(self, value, dialect): + """Convert the value to a literal for SQL statements.""" + if value is not None: # pragma: no cover + # Convert the value to a compressed and pickled representation + compressed_value = zlib.compress(pickle.dumps(value)) + return compressed_value.hex() # Convert binary to a safe literal format + return None # pragma: no cover + + +class NodeRevision( + Base, +): + """ + A node revision. + """ + + __tablename__ = "noderevision" + __table_args__ = (UniqueConstraint("version", "node_id"),) + + id: Mapped[int] = mapped_column( + sa.BigInteger().with_variant(sa.Integer, "sqlite"), + primary_key=True, + ) + + name: Mapped[str] = mapped_column(unique=False) + + display_name: Mapped[Optional[str]] = mapped_column( + String, + insert_default=lambda context: labelize(context.current_parameters.get("name")), + ) + type: Mapped[NodeType] = mapped_column(Enum(NodeType)) + description: Mapped[str] = mapped_column(String, default="") + created_by_id: int = SqlalchemyColumn( + Integer, + ForeignKey("users.id"), + nullable=False, + ) + created_by: Mapped[User] = relationship( + "User", + back_populates="created_node_revisions", + foreign_keys=[created_by_id], + lazy="selectin", + ) + query: Mapped[Optional[str]] = mapped_column(String) + mode: Mapped[NodeMode] = mapped_column( + Enum(NodeMode), + default=NodeMode.PUBLISHED, + ) + + version: Mapped[Optional[str]] = mapped_column( + String, + default=str(DEFAULT_DRAFT_VERSION), + ) + node_id: Mapped[int] = mapped_column( + ForeignKey("node.id", name="fk_noderevision_node_id_node"), + ) + node: Mapped[Node] = relationship( + "Node", + back_populates="revisions", + foreign_keys=[node_id], + lazy="selectin", + ) + catalog_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("catalog.id", name="fk_noderevision_catalog_id_catalog"), + ) + catalog: Mapped[Optional[Catalog]] = relationship( + "Catalog", + back_populates="node_revisions", + lazy="joined", + ) + schema_: Mapped[Optional[str]] = mapped_column(String, default=None) + table: Mapped[Optional[str]] = mapped_column(String, default=None) + + # A list of columns from the metric's parent that + # are required for grouping when using the metric + required_dimensions: Mapped[List["Column"]] = relationship( + secondary="metric_required_dimensions", + primaryjoin="NodeRevision.id==BoundDimensionsRelationship.metric_id", + secondaryjoin="Column.id==BoundDimensionsRelationship.bound_dimension_id", + ) + + metric_metadata_id: Mapped[Optional[int]] = mapped_column( + ForeignKey( + "metricmetadata.id", + name="fk_noderevision_metric_metadata_id_metricmetadata", + ), + ) + metric_metadata: Mapped[Optional[MetricMetadata]] = relationship( + primaryjoin="NodeRevision.metric_metadata_id==MetricMetadata.id", + cascade="all, delete", + uselist=False, + ) + + # A list of metric columns and dimension columns, only used by cube nodes + cube_elements: Mapped[List["Column"]] = relationship( + secondary="cube", + primaryjoin="NodeRevision.id==CubeRelationship.cube_id", + secondaryjoin="Column.id==CubeRelationship.cube_element_id", + lazy="joined", + order_by="Column.order", + ) + + status: Mapped[NodeStatus] = mapped_column( + Enum(NodeStatus), + default=NodeStatus.INVALID, + ) + updated_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + insert_default=partial(datetime.now, timezone.utc), + ) + + parents: Mapped[List["Node"]] = relationship( + back_populates="children", + secondary="noderelationship", + primaryjoin="NodeRevision.id==NodeRelationship.child_id", + secondaryjoin="Node.id==NodeRelationship.parent_id", + ) + + missing_parents: Mapped[List[MissingParent]] = relationship( + secondary="nodemissingparents", + primaryjoin="NodeRevision.id==NodeMissingParents.referencing_node_id", + secondaryjoin="MissingParent.id==NodeMissingParents.missing_parent_id", + cascade="all, delete", + ) + + columns: Mapped[List["Column"]] = relationship( + secondary="nodecolumns", + primaryjoin="NodeRevision.id==NodeColumns.node_id", + secondaryjoin="Column.id==NodeColumns.column_id", + cascade="all, delete", + order_by="Column.order", + ) + + dimension_links: Mapped[List["DimensionLink"]] = relationship( + back_populates="node_revision", + cascade="all, delete", + order_by="DimensionLink.id", + ) + + # The availability of materialized data needs to be stored on the NodeRevision + # level in order to support pinned versions, where a node owner wants to pin + # to a particular upstream node version. + availability: Mapped[Optional[AvailabilityState]] = relationship( + secondary="nodeavailabilitystate", + primaryjoin="NodeRevision.id==NodeAvailabilityState.node_id", + secondaryjoin="AvailabilityState.id==NodeAvailabilityState.availability_id", + cascade="all, delete", + uselist=False, + ) + + # Nodes of type SOURCE will not have this property as their materialization + # is not managed as a part of this service + materializations: Mapped[List["Materialization"]] = relationship( + back_populates="node_revision", + cascade="all, delete-orphan", + ) + + lineage: Mapped[Optional[List[Dict]]] = mapped_column( + JSON, + default=[], + ) + + query_ast: Mapped[CompressedPickleType | None] = mapped_column( + CompressedPickleType, + default=None, + ) + + custom_metadata: Mapped[Optional[Dict]] = mapped_column( + JSON, + default={}, + ) + + def __hash__(self) -> int: + return hash(self.id) + + def primary_key(self) -> List[Column]: + """ + Returns the primary key columns of this node. + """ + primary_key_columns = [] + for col in self.columns: + if col.has_primary_key_attribute(): + primary_key_columns.append(col) + return primary_key_columns + + @classmethod + def default_load_options(cls): + """ + Default options when loading a node + """ + from datajunction_server.database.dimensionlink import DimensionLink + + return ( + selectinload(NodeRevision.columns).options( + joinedload(Column.attributes).joinedload( + ColumnAttribute.attribute_type, + ), + joinedload(Column.dimension), + joinedload(Column.partition), + ), + joinedload(NodeRevision.catalog), + selectinload(NodeRevision.parents), + selectinload(NodeRevision.materializations), + selectinload(NodeRevision.metric_metadata), + selectinload(NodeRevision.availability), + selectinload(NodeRevision.dimension_links).options( + joinedload(DimensionLink.dimension).options( + selectinload(Node.current), + ), + joinedload(DimensionLink.node_revision), + ), + selectinload(NodeRevision.required_dimensions), + selectinload(NodeRevision.availability), + ) + + @staticmethod + def format_metric_alias(query: str, name: str) -> str: + """ + Return a metric query with the metric aliases reassigned to + have the same name as the node, if they aren't already matching. + """ + from datajunction_server.sql.parsing import ast + from datajunction_server.sql.parsing.backends.antlr4 import parse + + tree = parse(query) + projection_0 = tree.select.projection[0] + tree.select.projection[0] = projection_0.set_alias( + ast.Name(amenable_name(name)), + ) + return str(tree) + + def check_metric(self): + """ + Check if the Node defines a metric. + + The Node SQL query should have a single expression in its + projections and it should be an aggregation function. + """ + from datajunction_server.sql.parsing.backends.antlr4 import parse + + # must have a single expression + tree = parse(self.query) + if len(tree.select.projection) != 1: + raise DJInvalidInputException( + http_status_code=HTTPStatus.BAD_REQUEST, + message="Metric queries can only have a single " + f"expression, found {len(tree.select.projection)}", + ) + projection_0 = tree.select.projection[0] + + # must have an aggregation + if ( + not hasattr(projection_0, "is_aggregation") + or not projection_0.is_aggregation() # type: ignore + ): + raise DJInvalidMetricQueryException( + f"Metric {self.name} has an invalid query, should have an aggregate expression", + ) + + if tree.select.where: + raise DJInvalidMetricQueryException( + "Metric cannot have a WHERE clause. Please use IF(, ...) instead", + ) + + clauses = [ + "GROUP BY" if tree.select.group_by else None, + "HAVING" if tree.select.having else None, + "LATERAL VIEW" if tree.select.lateral_views else None, + "UNION or INTERSECT" if tree.select.set_op else None, + "LIMIT" if tree.select.limit else None, + "ORDER BY" if tree.select.organization.order else None, + "SORT BY" if tree.select.organization.sort else None, + ] + invalid_clauses = [clause for clause in clauses if clause is not None] + if invalid_clauses: + raise DJInvalidMetricQueryException( + "Metric has an invalid query. The following are not allowed: " + + ", ".join(invalid_clauses), + ) + + def extra_validation(self) -> None: + """ + Extra validation for node data. + """ + if self.type in {NodeType.TRANSFORM, NodeType.METRIC, NodeType.DIMENSION}: + if not self.query: + raise DJInvalidInputException( + f"Node {self.name} of type {self.type} needs a query", + ) + + if self.type != NodeType.METRIC and self.required_dimensions: + raise DJInvalidInputException( + f"Node {self.name} of type {self.type} cannot have " + "bound dimensions which are only for metrics.", + ) + + if self.type == NodeType.METRIC: + self.check_metric() + + if self.type == NodeType.CUBE: + if not self.cube_elements: + raise DJInvalidInputException( + f"Node {self.name} of type cube node needs cube elements", + ) + + def copy_dimension_links_from_revision(self, old_revision: "NodeRevision"): + """ + Copy dimension links and attributes from another node revision if the column names match + """ + old_columns_mapping = {col.name: col for col in old_revision.columns} + for col in self.columns: + if col.name in old_columns_mapping: + col.dimension_id = old_columns_mapping[col.name].dimension_id + col.attributes = old_columns_mapping[col.name].attributes or [] + return self + + class Config: + extra = Extra.allow + + def has_available_materialization(self, build_criteria: BuildCriteria) -> bool: + """ + Has a materialization available + """ + return ( + self.availability is not None # pragma: no cover + and self.availability.is_available( + criteria=build_criteria, + ) + ) + + def ordering(self) -> Dict[str, int]: + """ + Column ordering + """ + return { + col.name.replace("_DOT_", SEPARATOR): (col.order or idx) + for idx, col in enumerate(self.columns) + } + + def cube_elements_with_nodes(self) -> List[Tuple[Column, Optional["NodeRevision"]]]: + """ + Cube elements along with their nodes + """ + return [(element, element.node_revision()) for element in self.cube_elements] + + def cube_metrics(self) -> List[Node]: + """ + Cube node's metrics + """ + if self.type != NodeType.CUBE: + return [] # pragma: no cover + ordering = { + col.name.replace("_DOT_", SEPARATOR): (col.order or idx) + for idx, col in enumerate(self.columns) + } + return sorted( + [ + node_revision.node # type: ignore + for element, node_revision in self.cube_elements_with_nodes() + if node_revision + and node_revision.node + and node_revision.type == NodeType.METRIC + ], + key=lambda x: ordering[x.name], + ) + + def cube_dimensions(self) -> List[str]: + """ + Cube node's dimension attributes + """ + if self.type != NodeType.CUBE: + return [] # pragma: no cover + dimension_to_roles_mapping = { + col.name: col.dimension_column for col in self.columns + } + ordering = { + (col.name + (col.dimension_column or "")).split("[")[0]: col.order or idx + for idx, col in enumerate(self.columns) + } + return sorted( + [ + node_revision.name + + SEPARATOR + + element.name + + dimension_to_roles_mapping.get(element.name, "") + for element, node_revision in self.cube_elements_with_nodes() + if node_revision and node_revision.type != NodeType.METRIC + ], + key=lambda x: ordering[x], + ) + + @hybrid_property + def cube_node_metrics(self) -> List[str]: + """ + Cube node's metrics + """ + return [metric.name for metric in self.cube_metrics()] + + @hybrid_property + def cube_node_dimensions(self) -> List[str]: + """ + Cube node's dimension attributes + """ + return self.cube_dimensions() + + def temporal_partition_columns(self) -> List[Column]: + """ + The node's temporal partition columns, if any + """ + return [ + col + for col in self.columns + if col.partition and col.partition.type_ == PartitionType.TEMPORAL + ] + + def categorical_partition_columns(self) -> List[Column]: + """ + The node's categorical partition columns, if any + """ + return [ + col + for col in self.columns + if col.partition and col.partition.type_ == PartitionType.CATEGORICAL + ] + + def dimensions_to_columns_map(self): + """ + A mapping between each of the dimension attributes linked to this node to the columns + that they're linked to. + """ + return { # pragma: no cover + left.identifier(): right + for link in self.dimension_links + for left, right in link.foreign_key_mapping().items() + } + + def __deepcopy__(self, memo): + """ + Note: We should not use copy or deepcopy to copy any SQLAlchemy objects. + This is implemented here to make copying of AST structures easier, but does + not actually copy anything + """ + return None + + +class NodeColumns(Base): + """ + Join table for node columns. + """ + + __tablename__ = "nodecolumns" + + node_id: Mapped[int] = mapped_column( + ForeignKey("noderevision.id", name="fk_nodecolumns_node_id_noderevision"), + primary_key=True, + ) + column_id: Mapped[int] = mapped_column( + ForeignKey("column.id", name="fk_nodecolumns_column_id_column"), + primary_key=True, + ) diff --git a/datajunction-server/datajunction_server/database/notification_preference.py b/datajunction-server/datajunction_server/database/notification_preference.py new file mode 100644 index 000000000..dafd4d6dc --- /dev/null +++ b/datajunction-server/datajunction_server/database/notification_preference.py @@ -0,0 +1,60 @@ +"""History database schema.""" + +from datetime import datetime, timezone +from functools import partial + +from sqlalchemy import ( + ARRAY, + BigInteger, + DateTime, + Enum, + ForeignKey, + Integer, + String, + UniqueConstraint, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from datajunction_server.database.base import Base +from datajunction_server.database.history import ActivityType, EntityType +from datajunction_server.database.user import User +from datajunction_server.typing import UTCDatetime + + +class NotificationPreference(Base): # pylint: disable=too-few-public-methods + """ + User notification preferences for a specific entity and activity type. + """ + + __tablename__ = "notificationpreferences" + __table_args__ = ( + UniqueConstraint("entity_name", "entity_type", name="uix_entity_type_name"), + ) + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + ) + entity_type: Mapped[EntityType] = mapped_column( + Enum(EntityType, name="notification_entitytype"), + ) + entity_name: Mapped[str] = mapped_column(String) + activity_types: Mapped[list[ActivityType]] = mapped_column( + ARRAY( + Enum(ActivityType, name="notification_activitytype"), + ), # Use ARRAY of enums + default=list, + ) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id"), + nullable=False, + ) + user: Mapped["User"] = relationship( + "User", + back_populates="notification_preferences", + ) + alert_types: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) + created_at: Mapped[UTCDatetime] = mapped_column( + DateTime(timezone=True), + insert_default=partial(datetime.now, timezone.utc), + ) diff --git a/datajunction-server/datajunction_server/database/partition.py b/datajunction-server/datajunction_server/database/partition.py new file mode 100644 index 000000000..341bc4caf --- /dev/null +++ b/datajunction-server/datajunction_server/database/partition.py @@ -0,0 +1,126 @@ +"""Partition database schema.""" + +import re +from typing import Optional + +from sqlalchemy import BigInteger, Enum, ForeignKey, Integer +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from datajunction_server.database.base import Base +from datajunction_server.database.column import Column +from datajunction_server.models.partition import Granularity, PartitionType +from datajunction_server.naming import amenable_name +from datajunction_server.sql.functions import Function, function_registry +from datajunction_server.sql.parsing.types import TimestampType + + +class Partition(Base): # type: ignore + """ + A partition specification consists of a reference to a partition column and a partition type + (either temporal or categorical). Both partition types indicate how to partition the + materialized dataset, which the configured materializations will use when building + materialization jobs. The temporal partition additionally tells us how to incrementally + materialize the node, with the ongoing materialization job operating on the latest partitions. + + An expression can be optionally provided for temporal partitions, which evaluates to the + temporal partition for scheduled runs. This is typically used to configure a specific timestamp + format for the partition column, i.e., CAST(FORMAT(DJ_LOGICAL_TIMESTAMP(), "yyyyMMdd") AS INT) + would yield a date integer from the current processing partition. + """ + + __tablename__ = "partition" + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + ) + + type_: Mapped[PartitionType] = mapped_column(Enum(PartitionType)) + + # + # Temporal partitions will additionally have the following properties: + # + # Timestamp granularity + granularity: Mapped[Optional[Granularity]] = mapped_column(Enum(Granularity)) + # Timestamp format + format: Mapped[Optional[str]] + + # The column reference that this partition is defined on + column_id: Mapped[int] = mapped_column( + ForeignKey( + "column.id", + name="fk_partition_column_id_column", + ondelete="CASCADE", + ), + ) + column: Mapped[Column] = relationship( + back_populates="partition", + primaryjoin="Column.id==Partition.column_id", + ) + + def temporal_expression(self, interval: Optional[str] = None): + """ + This expression evaluates to the temporal partition value for scheduled runs. Defaults to + CAST(FORMAT(DJ_LOGICAL_TIMESTAMP(), 'yyyyMMdd') AS ). Includes the interval + offset in the expression if provided. + """ + from datajunction_server.sql.parsing import ast + from datajunction_server.sql.parsing.backends.antlr4 import parse + + timestamp_expression = ast.Cast( + expression=ast.Function( + ast.Name("DJ_LOGICAL_TIMESTAMP"), + args=[], + ), + data_type=TimestampType(), + ) + if interval: + interval_ast = parse(f"SELECT INTERVAL {interval}") + timestamp_expression = ast.BinaryOp( # type: ignore + left=timestamp_expression, + right=interval_ast.select.projection[0], # type: ignore + op=ast.BinaryOpKind.Minus, + ) + + if self.type_ == PartitionType.TEMPORAL: + return ast.Cast( + expression=ast.Function( + ast.Name("DATE_FORMAT"), + args=[ + timestamp_expression, + ast.String(f"'{self.format}'"), + ], + ), + data_type=self.column.type, + ) + return None # pragma: no cover + + def categorical_expression(self): + """ + Expression for the categorical partition + """ + from datajunction_server.sql.parsing import ast + + # Register a DJ function (inherits the `datajunction_server.sql.functions.Function` class) + # that has the partition column name as the function name. This will be substituted at + # runtime with the partition column name + amenable_partition_name = amenable_name(self.column.name) + clazz = type( + amenable_partition_name, + (Function,), + { + "is_runtime": True, + "substitute": staticmethod(lambda: f"${{{amenable_partition_name}}}"), + }, + ) + snake_cased = re.sub(r"(? Optional["QueryRequest"]: + """ + Retrieves saved query for a node SQL request + """ + versioned_request = await cls.to_versioned_query_request( + session, + nodes, + dimensions, + filters, + orderby, + query_type, + ) + statement = select(cls).where( + and_( + cls.query_type == query_type, + cls.nodes == (versioned_request["nodes"] or text("'[]'::jsonb")), + cls.parents == (versioned_request["parents"] or text("'[]'::jsonb")), + cls.dimensions + == (versioned_request["dimensions"] or text("'[]'::jsonb")), + cls.filters == (versioned_request["filters"] or text("'[]'::jsonb")), + cls.engine_name == engine_name, + cls.engine_version == engine_version, + cls.limit == limit, + cls.orderby == (versioned_request["orderby"] or text("'[]'::jsonb")), + cls.other_args == (other_args or text("'{}'::jsonb")), + ), + ) + query_request = (await session.execute(statement)).scalar_one_or_none() + if query_request: + return query_request + return None + + @classmethod + async def save_query_request( + cls, + session: AsyncSession, + query_type: QueryBuildType, + nodes: List[str], + dimensions: List[str], + filters: List[str], + engine_name: Optional[str], + engine_version: Optional[str], + limit: Optional[int], + orderby: List[str], + query: str, + columns: List[Dict[str, Any]], + other_args: Optional[Dict[str, Any]] = None, + ) -> "QueryRequest": + """ + Retrieves saved query for a node SQL request + """ + query_request = await cls.get_query_request( + session, + query_type=query_type, + nodes=nodes, + dimensions=dimensions, + filters=filters, + engine_name=engine_name, + engine_version=engine_version, + limit=limit, + orderby=orderby, + other_args=other_args, + ) + if query_request: # pragma: no cover + query_request.query = query + query_request.columns = columns + session.add(query_request) + await session.commit() + else: + versioned_request = await cls.to_versioned_query_request( + session, + nodes, + dimensions, + filters, + orderby, + query_type, + ) + query_request = QueryRequest( + query_type=query_type, + nodes=versioned_request["nodes"], + parents=versioned_request["parents"], + dimensions=versioned_request["dimensions"], + filters=versioned_request["filters"], + engine_name=engine_name, + engine_version=engine_version, + limit=limit, + orderby=versioned_request["orderby"], + query=query, + columns=columns, + other_args=other_args or text("'{}'::jsonb"), + ) + session.add(query_request) + await session.commit() + return query_request + + @classmethod + async def to_versioned_query_request( + cls, + session: AsyncSession, + nodes: List[str], + dimensions: List[str], + filters: List[str], + orderby: List[str], + query_type: QueryBuildType, + ) -> Dict[str, List[str]]: + """ + Prepare for searching in saved query requests by appending version numbers to all nodes + being worked with. + """ + nodes_objs = [ + await Node.get_by_name( + session, + node, + options=[ + joinedload(Node.current).options( + selectinload(NodeRevision.columns), + selectinload(NodeRevision.parents).options( + joinedload(Node.current), + ), + ), + ], + raise_if_not_exists=True, + ) + for node in nodes + ] + + if not nodes_objs and query_type in ( + QueryBuildType.MEASURES, + QueryBuildType.METRICS, + ): + raise DJInvalidInputException( + message="At least one metric is required", + http_status_code=HTTPStatus.UNPROCESSABLE_ENTITY, + ) + node_columns = [] + if len(nodes_objs) == 1: + node_columns = [col.name for col in nodes_objs[0].current.columns] # type: ignore + available_dimensions = { + dim.name + for dim in ( + await get_dimensions(session, nodes_objs[0]) # type: ignore + if len(nodes_objs) == 1 + else await get_shared_dimensions(session, nodes_objs) # type: ignore + ) + }.union(set(node_columns)) + invalid_dimensions = sorted( + list(set(dimensions).difference(available_dimensions)), + ) + if dimensions and invalid_dimensions: + raise DJInvalidInputException( + f"{', '.join(invalid_dimensions)} are not available " + f"dimensions on {', '.join(nodes)}", + ) + + dimension_nodes = [ + await Node.get_by_name(session, ".".join(dim.split(".")[:-1]), options=[]) + for dim in dimensions + ] + filter_asts = { + filter_: parse(f"SELECT 1 WHERE {filter_}") + for filter_ in filters + if filter_ + } + for filter_ in filter_asts: + for col in filter_asts[filter_].select.where.find_all(ast.Column): # type: ignore + if isinstance(col.parent, ast.Subscript): + if isinstance(col.parent.index, ast.Lambda): + col.role = str(col.parent.index) + else: + col.role = col.parent.index.identifier() # type: ignore + col.parent.swap(col) + dimension_node = await Node.get_by_name( + session, + ".".join(col.identifier().split(".")[:-1]), # type: ignore + options=[], + ) + if dimension_node: + col.alias_or_name.name = to_namespaced_name( + f"{col.alias_or_name.name}{'[' + col.role + ']' if col.role else ''}" + f"@{dimension_node.current_version}", # type: ignore + ) + + orders = [] + for order in orderby: + order_rule = order.split(" ") + metric = await Node.get_by_name(session, order_rule[0], options=[]) + if metric: + order_rule[0] = f"{metric.name}@{metric.current_version}" + else: + node = await Node.get_by_name( + session, + ".".join(order_rule[0].split(".")[:-1]), + options=[], + ) + order_rule[0] = f"{order_rule[0]}@{node.current_version}" # type: ignore + orders.append(" ".join(order_rule)) + + parents = [ + upstream + for node in nodes + for upstream in await get_upstream_nodes(session, node) + ] + return { + "nodes": [ + f"{node.name}@{node.current_version}" # type: ignore + for node in nodes_objs + ], + "parents": sorted( + list( + { + f"{parent.name}@{parent.current_version}" # type: ignore + for parent in parents + }, + ), + ), + "dimensions": [ + ( + f"{dim}@{node.current_version}" + if node + else f"{dim}@{nodes_objs[0].current_version}" # type: ignore + ) + for node, dim in zip(dimension_nodes, dimensions) + ], + "filters": [ + str(filter_ast.select.where) for filter_ast in filter_asts.values() + ], + "orderby": orders, + } diff --git a/datajunction-server/datajunction_server/database/tag.py b/datajunction-server/datajunction_server/database/tag.py new file mode 100644 index 000000000..98f64de50 --- /dev/null +++ b/datajunction-server/datajunction_server/database/tag.py @@ -0,0 +1,119 @@ +"""Tag database schema.""" + +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +from sqlalchemy import JSON, BigInteger, Column, ForeignKey, Integer, String, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Mapped, joinedload, mapped_column, relationship +from sqlalchemy.sql.base import ExecutableOption + +from datajunction_server.database.base import Base +from datajunction_server.database.user import User +from datajunction_server.errors import DJDoesNotExistException +from datajunction_server.models.base import labelize + +if TYPE_CHECKING: + from datajunction_server.database.node import Node + + +class Tag(Base): + """ + A tag. + """ + + __tablename__ = "tag" + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + ) + name: Mapped[str] = mapped_column(String, unique=True) + tag_type: Mapped[str] + description: Mapped[Optional[str]] + display_name: Mapped[str] = mapped_column( # pragma: no cover + String, + insert_default=lambda context: labelize(context.current_parameters.get("name")), + ) + created_by_id: Mapped[int] = Column(Integer, ForeignKey("users.id"), nullable=False) + created_by: Mapped[User] = relationship( + "User", + back_populates="created_tags", + foreign_keys=[created_by_id], + lazy="selectin", + ) + tag_metadata: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, default={}) + + nodes: Mapped[List["Node"]] = relationship( + back_populates="tags", + secondary="tagnoderelationship", + primaryjoin="TagNodeRelationship.tag_id==Tag.id", + secondaryjoin="TagNodeRelationship.node_id==Node.id", + ) + + @classmethod + async def find_tags( + cls, + session: AsyncSession, + tag_names: list[str] | None = None, + tag_types: list[str] | None = None, + ) -> list["Tag"]: + """ + Find tags by name or tag type. + """ + statement = select(Tag) + if tag_names: + statement = statement.where(Tag.name.in_(tag_names)) + if tag_types: + statement = statement.where(Tag.tag_type.in_(tag_types)) + return (await session.execute(statement)).scalars().all() + + @classmethod + async def get_tag_types(cls, session: AsyncSession) -> list[str]: + """ + Get all unique tag types. + """ + statement = select(Tag.tag_type).distinct() + return (await session.execute(statement)).scalars().all() + + @classmethod + async def list_nodes_with_tag( + cls, + session: AsyncSession, + tag_name: str, + options: List[ExecutableOption] | None = None, + ) -> list["Node"]: + """ + Find nodes with the tag. + """ + statement = select(cls).where(Tag.name == tag_name) + base_options = joinedload(Tag.nodes) + if options: + base_options = base_options.options(*options) + statement = statement.options(base_options) + tag = (await session.execute(statement)).unique().scalars().one_or_none() + if not tag: # pragma: no cover + raise DJDoesNotExistException( + message=f"A tag with name `{tag_name}` does not exist.", + http_status_code=404, + ) + return sorted( + [node for node in tag.nodes if not node.deactivated_at], + key=lambda x: x.name, + ) + + +class TagNodeRelationship(Base): + """ + Join table between tags and nodes + """ + + __tablename__ = "tagnoderelationship" + + tag_id: Mapped[int] = mapped_column( + ForeignKey("tag.id", name="fk_tagnoderelationship_tag_id_tag"), + primary_key=True, + ) + node_id: Mapped[int] = mapped_column( + ForeignKey("node.id", name="fk_tagnoderelationship_node_id_node"), + primary_key=True, + ) diff --git a/datajunction-server/datajunction_server/database/user.py b/datajunction-server/datajunction_server/database/user.py new file mode 100644 index 000000000..0b2d754e9 --- /dev/null +++ b/datajunction-server/datajunction_server/database/user.py @@ -0,0 +1,87 @@ +"""User database schema.""" + +from typing import TYPE_CHECKING, Optional + +from sqlalchemy import BigInteger, Enum, Integer, String, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from datajunction_server.database.base import Base +from datajunction_server.enum import StrEnum + +if TYPE_CHECKING: + from datajunction_server.database.collection import Collection + from datajunction_server.database.node import Node, NodeRevision + from datajunction_server.database.notification_preference import ( + NotificationPreference, + ) + from datajunction_server.database.tag import Tag + + +class OAuthProvider(StrEnum): + """ + Support oauth providers + """ + + BASIC = "basic" + GITHUB = "github" + GOOGLE = "google" + + +class User(Base): + """Class for a user.""" + + __tablename__ = "users" + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + ) + username: Mapped[str] = mapped_column(String, unique=True) + password: Mapped[Optional[str]] + email: Mapped[Optional[str]] + name: Mapped[Optional[str]] + oauth_provider: Mapped[OAuthProvider] = mapped_column( + Enum(OAuthProvider), + ) + is_admin: Mapped[bool] = mapped_column(default=False) + created_collections: Mapped[list["Collection"]] = relationship( + "Collection", + back_populates="created_by", + foreign_keys="Collection.created_by_id", + lazy="joined", + ) + created_nodes: Mapped[list["Node"]] = relationship( + "Node", + back_populates="created_by", + foreign_keys="Node.created_by_id", + lazy="selectin", + ) + created_node_revisions: Mapped[list["NodeRevision"]] = relationship( + "NodeRevision", + back_populates="created_by", + foreign_keys="NodeRevision.created_by_id", + ) + created_tags: Mapped[list["Tag"]] = relationship( + "Tag", + back_populates="created_by", + foreign_keys="Tag.created_by_id", + lazy="joined", + ) + notification_preferences: Mapped[list["NotificationPreference"]] = relationship( + "NotificationPreference", + back_populates="user", + ) + + @classmethod + async def get_by_username( + cls, + session: AsyncSession, + username: str, + ) -> Optional["User"]: + """ + Find a user by username + """ + statement = select(User).where(User.username == username) + result = await session.execute(statement) + return result.unique().scalar_one_or_none() diff --git a/datajunction-server/datajunction_server/enum.py b/datajunction-server/datajunction_server/enum.py new file mode 100644 index 000000000..cf348c444 --- /dev/null +++ b/datajunction-server/datajunction_server/enum.py @@ -0,0 +1,25 @@ +""" +Backwards-compatible StrEnum for both Python >= and < 3.11 +""" + +import enum +import sys + +if sys.version_info >= (3, 11): + from enum import ( # pragma: no cover + IntEnum, + StrEnum, + ) +else: + + class StrEnum(str, enum.Enum): # pragma: no cover + """Backwards compatible StrEnum for Python < 3.11""" # pragma: no cover + + def __repr__(self): + return str(self.value) + + def __str__(self): + return str(self.value) + + class IntEnum(int, enum.Enum): # pragma: no cover + """Backwards compatible IntEnum for Python < 3.11""" # pragma: no cover diff --git a/datajunction-server/datajunction_server/errors.py b/datajunction-server/datajunction_server/errors.py new file mode 100644 index 000000000..f8f31f995 --- /dev/null +++ b/datajunction-server/datajunction_server/errors.py @@ -0,0 +1,360 @@ +""" +Errors and warnings. +""" + +from http import HTTPStatus +from typing import Any, Dict, List, Literal, Optional, TypedDict + +from pydantic.main import BaseModel + +from datajunction_server.enum import IntEnum + + +class ErrorCode(IntEnum): + """ + Error codes. + """ + + # generic errors + UNKNOWN_ERROR = 0 + NOT_IMPLEMENTED_ERROR = 1 + ALREADY_EXISTS = 2 + + # metric API + INVALID_FILTER_PATTERN = 100 + INVALID_COLUMN_IN_FILTER = 101 + INVALID_VALUE_IN_FILTER = 102 + + # SQL API + INVALID_ARGUMENTS_TO_FUNCTION = 200 + INVALID_SQL_QUERY = 201 + MISSING_COLUMNS = 202 + UNKNOWN_NODE = 203 + NODE_TYPE_ERROR = 204 + INVALID_DIMENSION_JOIN = 205 + INVALID_COLUMN = 206 + QUERY_SERVICE_ERROR = 207 + INVALID_ORDER_BY = 208 + + # SQL Build Error + COMPOUND_BUILD_EXCEPTION = 300 + MISSING_PARENT = 301 + TYPE_INFERENCE = 302 + + # Authentication + AUTHENTICATION_ERROR = 400 + OAUTH_ERROR = 401 + INVALID_LOGIN_CREDENTIALS = 402 + USER_NOT_FOUND = 403 + + # Authorization + UNAUTHORIZED_ACCESS = 500 + INCOMPLETE_AUTHORIZATION = 501 + + # Node validation + INVALID_PARENT = 600 + INVALID_DIMENSION = 601 + INVALID_METRIC = 602 + + +class DebugType(TypedDict, total=False): + """ + Type for debug information. + """ + + # link to where an issue can be filed + issue: str + + # link to documentation about the problem + documentation: str + + # any additional context + context: Dict[str, Any] + + +class DJErrorType(TypedDict): + """ + Type for serialized errors. + """ + + code: int + message: str + debug: Optional[DebugType] + + +class DJError(BaseModel): + """ + An error. + """ + + code: ErrorCode + message: str + debug: Optional[Dict[str, Any]] + context: str = "" + + def __str__(self) -> str: + """ + Format the error nicely. + """ + context = f" from `{self.context}`" if self.context else "" + return f"{self.message}{context} (error code: {self.code})" + + +class DJErrorException(Exception): + """ + Wrapper allows raising DJError + """ + + def __init__(self, dj_error: DJError): + self.dj_error = dj_error + + +class DJWarningType(TypedDict): + """ + Type for serialized warnings. + """ + + code: Optional[int] + message: str + debug: Optional[DebugType] + + +class DJWarning(BaseModel): + """ + A warning. + """ + + code: Optional[ErrorCode] = None + message: str + debug: Optional[Dict[str, Any]] + + +DBAPIExceptions = Literal[ + "Warning", + "Error", + "InterfaceError", + "DatabaseError", + "DataError", + "OperationalError", + "IntegrityError", + "InternalError", + "ProgrammingError", + "NotSupportedError", +] + + +class DJExceptionType(TypedDict): + """ + Type for serialized exceptions. + """ + + message: Optional[str] + errors: List[DJErrorType] + warnings: List[DJWarningType] + + +class DJException(Exception): + """ + Base class for errors. + """ + + message: str + errors: List[DJError] + warnings: List[DJWarning] + + # exception that should be raised when ``DJException`` is caught by the DB API cursor + dbapi_exception: DBAPIExceptions = "Error" + + # status code that should be returned when ``DJException`` is caught by the API layer + http_status_code: int = 500 + + def __init__( + self, + message: Optional[str] = None, + errors: Optional[List[DJError]] = None, + warnings: Optional[List[DJWarning]] = None, + dbapi_exception: Optional[DBAPIExceptions] = None, + http_status_code: Optional[int] = None, + ): + self.errors = errors or [] + self.warnings = warnings or [] + self.message = message or "\n".join(error.message for error in self.errors) + + if dbapi_exception is not None: + self.dbapi_exception = dbapi_exception + if http_status_code is not None: + self.http_status_code = http_status_code + + super().__init__(self.message) + + def to_dict(self) -> DJExceptionType: + """ + Convert to dict. + """ + return { + "message": self.message, + "errors": [error.dict() for error in self.errors], + "warnings": [warning.dict() for warning in self.warnings], + } + + def __str__(self) -> str: + """ + Format the exception nicely. + """ + if not self.errors: + return self.message + + plural = "s" if len(self.errors) > 1 else "" + combined_errors = "\n".join(f"- {error}" for error in self.errors) + errors = f"The following error{plural} happened:\n{combined_errors}" + + return f"{self.message}\n{errors}" + + def __eq__(self, other) -> bool: + return ( + isinstance(other, DJException) + and self.message == other.message + and self.errors == other.errors + and self.warnings == other.warnings + and self.dbapi_exception == other.dbapi_exception + and self.http_status_code == other.http_status_code + ) + + +class DJNodeNotFound(DJException): + """ + Exception raised when a given node name is not found. + """ + + http_status_code: int = HTTPStatus.NOT_FOUND + + +class DJInvalidInputException(DJException): + """ + Exception raised when the input provided by the user is invalid. + """ + + dbapi_exception: DBAPIExceptions = "ProgrammingError" + http_status_code: int = HTTPStatus.UNPROCESSABLE_ENTITY + + +class DJInvalidMetricQueryException(DJInvalidInputException): + """ + Exception raised when the metric query provided by the user is invalid. + """ + + http_status_code: int = HTTPStatus.BAD_REQUEST + + +class DJNotImplementedException(DJException): + """ + Exception raised when some functionality hasn't been implemented in DJ yet. + """ + + dbapi_exception: DBAPIExceptions = "NotSupportedError" + http_status_code: int = 500 + + +class DJInternalErrorException(DJException): + """ + Exception raised when we do something wrong in the code. + """ + + dbapi_exception: DBAPIExceptions = "InternalError" + http_status_code: int = 500 + + +class DJAlreadyExistsException(DJException): + """ + Exception raised when trying to create an entity that already exists. + """ + + dbapi_exception: DBAPIExceptions = "DataError" + http_status_code: int = HTTPStatus.CONFLICT + + +class DJDoesNotExistException(DJException): + """ + Exception raised when an entity doesn't exist. + """ + + dbapi_exception: DBAPIExceptions = "DataError" + http_status_code: int = HTTPStatus.NOT_FOUND + + +class DJQueryServiceClientException(DJException): + """ + Exception raised when the query service returns an error + """ + + dbapi_exception: DBAPIExceptions = "InterfaceError" + http_status_code: int = 500 + + +class DJQueryServiceClientEntityNotFound(DJException): + """ + Exception raised when an entity is not found on the query service + """ + + http_status_code: int = 404 + + +class DJActionNotAllowedException(DJException): + """ + Exception raised when an action is not allowed. + """ + + +class DJPluginNotFoundException(DJException): + """ + Exception raised when plugin is not found. + """ + + +class DJQueryBuildException(DJException): + """ + Exception raised when query building fails. + """ + + +class DJQueryBuildError(DJError): + """ + Query build error + """ + + +class DJAuthorizationException(DJException): + """ + Exception raised when the user is not authorized to perform a particular request + """ + + http_status_code: int = HTTPStatus.FORBIDDEN + message: str = "You do not have permission to perform this action." + + +class DJAuthenticationException(DJException): + """ + Exception raised when the user fails to authenticate. + """ + + http_status_code: int = HTTPStatus.UNAUTHORIZED + message: str = "Authentication failed. Please log in and try again." + + +class DJUninitializedResourceException(DJInternalErrorException): + """ + Raised when a required system resource (e.g., DatabaseSessionManager) is not initialized. + """ + + +class DJConfigurationException(DJException): + """ + Exception raised when there is a missing or incorrect system configuration + that prevents the operation from proceeding. + """ + + +class DJGraphCycleException(DJException): + """ + Exception raised when a cycle is detected in a graph during topological sorting. + """ diff --git a/datajunction-server/datajunction_server/internal/__init__.py b/datajunction-server/datajunction_server/internal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/datajunction_server/internal/access/__init__.py b/datajunction-server/datajunction_server/internal/access/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/datajunction_server/internal/access/authentication/__init__.py b/datajunction-server/datajunction_server/internal/access/authentication/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/datajunction_server/internal/access/authentication/basic.py b/datajunction-server/datajunction_server/internal/access/authentication/basic.py new file mode 100644 index 000000000..ee384e448 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/access/authentication/basic.py @@ -0,0 +1,80 @@ +""" +Basic OAuth and JWT helper functions +""" + +import logging + +from passlib.context import CryptContext +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql.base import ExecutableOption + +from datajunction_server.database.user import User +from datajunction_server.errors import DJAuthenticationException, DJError, ErrorCode + +_logger = logging.getLogger(__name__) +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def validate_password_hash(plain_password, hashed_password) -> bool: + """ + Verify a plain-text password against a hashed password + """ + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password) -> str: + """ + Returns a hashed version of a plain-text password + """ + return pwd_context.hash(password) + + +async def get_user( + username: str, + session: AsyncSession, + *options: ExecutableOption, +) -> User: + """ + Get a DJ user + """ + user = ( + ( + await session.execute( + select(User).options(*options).where(User.username == username), + ) + ) + .unique() + .scalar_one_or_none() + ) + if not user: + raise DJAuthenticationException( + errors=[ + DJError( + message=f"User {username} not found", + code=ErrorCode.USER_NOT_FOUND, + ), + ], + ) + return user + + +async def validate_user_password( + username: str, + password: str, + session: AsyncSession, +) -> User: + """ + Get a DJ user and verify that the provided password matches the hashed password + """ + user = await get_user(username=username, session=session) + if not validate_password_hash(password, user.password): + raise DJAuthenticationException( + errors=[ + DJError( + message=f"Invalid password for user {username}", + code=ErrorCode.INVALID_LOGIN_CREDENTIALS, + ), + ], + ) + return user diff --git a/datajunction-server/datajunction_server/internal/access/authentication/github.py b/datajunction-server/datajunction_server/internal/access/authentication/github.py new file mode 100644 index 000000000..df764e986 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/access/authentication/github.py @@ -0,0 +1,72 @@ +""" +GitHub OAuth helper functions +""" + +import logging +import secrets +from urllib.parse import urljoin + +import requests +from sqlalchemy import select +from sqlalchemy.exc import NoResultFound + +from datajunction_server.database.user import User +from datajunction_server.errors import DJAuthenticationException +from datajunction_server.internal.access.authentication.basic import get_password_hash +from datajunction_server.models.user import OAuthProvider +from datajunction_server.utils import get_session, get_settings + +_logger = logging.getLogger(__name__) + + +def get_authorize_url(oauth_client_id: str) -> str: + """ + Get the authorize url for a GitHub OAuth app + """ + settings = get_settings() + redirect_uri = urljoin(settings.url, "/github/token/") + return ( + f"https://github.com/login/oauth/authorize?client_id={oauth_client_id}" + f"&scope=read:user&redirect_uri={redirect_uri}" + ) + + +def get_github_user(access_token: str) -> User: # pragma: no cover + """ + Get the user for a request + """ + headers = {"Accept": "application/json", "Authorization": f"Bearer {access_token}"} + user_data = requests.get( + "https://api.github.com/user", + headers=headers, + timeout=10, + ).json() + if "message" in user_data and user_data["message"] == "Bad credentials": + raise DJAuthenticationException( + "Cannot authorize user via GitHub, bad credentials", + ) + session = next(get_session()) # type: ignore + existing_user = None + try: + existing_user = session.execute( + select(User).where(User.username == user_data["login"]), + ).scalar() + except NoResultFound: + pass + if existing_user: + _logger.info("OAuth user found") + user = existing_user + else: + _logger.info("OAuth user does not exist, creating a new user") + new_user = User( + username=user_data["login"], + password=get_password_hash(secrets.token_urlsafe(13)), + email=user_data["email"], + name=user_data["name"], + oauth_provider=OAuthProvider.GITHUB, + ) + session.add(new_user) + session.commit() + session.refresh(new_user) + user = new_user + return user diff --git a/datajunction-server/datajunction_server/internal/access/authentication/google.py b/datajunction-server/datajunction_server/internal/access/authentication/google.py new file mode 100644 index 000000000..659007f58 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/access/authentication/google.py @@ -0,0 +1,106 @@ +""" +Google OAuth helper functions +""" + +import logging +import secrets +from http import HTTPStatus +from typing import Optional +from urllib.parse import urljoin + +import google_auth_oauthlib.flow +import requests +from google.auth.external_account_authorized_user import Credentials +from sqlalchemy import select + +from datajunction_server.database.user import User +from datajunction_server.errors import DJAuthenticationException +from datajunction_server.internal.access.authentication.basic import get_password_hash +from datajunction_server.models.user import OAuthProvider +from datajunction_server.utils import get_session, get_settings + +_logger = logging.getLogger(__name__) + +settings = get_settings() +flow = ( + google_auth_oauthlib.flow.Flow.from_client_secrets_file( + settings.google_oauth_client_secret_file, + scopes=[ + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email", + "openid", + ], + redirect_uri=urljoin(settings.url, "/google/token/"), + ) + if settings.google_oauth_client_secret_file + else None +) + + +def get_authorize_url( + state: Optional[str] = None, +) -> google_auth_oauthlib.flow.Flow: + """ + Get the authorize url for a Google OAuth app + """ + authorization_url, _ = flow.authorization_url( + access_type="offline", + include_granted_scopes="true", + prompt="consent", + state=state, + ) + return authorization_url + + +def get_google_access_token( + authorization_response_url: str, +) -> Credentials: + """ + Exchange an authorization token for an access token + """ + flow.fetch_token(authorization_response=authorization_response_url) + return flow.credentials + + +def get_google_user(token: str) -> User: + """ + Get the google user using an access token + """ + headers = {"Accept": "application/json", "Authorization": f"Bearer {token}"} + response = requests.get( + "https://www.googleapis.com/oauth2/v2/userinfo?alt=json", + headers=headers, + timeout=10, + ) + if response.status_code in (200, 201): + raise DJAuthenticationException( + http_status_code=HTTPStatus.FORBIDDEN, + message=f"Error retrieving Google user: {response.text}", + ) + user_data = response.json() + if "message" in user_data and user_data["message"] == "Bad credentials": + raise DJAuthenticationException( + http_status_code=HTTPStatus.FORBIDDEN, + message=f"Error retrieving Google user: {response.text}", + ) + session = next(get_session()) # type: ignore + existing_user = session.execute( + select(User).where(User.email == user_data["login"]), + ).scalar() + if existing_user: + _logger.info("OAuth user found") + user = existing_user + else: + _logger.info("OAuth user does not exist, creating a new user") + new_user = User( + username=user_data["email"], + email=user_data["email"], + password=get_password_hash(secrets.token_urlsafe(13)), + name=user_data["name"], + oauth_provider=OAuthProvider.GOOGLE, + ) + session.add(new_user) + session.commit() + session.refresh(new_user) + user = new_user + return user diff --git a/datajunction-server/datajunction_server/internal/access/authentication/http.py b/datajunction-server/datajunction_server/internal/access/authentication/http.py new file mode 100644 index 000000000..bb3564379 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/access/authentication/http.py @@ -0,0 +1,131 @@ +""" +A secure API router for routes that require authentication +""" + +from http import HTTPStatus +from typing import Any, Callable + +from fastapi import APIRouter, Depends +from fastapi.security import HTTPBearer +from fastapi.security.utils import get_authorization_scheme_param +from fastapi.types import DecoratedCallable +from jose.exceptions import JWEError, JWTError +from sqlalchemy.ext.asyncio import AsyncSession +from starlette.requests import Request + +from datajunction_server.constants import AUTH_COOKIE +from datajunction_server.errors import DJAuthenticationException, DJError, ErrorCode +from datajunction_server.internal.access.authentication.basic import get_user +from datajunction_server.internal.access.authentication.tokens import decode_token +from datajunction_server.utils import get_session, get_settings + + +class DJHTTPBearer(HTTPBearer): + """ + A custom HTTPBearer that accepts a cookie or bearer token + """ + + async def __call__( + self, + request: Request, + session: AsyncSession = Depends(get_session), + ) -> None: + # First check for a JWT sent in a cookie + jwt = request.cookies.get(AUTH_COOKIE) + if jwt: + try: + jwt_data = decode_token(jwt) + except (JWEError, JWTError) as exc: + raise DJAuthenticationException( + http_status_code=HTTPStatus.UNAUTHORIZED, + errors=[ + DJError( + message="Cannot decode authorization token", + code=ErrorCode.AUTHENTICATION_ERROR, + ), + ], + ) from exc + request.state.user = await get_user( + username=jwt_data["username"], + session=session, + ) + return + + authorization: str = request.headers.get("Authorization") + scheme, credentials = get_authorization_scheme_param(authorization) + if not (authorization and scheme and credentials): + if self.auto_error: + raise DJAuthenticationException( + http_status_code=HTTPStatus.FORBIDDEN, + errors=[ + DJError( + message="Not authenticated", + code=ErrorCode.AUTHENTICATION_ERROR, + ), + ], + ) + return # pragma: no cover + if scheme.lower() != "bearer": + if self.auto_error: + raise DJAuthenticationException( + http_status_code=HTTPStatus.FORBIDDEN, + errors=[ + DJError( + message="Invalid authentication credentials", + code=ErrorCode.AUTHENTICATION_ERROR, + ), + ], + ) + return # pragma: no cover + jwt_data = decode_token(credentials) + request.state.user = await get_user( + username=jwt_data["username"], + session=session, + ) + return + + +class TrailingSlashAPIRouter(APIRouter): + """ + A base APIRouter that handles trailing slashes + """ + + def api_route( + self, path: str, *, include_in_schema: bool = True, **kwargs: Any + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + For any given API route path, we always add both the path without the trailing slash + and the path with the trailing slash, ensuring that we can serve both types of calls. + This solution is pulled from https://github.com/tiangolo/fastapi/discussions/7298 + """ + if path.endswith("/"): + path = path[:-1] + + add_path = super().api_route( + path, include_in_schema=include_in_schema, **kwargs + ) + + path_with_trailing_slash = path + "/" + add_trailing_slash_path = super().api_route( + path_with_trailing_slash, include_in_schema=False, **kwargs + ) + + def decorator(func: DecoratedCallable) -> DecoratedCallable: + add_trailing_slash_path(func) + return add_path(func) + + return decorator + + +class SecureAPIRouter(TrailingSlashAPIRouter): + """ + A fastapi APIRouter with a DJHTTPBearer dependency + """ + + def __init__(self, *args: Any, **kwargs: Any): + settings = get_settings() + super().__init__( + *args, + dependencies=[Depends(DJHTTPBearer())] if settings.secret else [], + **kwargs, + ) diff --git a/datajunction-server/datajunction_server/internal/access/authentication/tokens.py b/datajunction-server/datajunction_server/internal/access/authentication/tokens.py new file mode 100644 index 000000000..bbfa0fb73 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/access/authentication/tokens.py @@ -0,0 +1,81 @@ +""" +Helper functions for authentication tokens +""" + +import logging +from datetime import datetime, timedelta +from typing import Optional + +from jose import jwe, jwt +from passlib.context import CryptContext + +from datajunction_server.utils import get_settings + +_logger = logging.getLogger(__name__) +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def encrypt(value: str) -> str: + """ + Encrypt a string value using the configured SECRET + """ + settings = get_settings() + return jwe.encrypt( + value, + settings.secret, + algorithm="dir", + encryption="A128GCM", + ).decode("utf-8") + + +def decrypt(value: str) -> str: + """ + Decrypt a string value using the configured SECRET + """ + settings = get_settings() + return jwe.decrypt(value, settings.secret).decode("utf-8") + + +def create_token( + data: dict, + secret: str, + iss: str, + expires_delta: Optional[timedelta] = None, +) -> str: + """ + Encode data into a signed JWT that's then encrypted using JWE. + + Tokens are created by encoding data (typically a user's information) into + a signed JWT that's then encrypted using JWE. The resulting string can + then be stored in a cookie or authorization headers. When returning a token + in any form other than an HTTP-only cookie, it's important that a reasonably + small expires_delta is provided, such as 24 hours. + """ + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: # pragma: no cover + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + to_encode.update({"iss": iss}) + encoded_jwt = jwt.encode( + to_encode, + secret, + algorithm="HS256", + ) + return encrypt(encoded_jwt) + + +def decode_token(token: str) -> dict: + """ + Decodes a token by first decrypting the JWE and then decoding the signed JWT. + """ + settings = get_settings() + decrypted_token = decrypt(token) + decoded_jwt = jwt.decode( + decrypted_token, + settings.secret, + algorithms=["HS256"], + issuer=settings.url, + ) + return decoded_jwt diff --git a/datajunction-server/datajunction_server/internal/access/authorization.py b/datajunction-server/datajunction_server/internal/access/authorization.py new file mode 100644 index 000000000..8fd26aa71 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/access/authorization.py @@ -0,0 +1,75 @@ +""" +Authorization related functionality +""" + +from typing import Iterable, List, Union + +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.user import User +from datajunction_server.models.access import ( + AccessControl, + AccessControlStore, + ResourceRequest, + ValidateAccessFn, +) +from datajunction_server.models.user import UserOutput + + +def validate_access_requests( + validate_access: ValidateAccessFn, + user: User, + resource_requests: Iterable[ResourceRequest], + raise_: bool = False, +) -> List[Union[NodeRevision, Node, ResourceRequest]]: + """ + Validate a set of access requests. Only approved requests are returned. + """ + if user is None: + return list(resource_requests) # pragma: no cover + access_control = AccessControlStore( + validate_access=validate_access, + user=UserOutput( + id=user.id, + username=user.username, + oauth_provider=user.oauth_provider, + ), + ) + + for request in resource_requests: + access_control.add_request(request) + + validation_results = access_control.validate() + if raise_: + access_control.raise_if_invalid_requests() # pragma: no cover + return [result for result in validation_results if result.approved] + + +def validate_access() -> ValidateAccessFn: + """ + A placeholder validate access dependency injected function + that returns a ValidateAccessFn that approves all requests + """ + + def _(access_control: AccessControl): + """ + Examines all requests in the AccessControl + and approves or denies each + + Args: + access_control (AccessControl): The access control object + containing the access control state and requests. + + Example: + if access_control.state == 'direct': + access_control.approve_all() + return + + if access_control.user=='dj': + request.approve_all() + return + + request.deny_all() + """ + access_control.approve_all() + + return _ diff --git a/datajunction-server/datajunction_server/internal/caching/cachelib_cache.py b/datajunction-server/datajunction_server/internal/caching/cachelib_cache.py new file mode 100644 index 000000000..e7f37698a --- /dev/null +++ b/datajunction-server/datajunction_server/internal/caching/cachelib_cache.py @@ -0,0 +1,44 @@ +""" +Cachelib-based cache implementation +""" + +from typing import Any, Optional + +from cachelib import SimpleCache +from fastapi import Request + +from datajunction_server.internal.caching.interface import Cache, CacheInterface +from datajunction_server.internal.caching.noop_cache import noop_cache + + +class CachelibCache(Cache): + """A standard implementation of CacheInterface that uses cachelib""" + + def __init__(self): + super().__init__() + self.cache = SimpleCache() + + def get(self, key: str) -> Optional[Any]: + """Get a cached value from the simple cache""" + super().get(key) + return self.cache.get(key) + + def set(self, key: str, value: Any, timeout: int = 3600) -> None: + """Cache a value in the simple cache""" + super().set(key, value, timeout) + self.cache.set(key, value, timeout=timeout) + + def delete(self, key: str) -> None: + """Delete a key in the simple cache""" + super().delete(key) + self.cache.delete(key) + + +cachelib_cache = CachelibCache() + + +def get_cache(request: Request) -> Optional[CacheInterface]: + """Dependency for retrieving a cachelib-based cache implementation""" + cache_control = request.headers.get("Cache-Control", "") + skip_cache = "no-cache" in cache_control + return noop_cache if skip_cache else cachelib_cache diff --git a/datajunction-server/datajunction_server/internal/caching/interface.py b/datajunction-server/datajunction_server/internal/caching/interface.py new file mode 100644 index 000000000..9804a19af --- /dev/null +++ b/datajunction-server/datajunction_server/internal/caching/interface.py @@ -0,0 +1,51 @@ +""" +Caching interface and logging wrapper +""" + +import logging +from abc import ABC, abstractmethod +from typing import Any, Optional + + +class CacheInterface(ABC): + """Cache interface""" + + @abstractmethod + def get(self, key: str) -> Optional[Any]: + """Get a cached value""" + + @abstractmethod + def set(self, key: str, value: Any, timeout: int = 300) -> None: + """Cache a value""" + + @abstractmethod + def delete(self, key: str) -> None: + """Delete a cache key""" + + +class Cache(CacheInterface): + """A wrapper for the cache interface to ensure standardized logging""" + + def __init__(self): + self.logger = logging.getLogger(self.__class__.__name__) + + def get(self, key: str) -> Optional[Any]: # type: ignore + """Log the cache check and then use the implemented cache""" + self.logger.info( + "%s: Getting cached value for key %s", + self.__class__.__name__, + key, + ) + + def set(self, key: str, value: Any, timeout: int = 300) -> None: + """Log the cache attempt and then use the implemented cache""" + self.logger.info( + "%s: Setting value for key %s with timeout: %s", + self.__class__.__name__, + key, + timeout, + ) + + def delete(self, key: str) -> None: + """Log the cache deletion attempt and then use the implemented cache""" + self.logger.info("%s: Deleting key %s", self.__class__.__name__, key) diff --git a/datajunction-server/datajunction_server/internal/caching/noop_cache.py b/datajunction-server/datajunction_server/internal/caching/noop_cache.py new file mode 100644 index 000000000..025de9444 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/caching/noop_cache.py @@ -0,0 +1,12 @@ +""" +NoOp cache implementation +""" + +from datajunction_server.internal.caching.interface import Cache + + +class NoOpCache(Cache): + """A NoOp implementation of CacheInterface that returns None for get and set""" + + +noop_cache = NoOpCache() diff --git a/datajunction-server/datajunction_server/internal/client.py b/datajunction-server/datajunction_server/internal/client.py new file mode 100644 index 000000000..7de951380 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/client.py @@ -0,0 +1,331 @@ +"""Helper functions to generate client code.""" + +import os +import urllib +from typing import List, Optional, cast + +from jinja2 import Environment, FileSystemLoader +from nbformat.v4 import new_code_cell, new_markdown_cell, new_notebook +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload, selectinload + +from datajunction_server.construction.utils import to_namespaced_name +from datajunction_server.database import DimensionLink, Node, NodeRevision +from datajunction_server.database.column import Column +from datajunction_server.models.attribute import ColumnAttributes +from datajunction_server.models.node_type import NodeType +from datajunction_server.sql.dag import topological_sort +from datajunction_server.sql.parsing import ast +from datajunction_server.sql.parsing.backends.antlr4 import parse +from datajunction_server.utils import SEPARATOR + +jinja_env = Environment( + loader=FileSystemLoader( + os.path.join(os.path.dirname(__file__), "templates"), + ), +) + + +def python_client_initialize(request_url: str): + """ + Returns the python client code to initialize the client. This function can be overridden + for different servers, based on how the client should be setup. + """ + parsed_url = urllib.parse.urlparse(str(request_url)) + server_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + template = jinja_env.get_template("client_setup.j2") + return template.render(request_url=server_url) + + +def python_client_code_for_linking_complex_dimension( + node_name: str, + dimension_link: DimensionLink, + replace_namespace: Optional[str] = None, +): + """ + Returns the python client code to create a complex dimension link. + """ + node_short_name = node_name.split(SEPARATOR)[-1] + node_namespace = SEPARATOR.join(node_name.split(SEPARATOR)[:-1]) + if replace_namespace: + join_on_ast = dimension_link.join_sql_ast() + for col in join_on_ast.find_all(ast.Column): + col_node_namespace = str( + SEPARATOR.join(col.identifier().split(SEPARATOR)[:-2]), + ) + col_short_name = str(SEPARATOR.join(col.identifier().split(SEPARATOR)[-2:])) + if ( + replace_namespace + and col_node_namespace == node_namespace # pragma: no cover + ): + col.name = to_namespaced_name(f"{replace_namespace}.{col_short_name}") + join_on = str( + join_on_ast.select.from_.relations[-1].extensions[0].criteria.on, # type: ignore + ) + else: + join_on = dimension_link.join_sql + + dimension_node_name = ( + dimension_link.dimension.name.replace( + node_namespace, + replace_namespace, + ) + if replace_namespace + else dimension_link.dimension.name + ) + + template = jinja_env.get_template("link_dimension.j2") + return template.render( + node_short_name=node_short_name, + dimension_node=dimension_node_name, + join_on=join_on, + join_type=dimension_link.join_type.value, + role=dimension_link.role, + ) + + +async def python_client_code_for_setting_column_attributes( + session: AsyncSession, + node_name: str, +): + """ + Returns the python client code to set column attributes. + """ + node_short_name = node_name.split(SEPARATOR)[-1] + node = await Node.get_by_name( + session, + node_name, + options=[ + joinedload(Node.current).options( + selectinload(NodeRevision.columns).options( + selectinload(Column.attributes), + ), + ), + ], + ) + + template = jinja_env.get_template("set_column_attributes.j2") + snippets = [ + template.render( + node_short_name=node_short_name, + column_name=col.name, + attributes=[ + attr.attribute_type + for attr in col.attributes + if attr.attribute_type.name != ColumnAttributes.PRIMARY_KEY.value + ], + ) + for col in node.current.columns # type: ignore + if col.has_attributes_besides(ColumnAttributes.PRIMARY_KEY.value) + ] + return "\n\n".join(snippets) + + +async def python_client_create_node( + session: AsyncSession, + node_name: str, + replace_namespace: Optional[str] = None, +): + """ + Returns the python client code for creating this node + + replace_namespace: a string to replace the node namespace with + """ + node_short_name = node_name.split(SEPARATOR)[-1] + node = cast( + Node, + await Node.get_by_name( + session, + node_name, + options=[ + joinedload(Node.current).options( + *NodeRevision.default_load_options(), + selectinload(NodeRevision.cube_elements) + .selectinload(Column.node_revisions) + .options( + selectinload(NodeRevision.node), + ), + ), + joinedload(Node.tags), + ], + raise_if_not_exists=True, + ), + ) + + if node.type == NodeType.SOURCE: + template = jinja_env.get_template("register_table.j2") + return template.render( + catalog=node.current.catalog.name, + schema=node.current.schema_, + table=node.current.table, + ) + + template = jinja_env.get_template(f"create_{node.type}.j2") + query = ( + node.current.query + if not node.current.query or not replace_namespace + else move_node_references_namespace( + SEPARATOR.join(node.name.split(SEPARATOR)[:-1]), + node.current.query, + replace_namespace, + ) + ).strip() + + return template.render( + short_name=node_short_name, + name=( + node.name + if not replace_namespace + else f"{replace_namespace}.{node_short_name}" + ), + display_name=node.current.display_name, + description=node.current.description.strip(), + mode=node.current.mode, + **( + {"primary_key": [col.name for col in node.current.primary_key()]} + if node.type != NodeType.METRIC + else { + "required_dimensions": [ # type: ignore + col.name for col in node.current.required_dimensions + ], + **( + { + "direction": ( # type: ignore + f"MetricDirection.{node.current.metric_metadata.direction.upper()}" + ), + } + if node.current.metric_metadata + and node.current.metric_metadata.direction + else {} + ), + **( + {"unit": node.current.metric_metadata.unit} + if node.current.metric_metadata + and node.current.metric_metadata.unit + else {} + ), + } + ), + **( + { + "metrics": [ + ( + f"{replace_namespace}{SEPARATOR}{metric.split(SEPARATOR)[-1]}" + if replace_namespace + else metric + ) + for metric in node.current.cube_node_metrics + ], + "dimensions": node.current.cube_node_dimensions, + } + if node.type == NodeType.CUBE + else {"query": query} + ), + tags=[tag.name for tag in node.tags], + ) + + +async def build_export_notebook( + session: AsyncSession, + nodes: List[Node], + introduction: str, + request_url: str, +): + """ + Builds a notebook with Python client code for exporting the list of provided nodes. + """ + notebook = new_notebook() + notebook.cells.append(new_markdown_cell(introduction)) + notebook.cells.append(new_code_cell(python_client_initialize(str(request_url)))) + sorted_nodes = topological_sort(nodes) + cells = await export_nodes_notebook_cells(session, sorted_nodes) + notebook.cells.extend(cells) + return notebook + + +def move_node_references_namespace(namespace: str, query: str, replacement: str) -> str: + """ + Moves all node references in this query to a different namespace but keeps + the node short names intact. + + Example: + move_node_references_namespace("SELECT a, b FROM default.one.c", "default.two") + The above will yield this modified query: + SELECT a, b FROM default.two.c + """ + query_ast = parse(query) + tables = query_ast.find_all(ast.Table) + for tbl in tables: + if str(tbl.name.namespace) == namespace: + tbl.name.namespace = to_namespaced_name(replacement) + return str(query_ast) + + +async def export_nodes_notebook_cells(session: AsyncSession, nodes: List[Node]): + """ + Returns notebook cells used for exporting the list of nodes. + A node export means the following: + - Client code to create the node and set the right tags + - Client code to link all dimensions set on the node + - Client code to set all column attributes on the node + """ + cells = [] + cells.append( + new_markdown_cell( + "### Upserting Nodes:\n" + "\n".join([f"* {node.name}" for node in nodes]), + ), + ) + + # Set up a namespace mapping between current namespaces and where they should be moved + # to. This is modifiable by the exported notebook user and can be used to move nodes + namespaces = {SEPARATOR.join(node.name.split(SEPARATOR)[:-1]) for node in nodes} + template = jinja_env.get_template("namespace_mapping.j2") + template.render(namespaces=namespaces) + cells.append(new_code_cell(template.render(namespaces=namespaces))) + + for node in nodes: + # Add cell for creating node + namespace = SEPARATOR.join(node.name.split(SEPARATOR)[:-1]) + cells.append( + new_code_cell( + await python_client_create_node( + session, + node.name, + replace_namespace=f"{{NAMESPACE_MAPPING['{namespace}']}}", + ), + ), + ) + + # Add cell for linking dimensions if needed + if node.current.dimension_links: + cells.append( + new_markdown_cell( + f"Linking dimensions for {node.type} node `{node.name}`:", + ), + ) + link_dimensions = "\n".join( + [ + python_client_code_for_linking_complex_dimension( + node.name, + link, + replace_namespace=f"{{NAMESPACE_MAPPING['{namespace}']}}", + ) + for link in node.current.dimension_links + ], + ) + cells.append(new_code_cell(link_dimensions)) + + # Add cell for setting column attributes if needed + if any( + col.has_attributes_besides(ColumnAttributes.PRIMARY_KEY.value) + for col in node.current.columns + ): + cells.append( + new_code_cell( + await python_client_code_for_setting_column_attributes( + session, + node.name, + ), + ), + ) + return cells diff --git a/datajunction-server/datajunction_server/internal/cube_materializations.py b/datajunction-server/datajunction_server/internal/cube_materializations.py new file mode 100644 index 000000000..3bd17589f --- /dev/null +++ b/datajunction-server/datajunction_server/internal/cube_materializations.py @@ -0,0 +1,272 @@ +"""Helper functions related to cube materializations.""" + +import itertools + +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.construction.build_v2 import get_measures_query +from datajunction_server.database.node import Column, NodeRevision +from datajunction_server.errors import DJInvalidInputException +from datajunction_server.models.column import SemanticType +from datajunction_server.models.cube_materialization import ( + CombineMaterialization, + CubeMetric, + DruidCubeConfig, + MeasureKey, + MeasuresMaterialization, + UpsertCubeMaterialization, +) +from datajunction_server.models.node_type import NodeNameVersion +from datajunction_server.models.partition import Granularity +from datajunction_server.sql.parsing import ast + + +def generate_partition_filter_sql( + temporal_partition: Column, + lookback_window: str, +) -> str: + """ + Generate filter SQL on partitions + """ + logical_ts = "CAST(DJ_LOGICAL_TIMESTAMP() AS TIMESTAMP)" + + def _partition_sql(timestamp_expression, column_type): + return f"CAST(DATE_FORMAT({timestamp_expression}, 'yyyyMMdd') AS {column_type})" + + partition_sql = _partition_sql(logical_ts, str(temporal_partition.type)) + if temporal_partition.partition.granularity == Granularity.DAY and ( + lookback_window == "1 DAY" or not lookback_window + ): + return f"{temporal_partition.name} = {partition_sql}" + lookback_timestamp = ( + f"{logical_ts} - INTERVAL {lookback_window}" # pragma: no cover + ) + partition_start = _partition_sql( # pragma: no cover + lookback_timestamp, + str(temporal_partition.type), + ) + return ( # pragma: no cover + f"{temporal_partition.name} BETWEEN {partition_start} AND {partition_sql}" + ) + + +def combine_measures_on_shared_grain( + measures_materializations: list[MeasuresMaterialization], + query_grain: list[str], +) -> ast.Query: + """ + Generate a query that combines measures datasets on their shared grain. + An example: + SELECT + COALESCE(measureA.grain1, measureB.grain1) grain1, + COALESCE(measureA.grain2, measureB.grain2) grain2, + measureA.one, + measureA.two, + measureB.three, + measureB.four + FROM measureA + JOIN measureB ON + measureA.grain1 = measureB.grain1 AND + measureA.grain2 = measureB.grain2 + """ + measures_tables = { + mat.output_table_name: mat.table_ast() for mat in measures_materializations + } + initial_mat = measures_materializations[0] + + # Coalesce grain fields + grain_fields = [ + ast.Function( + name=ast.Name("COALESCE"), + args=[ + ast.Column( + name=ast.Name(grain), + _table=measures_tables.get(mat.output_table_name), + ) + for mat in measures_materializations + ], + ).set_alias(alias=ast.Name(grain)) + for grain in query_grain + ] + measures_fields = [ + ast.Column( + name=ast.Name(measure.name), + _table=measures_tables.get(mat.output_table_name), + semantic_type=SemanticType.MEASURE, + ) + for mat in measures_materializations + for measure in mat.measures + ] + from_relation = ast.Relation( + primary=measures_tables.get(initial_mat.output_table_name), # type: ignore + extensions=[ + ast.Join( + join_type="FULL OUTER", + right=ast.Table(name=ast.Name(measures_tables[mat.output_table_name])), + criteria=ast.JoinCriteria( + on=_combine_measures_join_criteria( + measures_tables[initial_mat.output_table_name], + measures_tables[mat.output_table_name], + query_grain, + ), + ), + ) + for mat in measures_materializations[1:] + ], + ) + return ast.Query( + select=ast.Select( + projection=grain_fields + measures_fields, # type: ignore + from_=ast.From(relations=[from_relation]), + ), + ) + + +def _combine_measures_join_criteria(left_table, right_table, query_grain): + """ + Generate the join condition across tables for shared grains. + """ + return ast.BinaryOp.And( + *[ + ast.BinaryOp.Eq( + ast.Column(name=ast.Name(grain), _table=left_table), + ast.Column(name=ast.Name(grain), _table=right_table), + ) + for grain in query_grain + ] + ) + + +async def build_cube_materialization( + session: AsyncSession, + current_revision: NodeRevision, + upsert_input: UpsertCubeMaterialization, +) -> DruidCubeConfig: + """ + Build the full config needed for a Druid cube materialization + """ + temporal_partitions = current_revision.temporal_partition_columns() + temporal_partition = temporal_partitions[0] + measures_queries = await get_measures_query( + session=session, + metrics=current_revision.cube_node_metrics, + dimensions=current_revision.cube_node_dimensions, + filters=[ + generate_partition_filter_sql( + temporal_partition, + upsert_input.lookback_window, # type: ignore + ), + ], + include_all_columns=False, + use_materialized=True, + preagg_requested=True, + ) + query_grains = { + k: [q.node.name for q in queries] + for k, queries in itertools.groupby( + measures_queries, + lambda query: tuple(query.grain), # type: ignore + ) + } + if len(query_grains) > 1: + raise DJInvalidInputException( # pragma: no cover + "DJ cannot manage materializations for cubes that have underlying " + "measures queries at different grains: " + + " vs ".join( + f"{', '.join(query_nodes)} at [{', '.join(grain)}]" + for grain, query_nodes in query_grains.items() + ), + ) + measures_materializations = [ + MeasuresMaterialization.from_measures_query(measures_query, temporal_partition) + for measures_query in measures_queries + ] + + # Combine the queries on the shared query grain + query_grain = next(iter(query_grains)) + if len(measures_materializations) == 1: + measures_materialization = measures_materializations[0] + combiners = [ + CombineMaterialization( + node=measures_materialization.node, + output_table_name=measures_materialization.output_table_name, + columns=measures_materialization.columns, + grain=measures_materialization.grain, + measures=measures_materialization.measures, + dimensions=measures_materialization.dimensions, + timestamp_column=measures_materialization.timestamp_column, + timestamp_format=measures_materialization.timestamp_format, + granularity=measures_materialization.granularity, + upstream_tables=[measures_materialization.output_table_name], + ), + ] + if len(measures_materializations) > 1: + measures_materialization = measures_materializations[0] + combiner_query = combine_measures_on_shared_grain( + measures_materializations, + query_grain, # type: ignore + ) + columns_metadata_lookup = { + col.name: col + for mat in measures_materializations + for col in mat.columns + if col.name in combiner_query.select.column_mapping + } + measures_lookup = { + measure.name: measure + for mat in measures_materializations + for measure in mat.measures + } + combiners = [ + CombineMaterialization( + node=current_revision, + query=str(combiner_query), + columns=[ + columns_metadata_lookup.get(col.alias_or_name.name) # type: ignore + for col in combiner_query.select.projection + ], + grain=query_grain, + measures=[ + measures_lookup.get(col.alias_or_name.name) # type: ignore + for col in combiner_query.select.projection + if col.semantic_type == SemanticType.MEASURE # type: ignore + ], + dimensions=query_grain, + timestamp_column=measures_materialization.timestamp_column, + timestamp_format=measures_materialization.timestamp_format, + granularity=measures_materialization.granularity, + ), + ] + + metrics_mapping = { + metric: (measures_query.node, measures) + for measures_query in measures_queries + for metric, measures in measures_query.metrics.items() # type: ignore + } + config = DruidCubeConfig( + cube=NodeNameVersion( + name=current_revision.name, + version=current_revision.version, + ), + metrics=[ + CubeMetric( + metric=NodeNameVersion( + name=metric.name, + version=metric.current_version, + ), + required_measures=[ + MeasureKey( + node=metrics_mapping.get(metric.name)[0], # type: ignore + measure_name=measure.name, + ) + for measure in metrics_mapping.get(metric.name)[1][0] # type: ignore + ], + derived_expression=metrics_mapping.get(metric.name)[1][1], # type: ignore + ) + for metric in current_revision.cube_metrics() + ], + dimensions=current_revision.cube_node_dimensions, + measures_materializations=measures_materializations, + combiners=combiners, + ) + return config diff --git a/datajunction-server/datajunction_server/internal/engines.py b/datajunction-server/datajunction_server/internal/engines.py new file mode 100644 index 000000000..b6794e96d --- /dev/null +++ b/datajunction-server/datajunction_server/internal/engines.py @@ -0,0 +1,29 @@ +"""Helper functions for engines.""" + +from http import HTTPStatus + +from fastapi import HTTPException +from sqlalchemy import select +from sqlalchemy.exc import NoResultFound +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database.engine import Engine + + +async def get_engine(session: AsyncSession, name: str, version: str) -> Engine: + """ + Return an Engine instance given an engine name and version + """ + statement = ( + select(Engine) + .where(Engine.name == name) + .where(Engine.version == (version or "")) + ) + try: + engine = (await session.execute(statement)).scalar_one() + except NoResultFound as exc: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Engine not found: `{name}` version `{version}`", + ) from exc + return engine diff --git a/datajunction-server/datajunction_server/internal/materializations.py b/datajunction-server/datajunction_server/internal/materializations.py new file mode 100644 index 000000000..c158b32ad --- /dev/null +++ b/datajunction-server/datajunction_server/internal/materializations.py @@ -0,0 +1,440 @@ +"""Node materialization helper functions""" + +import logging +import zlib +from typing import Dict, List, Optional, Tuple, Union + +from pydantic import ValidationError +from sqlalchemy.exc import InvalidRequestError +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.api.helpers import build_sql_for_multiple_metrics +from datajunction_server.construction.build import get_default_criteria +from datajunction_server.construction.build_v2 import QueryBuilder, get_measures_query +from datajunction_server.database.materialization import Materialization +from datajunction_server.database.node import NodeRevision +from datajunction_server.database.user import User +from datajunction_server.errors import DJException, DJInvalidInputException +from datajunction_server.internal.cube_materializations import ( + build_cube_materialization, +) +from datajunction_server.materialization.jobs import MaterializationJob +from datajunction_server.models import access +from datajunction_server.models.column import SemanticType +from datajunction_server.models.cube_materialization import UpsertCubeMaterialization +from datajunction_server.models.materialization import ( + DruidMeasuresCubeConfig, + DruidMetricsCubeConfig, + GenericMaterializationConfig, + MaterializationInfo, + MaterializationJobTypeEnum, + Measure, + MetricMeasures, + UpsertMaterialization, +) +from datajunction_server.models.metric import TranslatedSQL +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.query import ColumnMetadata +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.sql.parsing import ast +from datajunction_server.sql.parsing.ast import CompileContext +from datajunction_server.sql.parsing.backends.antlr4 import parse +from datajunction_server.sql.parsing.types import TimestampType +from datajunction_server.utils import SEPARATOR + +MAX_COLUMN_NAME_LENGTH = 128 +_logger = logging.getLogger(__name__) + + +async def rewrite_metrics_expressions( + session: AsyncSession, + current_revision: NodeRevision, + measures_query: TranslatedSQL, +) -> Dict[str, MetricMeasures]: + """ + Map each metric to a rewritten version of the metric expression with the measures from + the materialized measures table. + """ + context = CompileContext(session, DJException()) + metrics_expressions = {} + measures_to_output_columns_lookup = { + column.semantic_entity: column.name + for column in measures_query.columns # type: ignore + } + for metric in current_revision.cube_metrics(): + measures_for_metric = [] + metric_ast = parse(metric.current.query) + await metric_ast.compile(context) + for col in metric_ast.select.find_all(ast.Column): + full_column_name = ( + col.table.dj_node.name + SEPARATOR + col.alias_or_name.name # type: ignore + ) + if ( + full_column_name in measures_to_output_columns_lookup + ): # pragma: no cover + measures_for_metric.append( + Measure( + name=full_column_name, + field_name=measures_to_output_columns_lookup[full_column_name], + type=str(col.type), + agg="sum", + ), + ) + + col._table = None + col.name = ast.Name( + measures_to_output_columns_lookup[full_column_name], + ) + if ( + hasattr(metric_ast.select.projection[0], "alias") + and metric_ast.select.projection[0].alias # type: ignore + ): + metric_ast.select.projection[0].alias.name = "" # type: ignore + if measures_for_metric: + metrics_expressions[metric.name] = MetricMeasures( + metric=metric.name, + measures=measures_for_metric, + combiner=str(metric_ast.select.projection[0]), + ) + return metrics_expressions + + +async def build_cube_materialization_config( + session: AsyncSession, + current_revision: NodeRevision, + upsert_input: UpsertMaterialization, + validate_access: access.ValidateAccessFn, + current_user: User, +) -> DruidMeasuresCubeConfig: + """ + Builds the materialization config for a cube. + + If the job type is DRUID_METRICS_CUBE, we build an aggregation query with all metric + aggregations and ingest this aggregated table to Druid. + + Alternatively, we build a measures query where we ingest the referenced measures for all + selected metrics at the level of dimensions provided. This query is used to create + an intermediate table for ingestion into an OLAP database like Druid. + + We additionally provide a metric to measures mapping that tells us both which measures + in the query map to each selected metric and how to rewrite each metric expression + based on the materialized measures table. + """ + try: + # Druid Metrics Cube (Post-Agg) + if upsert_input.job == MaterializationJobTypeEnum.DRUID_METRICS_CUBE: + metrics_query, _, _ = await build_sql_for_multiple_metrics( + session=session, + metrics=[node.name for node in current_revision.cube_metrics()], + dimensions=current_revision.cube_dimensions(), + use_materialized=False, + ) + generic_config = DruidMetricsCubeConfig( + lookback_window=upsert_input.config.lookback_window, + node_name=current_revision.name, + query=metrics_query.sql, + dimensions=[ + col.name + for col in metrics_query.columns # type: ignore + if col.semantic_type != SemanticType.METRIC + ], + metrics=[ + col + for col in metrics_query.columns # type: ignore + if col.semantic_type == SemanticType.METRIC + ], + spark=upsert_input.config.spark, + upstream_tables=metrics_query.upstream_tables, + columns=metrics_query.columns, + ) + return generic_config + + # Druid Measures Cube (Pre-Agg) + measures_queries = await get_measures_query( + session=session, + metrics=[node.name for node in current_revision.cube_metrics()], + dimensions=current_revision.cube_dimensions(), + filters=[], + current_user=current_user, + validate_access=validate_access, + ) + for measures_query in measures_queries: + metrics_expressions = await rewrite_metrics_expressions( + session, + current_revision, + measures_query, + ) + generic_config = DruidMeasuresCubeConfig( + node_name=current_revision.name, + query=measures_query.sql, + dimensions=[ + col.name + for col in measures_query.columns # type: ignore + if col.semantic_type == SemanticType.DIMENSION + ], + measures=metrics_expressions, + spark=upsert_input.config.spark, + upstream_tables=measures_query.upstream_tables, + columns=measures_query.columns, + ) + return generic_config + except (KeyError, ValidationError, AttributeError) as exc: # pragma: no cover + raise DJInvalidInputException( # pragma: no cover + message=( + "No change has been made to the materialization config for " + f"node `{current_revision.name}` and job `{upsert_input.job.name}` as" + " the config does not have valid configuration for " + f"engine `{upsert_input.job.name}`." + ), + ) from exc + + +async def build_non_cube_materialization_config( + session: AsyncSession, + current_revision: NodeRevision, + upsert: UpsertMaterialization, +) -> GenericMaterializationConfig: + """ + Build materialization config for non-cube nodes (transforms and dimensions). + """ + _logger.info( + "Building materialization config for node=%s node_type=%s %s", + current_revision.name, + current_revision.type, + upsert, + ) + build_criteria = get_default_criteria( + node=current_revision, + ) + query_builder = await QueryBuilder.create(session, current_revision) + materialization_ast = await ( + query_builder.ignore_errors().with_build_criteria(build_criteria).build() + ) + generic_config = GenericMaterializationConfig( + lookback_window=upsert.config.lookback_window, + query=str(materialization_ast), + spark=upsert.config.spark if upsert.config.spark else {}, + upstream_tables=[ + f"{current_revision.catalog.name}.{tbl.identifier()}" + for tbl in materialization_ast.find_all(ast.Table) + ], + columns=[ + ColumnMetadata(name=col.name, type=str(col.type)) + for col in current_revision.columns + ], + ) + return generic_config + + +async def create_new_materialization( + session: AsyncSession, + current_revision: NodeRevision, + upsert: UpsertMaterialization | UpsertCubeMaterialization, + validate_access: access.ValidateAccessFn, + current_user: User, +) -> Materialization: + """ + Create a new materialization based on the input values. + """ + generic_config = None + try: + await session.refresh(current_revision, ["columns"]) + except InvalidRequestError: + pass + temporal_partition = current_revision.temporal_partition_columns() + timestamp_columns = [ + col for col in current_revision.columns if col.type == TimestampType() + ] + if current_revision.type in ( + NodeType.DIMENSION, + NodeType.TRANSFORM, + ): + generic_config = await build_non_cube_materialization_config( + session, + current_revision, + upsert, + ) + + categorical_partitions = current_revision.categorical_partition_columns() + if current_revision.type == NodeType.CUBE: + if not temporal_partition and not timestamp_columns: + raise DJInvalidInputException( + "The cube materialization cannot be configured if there is no " + "temporal partition specified on the cube. Please make sure at " + "least one cube element has a temporal partition defined", + ) + + # Druid Cube (this job will subsume all existing cube materialization types) + if upsert.job == MaterializationJobTypeEnum.DRUID_CUBE: + generic_config = await build_cube_materialization( + session=session, + current_revision=current_revision, + upsert_input=upsert, + ) + else: + generic_config = await build_cube_materialization_config( + session, + current_revision, + upsert, + validate_access, + current_user=current_user, + ) + materialization_name = ( + f"{upsert.job.name.lower()}__{upsert.strategy.name.lower()}" + + (f"__{temporal_partition[0].name}" if temporal_partition else "") + + ("__" if categorical_partitions else "") + + ("__".join([partition.name for partition in categorical_partitions])) + ) + return Materialization( + name=materialization_name, + node_revision=current_revision, + config=generic_config.dict(), # type: ignore + schedule=upsert.schedule or "@daily", + strategy=upsert.strategy, + job=upsert.job.value.job_class, # type: ignore + ) + + +async def schedule_materialization_jobs( + session: AsyncSession, + node_revision_id: int, + materialization_names: List[str], + query_service_client: QueryServiceClient, + request_headers: Optional[Dict[str, str]] = None, +) -> Dict[str, MaterializationInfo]: + """ + Schedule recurring materialization jobs + """ + materializations = await Materialization.get_by_names( + session, + node_revision_id, + materialization_names, + ) + materialization_jobs = { + cls.__name__: cls for cls in MaterializationJob.__subclasses__() + } + materialization_to_output = {} + for materialization in materializations: + clazz = materialization_jobs.get(materialization.job) + if clazz and materialization.name: # pragma: no cover + materialization_to_output[materialization.name] = clazz().schedule( # type: ignore + materialization, + query_service_client, + request_headers=request_headers, + ) + return materialization_to_output + + +def _get_readable_name(expr): + """ + Returns a readable name based on the columns in the expression. This is used + if we want to represent the expression as a single measure, which needs a name + """ + columns = [col for arg in expr.args for col in arg.find_all(ast.Column)] + readable_name = "_".join( + str(col.alias_or_name).rsplit(".", maxsplit=1)[-1] for col in columns + ) + return ( + readable_name[: MAX_COLUMN_NAME_LENGTH - 28] + + str(zlib.crc32(readable_name.encode("utf-8"))) + if columns + else "placeholder" + ) + + +def decompose_expression( + expr: Union[ast.Aliasable, ast.Expression], +) -> Tuple[ast.Expression, List[ast.Alias]]: + """ + Takes a metric expression and (a) determines the measures needed to evaluate + the metric and (b) includes the query expression needed to recombine these + measures into the metric, given a materialized cube. + + Simple aggregations are operations that can be computed incrementally as new + data is ingested, without relying on the results of other aggregations. + Examples include SUM, COUNT, MIN, MAX. + + Some complex aggregations can be decomposed to simple aggregations: i.e., AVG(x) can + be decomposed to SUM(x)/COUNT(x). + """ + if isinstance(expr, ast.Alias): + expr = expr.child # pragma: no cover + + if isinstance(expr, ast.Number): + return expr, [] # type: ignore + + if not expr.is_aggregation(): # type: ignore # pragma: no cover + return expr, [expr] # type: ignore + + simple_aggregations = {"sum", "count", "min", "max"} + if isinstance(expr, ast.Function): + function_name = expr.alias_or_name.name.lower() + readable_name = _get_readable_name(expr) + + if function_name in simple_aggregations: + measure_name = ast.Name(f"{readable_name}_{function_name}") + if not expr.args[0].is_aggregation(): + combiner: ast.Expression = ast.Function( + name=ast.Name(function_name), + args=[ast.Column(name=measure_name)], + ) + return combiner, [expr.set_alias(measure_name)] + + combiner, measures = decompose_expression(expr.args[0]) + return ( + ast.Function( + name=ast.Name(function_name), + args=[combiner], + ), + measures, + ) + + if function_name == "avg": # pragma: no cover + numerator_measure_name = ast.Name(f"{readable_name}_sum") + denominator_measure_name = ast.Name(f"{readable_name}_count") + combiner = ast.BinaryOp( + left=ast.Function( + ast.Name("sum"), + args=[ast.Column(name=numerator_measure_name)], + ), + right=ast.Function( + ast.Name("count"), + args=[ast.Column(name=denominator_measure_name)], + ), + op=ast.BinaryOpKind.Divide, + ) + return combiner, [ + ( + ast.Function(ast.Name("sum"), args=expr.args).set_alias( + numerator_measure_name, + ) + ), + ( + ast.Function(ast.Name("count"), args=expr.args).set_alias( + denominator_measure_name, + ) + ), + ] + acceptable_binary_ops = { + ast.BinaryOpKind.Plus, + ast.BinaryOpKind.Minus, + ast.BinaryOpKind.Multiply, + ast.BinaryOpKind.Divide, + } + if isinstance(expr, ast.BinaryOp): + if expr.op in acceptable_binary_ops: # pragma: no cover + measures_combiner_left, measures_left = decompose_expression(expr.left) + measures_combiner_right, measures_right = decompose_expression(expr.right) + combiner = ast.BinaryOp( + left=measures_combiner_left, + right=measures_combiner_right, + op=expr.op, + ) + return combiner, measures_left + measures_right + + if isinstance(expr, ast.Cast): + return decompose_expression(expr.expression) + + raise DJInvalidInputException( # pragma: no cover + f"Metric expression {expr} cannot be decomposed into its constituent measures", + ) diff --git a/datajunction-server/datajunction_server/internal/namespaces.py b/datajunction-server/datajunction_server/internal/namespaces.py new file mode 100644 index 000000000..08e24f2f8 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/namespaces.py @@ -0,0 +1,556 @@ +""" +Helper methods for namespaces endpoints. +""" + +import os +import re +from datetime import datetime +from typing import Dict, List, Tuple + +from sqlalchemy import or_, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload + +from datajunction_server.api.helpers import get_node_namespace +from datajunction_server.database.history import ActivityType, EntityType, History +from datajunction_server.database.namespace import NodeNamespace +from datajunction_server.database.node import Column, Node, NodeRevision +from datajunction_server.database.user import User +from datajunction_server.errors import ( + DJActionNotAllowedException, + DJDoesNotExistException, + DJInvalidInputException, +) +from datajunction_server.internal.nodes import ( + get_cube_revision_metadata, + hard_delete_node, +) +from datajunction_server.models.node import NodeMinimumDetail +from datajunction_server.models.node_type import NodeType +from datajunction_server.sql.dag import topological_sort +from datajunction_server.typing import UTCDatetime +from datajunction_server.utils import SEPARATOR + +# A list of namespace names that cannot be used because they are +# part of a list of reserved SQL keywords +RESERVED_NAMESPACE_NAMES = [ + "user", +] + + +async def get_nodes_in_namespace( + session: AsyncSession, + namespace: str, + node_type: NodeType = None, + include_deactivated: bool = False, +) -> List[NodeMinimumDetail]: + """ + Gets a list of node names in the namespace + """ + return await NodeNamespace.list_nodes( + session, + namespace, + node_type=node_type, + include_deactivated=include_deactivated, + ) + + +async def get_nodes_in_namespace_detailed( + session: AsyncSession, + namespace: str, + node_type: NodeType = None, +) -> List[Node]: + """ + Gets a list of node names (w/ full details) in the namespace + """ + await get_node_namespace(session, namespace) + list_nodes_query = ( + select(Node) + .where( + or_( + Node.namespace.like(f"{namespace}.%"), + Node.namespace == namespace, + ), + Node.current_version == NodeRevision.version, + Node.name == NodeRevision.name, + Node.type == node_type if node_type else True, + ) + .options( + joinedload(Node.current).options( + *NodeRevision.default_load_options(), + ), + joinedload(Node.tags), + ) + ) + return (await session.execute(list_nodes_query)).unique().scalars().all() + + +async def list_namespaces_in_hierarchy( + session: AsyncSession, + namespace: str, +) -> List[NodeNamespace]: + """ + Get all namespaces in hierarchy under the specified namespace + """ + statement = select(NodeNamespace).where( + or_( + NodeNamespace.namespace.like( + f"{namespace}.%", + ), + NodeNamespace.namespace == namespace, + ), + ) + namespaces = (await session.execute(statement)).scalars().all() + if len(namespaces) == 0: + raise DJDoesNotExistException( + message=(f"Namespace `{namespace}` does not exist."), + http_status_code=404, + ) + return namespaces + + +async def mark_namespace_deactivated( + session: AsyncSession, + namespace: NodeNamespace, + current_user: User, + message: str = None, +): + """ + Deactivates the node namespace and updates history indicating so + """ + now = datetime.utcnow() + namespace.deactivated_at = UTCDatetime( + year=now.year, + month=now.month, + day=now.day, + hour=now.hour, + minute=now.minute, + second=now.second, + ) + session.add( + History( + entity_type=EntityType.NAMESPACE, + entity_name=namespace.namespace, + node=None, + activity_type=ActivityType.DELETE, + details={"message": message or ""}, + user=current_user.username, + ), + ) + await session.commit() + + +async def mark_namespace_restored( + session: AsyncSession, + namespace: NodeNamespace, + current_user: User, + message: str = None, +): + """ + Restores the node namespace and updates history indicating so + """ + namespace.deactivated_at = None # type: ignore + session.add( + History( + entity_type=EntityType.NAMESPACE, + entity_name=namespace.namespace, + node=None, + activity_type=ActivityType.RESTORE, + details={"message": message or ""}, + user=current_user.username, + ), + ) + await session.commit() + + +def validate_namespace(namespace: str): + """ + Validate that the namespace parts are valid (i.e., cannot start with numbers or be empty) + """ + parts = namespace.split(SEPARATOR) + for part in parts: + if ( + not part + or not re.match("^[a-zA-Z][a-zA-Z0-9_]*$", part) + or part in RESERVED_NAMESPACE_NAMES + ): + raise DJInvalidInputException( + f"{namespace} is not a valid namespace. Namespace parts cannot start with numbers" + f", be empty, or use the reserved keyword [{', '.join(RESERVED_NAMESPACE_NAMES)}]", + ) + + +def get_parent_namespaces(namespace: str): + """ + Return a list of all parent namespaces + """ + parts = namespace.split(SEPARATOR) + return [SEPARATOR.join(parts[0:i]) for i in range(len(parts)) if parts[0:i]] + + +async def create_namespace( + session: AsyncSession, + namespace: str, + current_user: User, + include_parents: bool = True, +) -> List[str]: + """ + Creates a namespace entry in the database table. + """ + parents = ( + get_parent_namespaces(namespace) + [namespace] + if include_parents + else [namespace] + ) + for parent_namespace in parents: + if not await get_node_namespace( # pragma: no cover + session=session, + namespace=parent_namespace, + raise_if_not_exists=False, + ): + node_namespace = NodeNamespace(namespace=parent_namespace) + session.add(node_namespace) + session.add( + History( + entity_type=EntityType.NAMESPACE, + entity_name=namespace, + node=None, + activity_type=ActivityType.CREATE, + user=current_user.username, + ), + ) + await session.commit() + return parents + + +async def hard_delete_namespace( + session: AsyncSession, + namespace: str, + current_user: User, + cascade: bool = False, +): + """ + Hard delete a node namespace. + """ + node_names = ( + ( + await session.execute( + select(Node.name) + .where( + or_( + Node.namespace.like( + f"{namespace}.%", + ), + Node.namespace == namespace, + ), + ) + .order_by(Node.name), + ) + ) + .scalars() + .all() + ) + + if not cascade and node_names: + raise DJActionNotAllowedException( + message=( + f"Cannot hard delete namespace `{namespace}` as there are still the " + f"following nodes under it: `{node_names}`. Set `cascade` to true to " + "additionally hard delete the above nodes in this namespace. WARNING:" + " this action cannot be undone." + ), + ) + + impacts = {} + for node_name in node_names: + impacts[node_name] = await hard_delete_node( + node_name, + session, + current_user=current_user, + ) + + namespaces = await list_namespaces_in_hierarchy(session, namespace) + for _namespace in namespaces: + impacts[_namespace.namespace] = { + "namespace": _namespace.namespace, + "status": "deleted", + } + await session.delete(_namespace) + await session.commit() + return impacts + + +def _get_dir_and_filename( + node_name: str, + node_type: str, + namespace_requested: str, +) -> Tuple[str, str, str]: + """ + Get the directory, filename, and build name for a node + """ + dot_split = node_name.replace(f"{namespace_requested}.", "").split(".") + filename = f"{dot_split[-1]}.{node_type}.yaml" + directory = os.path.sep.join(dot_split[:-1]) + build_name = ( + f"{SEPARATOR.join(dot_split[:-1])}.{dot_split[-1]}" + if directory + else dot_split[-1] + ) + return filename, directory, build_name + + +def _non_primary_key_attributes(column: Column): + """ + Returns all non-PK column attributes for a column + """ + return [ + attr.attribute_type.name + for attr in column.attributes + if attr.attribute_type.name not in ("primary_key",) + ] + + +def _attributes_config(column: Column): + """ + Returns a project config definition for a partition on a column + """ + non_pk_attributes = _non_primary_key_attributes(column) + if non_pk_attributes: + return {"attributes": _non_primary_key_attributes(column)} + return {} + + +def _partition_config(column: Column): + """ + Returns a project config definition for a partition on a column + """ + if column.partition: + return { + "partition": { + "format": column.partition.format, + "granularity": column.partition.granularity, + "type_": column.partition.type_, + }, + } + return {} + + +def _source_project_config(node: Node, namespace_requested: str) -> Dict: + """ + Returns a project config definition for a source node + """ + filename, directory, build_name = _get_dir_and_filename( + node_name=node.name, + node_type=node.type, + namespace_requested=namespace_requested, + ) + return { + "filename": filename, + "directory": directory, + "build_name": build_name, + "display_name": node.current.display_name, + "description": node.current.description, + "table": f"{node.current.catalog}.{node.current.schema_}.{node.current.table}", + "columns": [ + { + "name": column.name, + "type": str(column.type), + **_attributes_config(column), + **_partition_config(column), + } + for column in node.current.columns + ], + "primary_key": [pk.name for pk in node.current.primary_key()], + "dimension_links": _dimension_links_config(node), + "tags": [tag.name for tag in node.tags], + } + + +def _transform_project_config(node: Node, namespace_requested: str) -> Dict: + """ + Returns a project config definition for a transform node + """ + filename, directory, build_name = _get_dir_and_filename( + node_name=node.name, + node_type=node.type, + namespace_requested=namespace_requested, + ) + return { + "filename": filename, + "directory": directory, + "build_name": build_name, + "display_name": node.current.display_name, + "description": node.current.description, + "query": node.current.query, + "columns": [ + { + "name": column.name, + **_attributes_config(column), + **_partition_config(column), + } + for column in node.current.columns + if _non_primary_key_attributes(column) or column.partition + ], + "primary_key": [pk.name for pk in node.current.primary_key()], + "dimension_links": _dimension_links_config(node), + "tags": [tag.name for tag in node.tags], + } + + +def _dimension_project_config(node: Node, namespace_requested: str) -> Dict: + """ + Returns a project config definition for a dimension node + """ + filename, directory, build_name = _get_dir_and_filename( + node_name=node.name, + node_type=node.type, + namespace_requested=namespace_requested, + ) + return { + "filename": filename, + "directory": directory, + "build_name": build_name, + "display_name": node.current.display_name, + "description": node.current.description, + "query": node.current.query, + "columns": [ + { + "name": column.name, + **_attributes_config(column), + **_partition_config(column), + } + for column in node.current.columns + if _non_primary_key_attributes(column) or column.partition + ], + "primary_key": [pk.name for pk in node.current.primary_key()], + "dimension_links": _dimension_links_config(node), + "tags": [tag.name for tag in node.tags], + } + + +def _metric_project_config(node: Node, namespace_requested: str) -> Dict: + """ + Returns a project config definition for a metric node + """ + filename, directory, build_name = _get_dir_and_filename( + node_name=node.name, + node_type=node.type, + namespace_requested=namespace_requested, + ) + return { + "filename": filename, + "directory": directory, + "build_name": build_name, + "display_name": node.current.display_name, + "description": node.current.description, + "query": node.current.query, + "tags": [tag.name for tag in node.tags], + "required_dimensions": [dim.name for dim in node.current.required_dimensions], + "direction": ( + node.current.metric_metadata.direction.name.lower() + if node.current.metric_metadata and node.current.metric_metadata.direction + else None + ), + "unit": ( + node.current.metric_metadata.unit.name.lower() + if node.current.metric_metadata and node.current.metric_metadata.unit + else None + ), + } + + +async def _cube_project_config( + session: AsyncSession, + node: Node, + namespace_requested: str, +) -> Dict: + """ + Returns a project config definition for a cube node + """ + filename, directory, build_name = _get_dir_and_filename( + node_name=node.name, + node_type=NodeType.CUBE, + namespace_requested=namespace_requested, + ) + cube_revision = await get_cube_revision_metadata(session, node.name) + metrics = [] + dimensions = [] + for element in cube_revision.cube_elements: + if element.type == NodeType.METRIC: + metrics.append(element.node_name) + else: + dimensions.append(f"{element.node_name}.{element.name}") + return { + "filename": filename, + "directory": directory, + "build_name": build_name, + "display_name": cube_revision.display_name, + "description": cube_revision.description, + "metrics": metrics, + "dimensions": dimensions, + "columns": [ + { + "name": column.name, + **_partition_config(column), + } + for column in cube_revision.columns + if column.partition + ], + "tags": [tag.name for tag in node.tags], + } + + +def _dimension_links_config(node: Node): + join_links = [ + { + "type": "join", + "dimension_node": link.dimension.name, + "join_type": link.join_type, + "join_on": link.join_sql, + **({"role": link.role} if link.role else {}), + } + for link in node.current.dimension_links + ] + reference_links = [ + { + "type": "reference", + "node_column": column.name, + "dimension": column.dimension.name + + SEPARATOR + + (column.dimension_column or ""), + } + for column in node.current.columns + if column.dimension + ] + return join_links + reference_links + + +async def get_project_config( + session: AsyncSession, + nodes: List[Node], + namespace_requested: str, +) -> List[Dict]: + """ + Returns a project config definition + """ + sorted_nodes = topological_sort(nodes) + project_config_mapping = { + NodeType.SOURCE: _source_project_config, + NodeType.TRANSFORM: _transform_project_config, + NodeType.DIMENSION: _dimension_project_config, + NodeType.METRIC: _metric_project_config, + } + project_components = [ + project_config_mapping[node.type]( + node=node, + namespace_requested=namespace_requested, + ) + if node.type in project_config_mapping + else await _cube_project_config( + session=session, + node=node, + namespace_requested=namespace_requested, + ) + for node in sorted_nodes + ] + return project_components diff --git a/datajunction-server/datajunction_server/internal/nodes.py b/datajunction-server/datajunction_server/internal/nodes.py new file mode 100644 index 000000000..ccd43c87f --- /dev/null +++ b/datajunction-server/datajunction_server/internal/nodes.py @@ -0,0 +1,2177 @@ +"""Nodes endpoint helper functions""" + +import logging +from collections import defaultdict +from datetime import datetime +from http import HTTPStatus +from typing import Dict, List, Optional, Union + +from fastapi import BackgroundTasks +from fastapi.responses import JSONResponse +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload, selectinload + +from datajunction_server.api.catalogs import UNKNOWN_CATALOG_ID +from datajunction_server.api.helpers import ( + get_attribute_type, + get_node_by_name, + map_dimensions_to_roles, + resolve_downstream_references, + validate_cube, +) +from datajunction_server.construction.build_v2 import compile_node_ast +from datajunction_server.database.attributetype import AttributeType, ColumnAttribute +from datajunction_server.database.column import Column +from datajunction_server.database.dimensionlink import DimensionLink +from datajunction_server.database.history import ActivityType, EntityType, History +from datajunction_server.database.materialization import Materialization +from datajunction_server.database.metricmetadata import MetricMetadata +from datajunction_server.database.node import MissingParent, Node, NodeRevision +from datajunction_server.database.partition import Partition +from datajunction_server.database.user import User +from datajunction_server.errors import ( + DJDoesNotExistException, + DJError, + DJException, + DJInvalidInputException, + DJNodeNotFound, + ErrorCode, +) +from datajunction_server.internal.materializations import ( + create_new_materialization, + schedule_materialization_jobs, +) +from datajunction_server.internal.validation import NodeValidator, validate_node_data +from datajunction_server.models import access +from datajunction_server.models.attribute import ( + AttributeTypeIdentifier, + ColumnAttributes, + UniquenessScope, +) +from datajunction_server.models.base import labelize +from datajunction_server.models.cube import CubeElementMetadata, CubeRevisionMetadata +from datajunction_server.models.dimensionlink import ( + JoinLinkInput, + JoinType, + LinkDimensionIdentifier, +) +from datajunction_server.models.history import status_change_history +from datajunction_server.models.materialization import ( + MaterializationConfigOutput, + MaterializationJobTypeEnum, + UpsertMaterialization, +) +from datajunction_server.models.cube_materialization import UpsertCubeMaterialization +from datajunction_server.models.node import ( + DEFAULT_DRAFT_VERSION, + DEFAULT_PUBLISHED_VERSION, + ColumnOutput, + CreateCubeNode, + CreateNode, + CreateSourceNode, + LineageColumn, + NodeMode, + NodeStatus, + UpdateNode, +) +from datajunction_server.models.node_type import NodeType +from datajunction_server.naming import amenable_name, from_amenable_name +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.sql.dag import ( + get_downstream_nodes, + get_nodes_with_dimension, + topological_sort, +) +from datajunction_server.sql.parsing import ast +from datajunction_server.sql.parsing.ast import CompileContext +from datajunction_server.sql.parsing.backends.antlr4 import parse +from datajunction_server.typing import UTCDatetime +from datajunction_server.utils import SEPARATOR, Version, VersionUpgrade, get_settings + +_logger = logging.getLogger(__name__) + +settings = get_settings() + + +def get_node_column(node: Node, column_name: str) -> Column: + """ + Gets the specified column on a node + """ + column_map = {column.name: column for column in node.current.columns} + if column_name not in column_map: + raise DJDoesNotExistException( + message=f"Column `{column_name}` does not exist on node `{node.name}`!", + ) + column = column_map[column_name] + return column + + +async def validate_and_build_attribute( + session: AsyncSession, + column: Column, + attribute: AttributeTypeIdentifier, + node: Node, +) -> ColumnAttribute: + """ + Run some validation and build column attribute. + """ + # Verify attribute type exists + attribute_type = await get_attribute_type( + session, + attribute.name, + attribute.namespace, + ) + if not attribute_type: + raise DJDoesNotExistException( + message=f"Attribute type `{attribute.namespace}" + f".{attribute.name}` " + f"does not exist!", + ) + + # Verify that the attribute type is allowed for this node + if node.type not in attribute_type.allowed_node_types: + raise DJInvalidInputException( + message=f"Attribute type `{attribute.namespace}.{attribute_type.name}` " + f"not allowed on node type `{node.type}`!", + ) + + return ColumnAttribute( + attribute_type=attribute_type, + column=column, + ) + + +async def set_node_column_attributes( + session: AsyncSession, + node: Node, + column_name: str, + attributes: List[AttributeTypeIdentifier], + current_user: User, +) -> List[Column]: + """ + Sets the column attributes on the node if allowed. + """ + column = get_node_column(node, column_name) + all_columns_map = {column.name: column for column in node.current.columns} + + existing_attributes = column.attributes + existing_attributes_map = { + attr.attribute_type.name: attr for attr in existing_attributes + } + await session.refresh(column) + column.attributes = [] + for attribute in attributes: + if attribute.name in existing_attributes_map: + column.attributes.append(existing_attributes_map[attribute.name]) + else: + column.attributes.append( + await validate_and_build_attribute(session, column, attribute, node), + ) + + # Validate column attributes by building mapping between + # attribute scope and columns + attributes_columns_map = defaultdict(set) + all_columns = all_columns_map.values() + + for _col in all_columns: + for attribute in _col.attributes: + scopes_map = { + UniquenessScope.NODE: attribute.attribute_type, + UniquenessScope.COLUMN_TYPE: _col.type, + } + attributes_columns_map[ + ( # type: ignore + attribute.attribute_type, + tuple( + scopes_map[item] + for item in attribute.attribute_type.uniqueness_scope + ), + ) + ].add(_col.name) + + for (attribute, _), columns in attributes_columns_map.items(): + if len(columns) > 1 and attribute.uniqueness_scope: + column.attributes = existing_attributes + raise DJInvalidInputException( + message=f"The column attribute `{attribute.name}` is scoped to be " + f"unique to the `{attribute.uniqueness_scope}` level, but there " + "is more than one column tagged with it: " + f"`{', '.join(sorted(list(columns)))}`", + ) + + session.add(column) + session.add( + History( + entity_type=EntityType.COLUMN_ATTRIBUTE, + node=node.name, + activity_type=ActivityType.SET_ATTRIBUTE, + details={ + "column": column.name, + "attributes": [attr.dict() for attr in attributes], + }, + user=current_user.username, + ), + ) + await session.commit() + await session.refresh(column) + return [column] + + +async def create_node_revision( + data: CreateNode, + node_type: NodeType, + session: AsyncSession, + current_user: User, +) -> NodeRevision: + """ + Create a non-source node revision. + """ + node_revision = NodeRevision( + name=data.name, + display_name=( + data.display_name + if data.display_name + else labelize(data.name.split(SEPARATOR)[-1]) + ), + description=data.description, + type=node_type, + status=NodeStatus.VALID, + query=data.query, + mode=data.mode, + required_dimensions=data.required_dimensions or [], + created_by_id=current_user.id, + custom_metadata=data.custom_metadata, + ) + node_validator = await validate_node_data(node_revision, session) + + if node_validator.status == NodeStatus.INVALID: + if node_revision.mode == NodeMode.DRAFT: + node_revision.status = NodeStatus.INVALID + else: + raise DJException( + http_status_code=HTTPStatus.BAD_REQUEST, + errors=node_validator.errors, + ) + else: + node_revision.status = NodeStatus.VALID + node_revision.missing_parents = [ + MissingParent(name=missing_parent) + for missing_parent in node_validator.missing_parents_map + ] + node_revision.required_dimensions = node_validator.required_dimensions + node_revision.metric_metadata = ( + MetricMetadata.from_input(data.metric_metadata) + if node_type == NodeType.METRIC and data.metric_metadata is not None + else None + ) + new_parents = [node.name for node in node_validator.dependencies_map] + catalog_ids = [ + node.catalog_id for node in node_validator.dependencies_map if node.catalog_id + ] + if node_revision.mode == NodeMode.PUBLISHED and not len(set(catalog_ids)) <= 1: + raise DJException( + f"Cannot create nodes with multi-catalog dependencies: {set(catalog_ids)}", + ) + catalog_id = next(iter(catalog_ids), settings.default_catalog_id) + parent_refs = ( + ( + await session.execute( + select(Node).where( + Node.name.in_( # type: ignore + new_parents, + ), + ), + ) + ) + .unique() + .scalars() + .all() + ) + node_revision.parents = parent_refs + + _logger.info( + "Parent nodes for %s (%s): %s", + data.name, + node_revision.version, + [p.name for p in node_revision.parents], + ) + node_revision.columns = node_validator.columns or [] + if node_revision.type == NodeType.METRIC: + if node_revision.columns: + node_revision.columns[0].display_name = node_revision.display_name + node_revision.catalog_id = catalog_id + return node_revision + + +async def create_cube_node_revision( + session: AsyncSession, + data: CreateCubeNode, + current_user: User, +) -> NodeRevision: + """ + Create a cube node revision. + """ + ( + metric_columns, + metric_nodes, + dimension_nodes, + dimension_columns, + catalog, + ) = await validate_cube( + session, + data.metrics, + data.dimensions, + require_dimensions=True, + ) + status = ( + NodeStatus.VALID + if ( + all(metric.current.status == NodeStatus.VALID for metric in metric_nodes) + and all(dim.current.status == NodeStatus.VALID for dim in dimension_nodes) + ) + else NodeStatus.INVALID + ) + + # Build the "columns" for this node based on the cube elements. These are used + # for marking partition columns when the cube gets materialized. + node_columns = [] + dimension_to_roles_mapping = map_dimensions_to_roles(data.dimensions) + for idx, col in enumerate(metric_columns + dimension_columns): + await session.refresh(col, ["node_revisions"]) + referenced_node = col.node_revision() + full_element_name = ( + referenced_node.name # type: ignore + if referenced_node.type == NodeType.METRIC # type: ignore + else f"{referenced_node.name}.{col.name}" # type: ignore + ) + node_column = Column( + name=full_element_name, + display_name=col.display_name, + type=col.type, + attributes=[ + ColumnAttribute(attribute_type_id=attr.attribute_type_id) + for attr in col.attributes + ], + order=idx, + ) + if full_element_name in dimension_to_roles_mapping: + node_column.dimension_column = dimension_to_roles_mapping[full_element_name] + + node_columns.append(node_column) + + node_revision = NodeRevision( + name=data.name, + display_name=data.display_name or labelize(data.name.split(SEPARATOR)[-1]), + description=data.description, + type=NodeType.CUBE, + query="", + columns=node_columns, + cube_elements=metric_columns + dimension_columns, + parents=list(set(dimension_nodes + metric_nodes)), + status=status, + catalog=catalog, + created_by_id=current_user.id, + ) + return node_revision + + +async def save_node( + session: AsyncSession, + node_revision: NodeRevision, + node: Node, + node_mode: NodeMode, + current_user: User, +): + """ + Saves the newly created node revision + Links the node and node revision together and saves them + """ + node_revision.node = node + node_revision.version = ( + str(DEFAULT_DRAFT_VERSION) + if node_mode == NodeMode.DRAFT + else str(DEFAULT_PUBLISHED_VERSION) + ) + node.current_version = node_revision.version + node_revision.extra_validation() + + session.add(node) + session.add( + History( + node=node.name, + entity_type=EntityType.NODE, + entity_name=node.name, + activity_type=ActivityType.CREATE, + user=current_user.username, + ), + ) + await session.commit() + await session.refresh(node, ["current"]) + + newly_valid_nodes = await resolve_downstream_references( + session=session, + node_revision=node_revision, + current_user=current_user, + ) + await propagate_valid_status( + session=session, + valid_nodes=newly_valid_nodes, + catalog_id=node.current.catalog_id, + current_user=current_user, + ) + await session.refresh(node.current) + + +async def copy_to_new_node( + session: AsyncSession, + existing_node_name: str, + new_name: str, + current_user: User, +) -> Node: + """ + Copies the existing node to a new node with a new name. + """ + node = await Node.get_by_name( + session, + existing_node_name, + options=[ + joinedload(Node.current).options( + *NodeRevision.default_load_options(), + joinedload(NodeRevision.missing_parents), + ), + joinedload(Node.tags), + ], + ) + old_revision = node.current # type: ignore + new_node = Node( + name=new_name, + type=node.type, # type: ignore + display_name=node.display_name, # type: ignore + namespace=".".join(new_name.split(".")[:-1]), + current_version=node.current_version, # type: ignore + created_at=node.created_at, # type: ignore + deactivated_at=node.deactivated_at, # type: ignore + tags=node.tags, # type: ignore + missing_table=node.missing_table, # type: ignore + created_by_id=current_user.id, + ) + new_revision = NodeRevision( + name=new_name, + display_name=old_revision.display_name, + type=old_revision.type, + description=old_revision.description, + query=old_revision.query, + mode=old_revision.mode, + version=old_revision.version, + node=new_node, + catalog=old_revision.catalog, + schema_=old_revision.schema_, + table=old_revision.table, + required_dimensions=[col.copy() for col in old_revision.required_dimensions], + metric_metadata=old_revision.metric_metadata, + cube_elements=list(old_revision.cube_elements), + status=old_revision.status, + parents=old_revision.parents, + missing_parents=[ + MissingParent(name=missing_parent.name) + for missing_parent in old_revision.missing_parents + ], + columns=[col.copy() for col in old_revision.columns], + # TODO: availability and materializations are missing here + lineage=old_revision.lineage, + created_by_id=current_user.id, + custom_metadata=old_revision.custom_metadata, + ) + + # Assemble new dimension links, where each link will need to have their join SQL rewritten + new_dimension_links = [] + for link in old_revision.dimension_links: + join_ast = link.join_sql_ast() + for col in join_ast.find_all(ast.Column): + if str(col.alias_or_name.namespace) == old_revision.name: + col.alias_or_name.namespace = ast.Name(new_name) + new_join_sql = str( + join_ast.select.from_.relations[-1].extensions[-1].criteria.on, + ) + new_dimension_links.append( + DimensionLink( + node_revision=new_revision, + dimension_id=link.dimension_id, + join_sql=new_join_sql, + join_type=link.join_type, + join_cardinality=link.join_cardinality, + materialization_conf=link.materialization_conf, + ), + ) + new_revision.dimension_links = new_dimension_links + + # Reset the version of the new node + new_revision.version = ( + str(DEFAULT_DRAFT_VERSION) + if new_revision.mode == NodeMode.DRAFT + else str(DEFAULT_PUBLISHED_VERSION) + ) + new_node.current_version = new_revision.version + session.add(new_revision) + session.add(new_node) + + # Add a history event recording the copy + session.add( + History( + node=new_node.name, + entity_type=EntityType.NODE, + entity_name=new_node.name, + activity_type=ActivityType.CREATE, + user=current_user.username, + details={"copied_from": node.name}, # type: ignore + ), + ) + await session.commit() + + # If the new node makes any downstream nodes valid, propagate + newly_valid_nodes = await resolve_downstream_references( + session=session, + node_revision=new_revision, + current_user=current_user, + ) + await propagate_valid_status( + session=session, + valid_nodes=newly_valid_nodes, + catalog_id=node.current.catalog_id, # type: ignore + current_user=current_user, + ) + await session.refresh(node.current) # type: ignore + return node # type: ignore + + +async def update_any_node( + name: str, + data: UpdateNode, + session: AsyncSession, + request_headers: Dict[str, str], + query_service_client: QueryServiceClient, + current_user: User, + background_tasks: BackgroundTasks = None, + validate_access: access.ValidateAccessFn = None, +) -> Node: + """ + Node update helper function that handles updating any node + """ + node = await Node.get_by_name( + session, + name, + for_update=True, + include_inactive=True, + options=[ + selectinload(Node.current).options(*NodeRevision.default_load_options()), + ], + raise_if_not_exists=True, + ) + if node.type == NodeType.CUBE: # type: ignore + node = await Node.get_cube_by_name(session, name) + node_revision = await update_cube_node( + session, + node.current, # type: ignore + data, + request_headers=request_headers, + query_service_client=query_service_client, + current_user=current_user, + background_tasks=background_tasks, + validate_access=validate_access, # type: ignore + ) + return node_revision.node if node_revision else node + return await update_node_with_query( + name, + data, + session, + request_headers=request_headers, + query_service_client=query_service_client, + current_user=current_user, + background_tasks=background_tasks, + validate_access=validate_access, # type: ignore + ) + + +async def update_node_with_query( + name: str, + data: UpdateNode, + session: AsyncSession, + *, + request_headers: Dict[str, str], + query_service_client: QueryServiceClient, + current_user: User, + background_tasks: BackgroundTasks, + validate_access: access.ValidateAccessFn, +) -> Node: + """ + Update the named node with the changes defined in the UpdateNode object. + Propagate these changes to all of the node's downstream children. + + Note: this function works for both source nodes and nodes with query (transforms, + dimensions, metrics). We should update it to separate out the logic for source nodes + """ + node = await Node.get_by_name( + session, + name, + options=[ + joinedload(Node.current).options(*NodeRevision.default_load_options()), + ], + # for_update=True, + include_inactive=True, + ) + old_revision = node.current # type: ignore + new_revision = await create_new_revision_from_existing( + session=session, + old_revision=old_revision, + node=node, # type: ignore + current_user=current_user, + data=data, + ) + + if not new_revision: + return node # type: ignore + + node.current_version = new_revision.version # type: ignore + + new_revision.extra_validation() + + session.add(new_revision) + session.add(node) + session.add(node_update_history_event(new_revision, current_user)) + + if new_revision.status != old_revision.status: # type: ignore + session.add( + status_change_history( + new_revision, # type: ignore + old_revision.status, + new_revision.status, # type: ignore + current_user=current_user, + ), + ) + await session.commit() + + await session.refresh(new_revision) + await session.refresh(node) + + # Handle materializations: Note that this must be done after we commit the new revision, + # as otherwise the SQL build won't know about the new revision's query + await session.refresh(old_revision, ["materializations"]) + await session.refresh(old_revision, ["columns"]) + active_materializations = [ + mat for mat in old_revision.materializations if not mat.deactivated_at + ] + + await session.refresh(new_revision, ["materializations"]) + if active_materializations and new_revision.query != old_revision.query: + for old in active_materializations: + new_revision.materializations.append( + await create_new_materialization( + session, + new_revision, + UpsertMaterialization( # type: ignore + name=old.name, + config=old.config, + schedule=old.schedule, + strategy=old.strategy, + job=MaterializationJobTypeEnum.find_match(old.job), + ) + if old.job != MaterializationJobTypeEnum.DRUID_CUBE.value.job_class + else ( + UpsertCubeMaterialization( + job=MaterializationJobTypeEnum.find_match(old.job), + strategy=old.strategy, + schedule=old.schedule, + lookback_window=old.lookback_window, + ) + ), + validate_access, + current_user=current_user, + ), + ) + background_tasks.add_task( + schedule_materialization_jobs, + session=session, + node_revision_id=node.current.id, # type: ignore + materialization_names=[ + mat.name + for mat in node.current.materializations # type: ignore + ], + query_service_client=query_service_client, + request_headers=request_headers, + ) + session.add(new_revision) + await session.commit() + + if background_tasks: # pragma: no cover + background_tasks.add_task( + save_column_level_lineage, + session=session, + node_revision=new_revision, + ) + # TODO: Do not save this until: + # 1. We get to the bottom of why there are query building discrepancies + # 2. We audit our database calls to defer pulling the query_ast in most cases + # background_tasks.add_task( + # save_query_ast, + # session=session, + # node_name=new_revision.name, + # ) + + history_events = {} + old_columns_map = {col.name: col.type for col in old_revision.columns} + + await session.refresh(new_revision, ["columns"]) + await session.refresh(old_revision) + history_events[node.name] = { # type: ignore + "name": node.name, # type: ignore + "current_version": node.current_version, # type: ignore + "previous_version": old_revision.version, + "updated_columns": [ + col.name + for col in new_revision.columns # type: ignore + if col.name not in old_columns_map or old_columns_map[col.name] != col.type + ], + } + + background_tasks.add_task( + propagate_update_downstream, + session, + node, + current_user=current_user, + ) + await session.refresh(node, ["current"]) + await session.refresh(node.current, ["materializations"]) # type: ignore + return node # type: ignore + + +def has_minor_changes( + old_revision: NodeRevision, + data: UpdateNode, +): + """ + Whether the node has minor changes + """ + return ( + (data and data.description and old_revision.description != data.description) + or (data and data.mode and old_revision.mode != data.mode) + or ( + data + and data.display_name + and old_revision.display_name != data.display_name + ) + ) + + +def node_update_history_event(new_revision: NodeRevision, current_user: User): + """ + History event for node updates + """ + return History( + entity_type=EntityType.NODE, + entity_name=new_revision.name, + node=new_revision.name, + activity_type=ActivityType.UPDATE, + details={ + "version": new_revision.version, # type: ignore + }, + user=current_user.username, + ) + + +async def update_cube_node( + session: AsyncSession, + node_revision: NodeRevision, + data: UpdateNode, + *, + request_headers: Dict[str, str], + query_service_client: QueryServiceClient, + current_user: User, + background_tasks: BackgroundTasks = None, + validate_access: access.ValidateAccessFn, +) -> Optional[NodeRevision]: + """ + Update cube node based on changes + """ + node = await Node.get_cube_by_name(session, node_revision.name) + node_revision = node.current # type: ignore + minor_changes = has_minor_changes(node_revision, data) + old_metrics = [m.name for m in node_revision.cube_metrics()] + old_dimensions = node_revision.cube_dimensions() + major_changes = (data.metrics and data.metrics != old_metrics) or ( + data.dimensions and data.dimensions != old_dimensions + ) + create_cube = CreateCubeNode( + name=node_revision.name, + display_name=data.display_name or node_revision.display_name, + description=data.description or node_revision.description, + metrics=data.metrics or old_metrics, + dimensions=data.dimensions or old_dimensions, + mode=data.mode or node_revision.mode, + filters=data.filters or [], + orderby=data.orderby or None, + limit=data.limit or None, + ) + if not major_changes and not minor_changes: + return None + + new_cube_revision = await create_cube_node_revision( + session, + create_cube, + current_user, + ) + + old_version = Version.parse(node_revision.version) + if major_changes: + new_cube_revision.version = str(old_version.next_major_version()) + elif minor_changes: # pragma: no cover + new_cube_revision.version = str(old_version.next_minor_version()) + new_cube_revision.node = node_revision.node + new_cube_revision.node.current_version = new_cube_revision.version # type: ignore + + session.add(node_update_history_event(new_cube_revision, current_user)) + + # Bring over existing partition columns, if any + new_columns_mapping = {col.name: col for col in new_cube_revision.columns} + for col in node_revision.columns: + new_col = new_columns_mapping.get(col.name) + if col.partition and new_col: + new_col.partition = Partition( + column=new_col, + type_=col.partition.type_, + format=col.partition.format, + granularity=col.partition.granularity, + ) + + # Update existing materializations + active_materializations = [ + mat + for mat in node_revision.materializations + if not mat.deactivated_at and mat.name != "default" + ] + if major_changes and active_materializations: + for old in active_materializations: + new_cube_revision.materializations.append( + await create_new_materialization( + session, + new_cube_revision, + UpsertMaterialization( + **MaterializationConfigOutput.from_orm(old).dict( + exclude={"job"}, + ), + job=MaterializationJobTypeEnum.find_match(old.job), + ), + validate_access, + current_user=current_user, + ), + ) + session.add( + History( + entity_type=EntityType.MATERIALIZATION, + entity_name=old.name, + node=node_revision.name, + activity_type=ActivityType.UPDATE, + details={}, + user=current_user.username, + ), + ) + session.add(new_cube_revision) + session.add(new_cube_revision.node) + await session.commit() + + await session.refresh(new_cube_revision, ["materializations"]) + if background_tasks: + background_tasks.add_task( # pragma: no cover + schedule_materialization_jobs, + session=session, + node_revision_id=new_cube_revision.id, + materialization_names=[ + mat.name for mat in new_cube_revision.materializations + ], + query_service_client=query_service_client, + request_headers=request_headers, + ) + else: + await schedule_materialization_jobs( # pragma: no cover + session=session, + node_revision_id=new_cube_revision.id, + materialization_names=[ + mat.name for mat in new_cube_revision.materializations + ], + query_service_client=query_service_client, + request_headers=request_headers, + ) + + await session.refresh(new_cube_revision) + await session.refresh(new_cube_revision.node) + await session.refresh(new_cube_revision.node.current) + return new_cube_revision + + +async def propagate_update_downstream( + session: AsyncSession, + node: Node, + *, + current_user: User, +): + """ + Propagate the updated node's changes to all of its downstream children. + Some potential changes to the upstream node and their effects on downstream nodes: + - altered column names: may invalidate downstream nodes + - altered column types: may invalidate downstream nodes + - new columns: won't affect downstream nodes + """ + _logger.info("Propagating update of node %s downstream", node.name) + downstreams = await get_downstream_nodes( + session, + node.name, + include_deactivated=False, + include_cubes=False, + ) + downstreams = topological_sort(downstreams) + _logger.info( + "Revalidating the following downstreams %s", + [downstream.name for downstream in downstreams], + ) + + # The downstreams need to be sorted topologically in order for the updates to be done + # in the right order. Otherwise it is possible for a leaf node like a metric to be updated + # before its upstreams are updated. + for downstream in downstreams: + original_node_revision = downstream.current + previous_status = original_node_revision.status + node_validator = await revalidate_node( + downstream.name, + session, + current_user=current_user, + ) + + # Record history event + if ( + original_node_revision.version != downstream.current_version + or previous_status != node_validator.status + ): + event = History( + entity_type=EntityType.NODE, + entity_name=downstream.name, + node=downstream.name, + activity_type=ActivityType.UPDATE, + details={ + "changes": { + "updated_columns": sorted(list(node_validator.updated_columns)), + }, + "upstream": { + "node": node.name, + "version": node.current_version, + }, + "reason": f"Caused by update of `{node.name}` to " + f"{node.current_version}", + }, + pre={ + "status": previous_status, + "version": original_node_revision.version, + }, + post={ + "status": node_validator.status, + "version": downstream.current_version, + }, + user=current_user.username, + ) + session.add(event) + await session.commit() + + +def copy_existing_node_revision(old_revision: NodeRevision, current_user: User): + """ + Create an exact copy of the node revision + """ + return NodeRevision( + name=old_revision.name, + version=old_revision.version, + display_name=old_revision.display_name, + description=old_revision.description, + query=old_revision.query, + type=old_revision.type, + columns=old_revision.columns, + catalog=old_revision.catalog, + schema_=old_revision.schema_, + table=old_revision.table, + parents=old_revision.parents, + mode=old_revision.mode, + materializations=old_revision.materializations, + status=old_revision.status, + required_dimensions=old_revision.required_dimensions, + metric_metadata=old_revision.metric_metadata, + dimension_links=[ + DimensionLink( + dimension_id=link.dimension_id, + join_sql=link.join_sql, + join_type=link.join_type, + join_cardinality=link.join_cardinality, + materialization_conf=link.materialization_conf, + ) + for link in old_revision.dimension_links + ], + created_by_id=current_user.id, + custom_metadata=old_revision.custom_metadata, + ) + + +async def create_node_from_inactive( + new_node_type: NodeType, + data: Union[CreateSourceNode, CreateNode, CreateCubeNode], + session: AsyncSession, + *, + current_user: User, + request_headers: Dict[str, str], + query_service_client: QueryServiceClient, + background_tasks: BackgroundTasks = None, + validate_access: access.ValidateAccessFn = None, +) -> Optional[Node]: + """ + If the node existed and is inactive the re-creation takes different steps than + creating it from scratch. + """ + previous_inactive_node = await Node.get_by_name( + session, + data.name, + options=[ + joinedload(Node.current).options(*NodeRevision.default_load_options()), + ], + raise_if_not_exists=False, + include_inactive=True, + ) + if previous_inactive_node and previous_inactive_node.deactivated_at: + if previous_inactive_node.type != new_node_type: + raise DJInvalidInputException( # pragma: no cover + message=f"A node with name `{data.name}` of a `{previous_inactive_node.type.value}` " + "type existed before. If you want to re-create it with a different type, " + "you need to remove all traces of the previous node with a hard delete call: " + "DELETE /nodes/{node_name}/hard", + http_status_code=HTTPStatus.CONFLICT, + ) + if new_node_type != NodeType.CUBE: + update_node = UpdateNode( + # MutableNodeFields + display_name=data.display_name, + description=data.description, + mode=data.mode, + ) + if isinstance(data, CreateSourceNode): + update_node.catalog = data.catalog + update_node.schema_ = data.schema_ + update_node.table = data.table + update_node.columns = data.columns + update_node.missing_table = data.missing_table + + if isinstance(data, CreateNode): + update_node.query = data.query + + await update_node_with_query( + name=data.name, + data=update_node, + session=session, + request_headers=request_headers, + query_service_client=query_service_client, + current_user=current_user, + background_tasks=background_tasks, + validate_access=validate_access, # type: ignore + ) + else: + await update_cube_node( + session, + previous_inactive_node.current, + data, + request_headers=request_headers, + query_service_client=query_service_client, + current_user=current_user, + background_tasks=background_tasks, + validate_access=validate_access, # type: ignore + ) + try: + await activate_node( + name=data.name, + session=session, + current_user=current_user, + ) + return await get_node_by_name(session, data.name, with_current=True) + except Exception as exc: # pragma: no cover + raise DJException( + f"Restoring node `{data.name}` failed: {exc}", + ) from exc + + return None + + +async def create_new_revision_from_existing( + session: AsyncSession, + old_revision: NodeRevision, + node: Node, + current_user: User, + data: UpdateNode = None, + version_upgrade: VersionUpgrade = None, +) -> Optional[NodeRevision]: + """ + Creates a new revision from an existing node revision. + """ + # minor changes + minor_changes = ( + (data and data.description and old_revision.description != data.description) + or (data and data.mode and old_revision.mode != data.mode) + or ( + data + and data.display_name + and old_revision.display_name != data.display_name + ) + or ( + data + and data.metric_metadata + and old_revision.metric_metadata != data.metric_metadata + ) + or ( + data + and data.custom_metadata + and old_revision.custom_metadata != data.custom_metadata + ) + ) + + # major changes + query_changes = ( + old_revision.type != NodeType.SOURCE + and data + and data.query + and old_revision.query != data.query + ) + column_changes = ( + old_revision.type == NodeType.SOURCE + and data + and data.columns + and ({col.identifier() for col in old_revision.columns} != data.columns) + ) + pk_changes = data and ( + data.primary_key == [] + and len(old_revision.primary_key()) + or {col.name for col in old_revision.primary_key()} + != set(data.primary_key or []) + ) + required_dim_changes = ( + data + and data.required_dimensions + and {col.name for col in old_revision.required_dimensions} + != set(data.required_dimensions) + ) + major_changes = ( + query_changes or column_changes or pk_changes or required_dim_changes + ) + + # If nothing has changed, do not create the new node revision + if not minor_changes and not major_changes and not version_upgrade: + return None + + old_version = Version.parse(node.current_version) + new_mode = data.mode if data and data.mode else old_revision.mode + new_revision = NodeRevision( + name=old_revision.name, + node_id=node.id, + version=str( + old_version.next_major_version() + if major_changes or version_upgrade == VersionUpgrade.MAJOR + else old_version.next_minor_version(), + ), + display_name=( + data.display_name + if data and data.display_name + else old_revision.display_name + ), + description=( + data.description if data and data.description else old_revision.description + ), + query=(data.query if data and data.query else old_revision.query), + type=old_revision.type, + columns=[ + Column( + name=column_data.name, + type=column_data.type, + dimension_column=column_data.dimension, + attributes=column_data.attributes or [], + order=idx, + ) + for idx, column_data in enumerate(data.columns) + ] + if data and data.columns + else old_revision.columns, + catalog=old_revision.catalog, + schema_=old_revision.schema_, + table=old_revision.table, + parents=[], + mode=new_mode, + materializations=[], + status=old_revision.status, + metric_metadata=( + MetricMetadata.from_input(data.metric_metadata) + if data and data.metric_metadata + else old_revision.metric_metadata + ), + dimension_links=[ + DimensionLink( + dimension_id=link.dimension_id, + join_sql=link.join_sql, + join_type=link.join_type, + join_cardinality=link.join_cardinality, + materialization_conf=link.materialization_conf, + ) + for link in old_revision.dimension_links + ], + created_by_id=current_user.id, + custom_metadata=old_revision.custom_metadata, + ) + if data.required_dimensions: # type: ignore + new_revision.required_dimensions = data.required_dimensions # type: ignore + + if data.custom_metadata: # type: ignore + new_revision.custom_metadata = data.custom_metadata # type: ignore + + # Link the new revision to its parents if a new revision was created and update its status + if new_revision.type != NodeType.SOURCE: + node_validator = await validate_node_data(new_revision, session) + new_revision.columns = node_validator.columns + new_revision.status = node_validator.status + if node_validator.errors: + if new_mode == NodeMode.DRAFT: + new_revision.status = NodeStatus.INVALID + else: + raise DJException( + http_status_code=HTTPStatus.BAD_REQUEST, + errors=node_validator.errors, + ) + + # Update dimension links based on new columns + new_column_names = {col.name for col in new_revision.columns} + new_revision.dimension_links = [ + link + for link in old_revision.dimension_links + if link.foreign_key_column_names.intersection(new_column_names) + ] + + new_parents = [n.name for n in node_validator.dependencies_map] + parent_refs = ( + ( + await session.execute( + select(Node) + .where( + Node.name.in_( # type: ignore + new_parents, + ), + ) + .options(joinedload(Node.current)), + ) + ) + .unique() + .scalars() + .all() + ) + new_revision.parents = list(parent_refs) + catalogs = [ + parent.current.catalog_id + for parent in parent_refs + if parent.current.catalog_id + ] + if catalogs: + new_revision.catalog_id = catalogs[0] + new_revision.columns = node_validator.columns or [] + if new_revision.type == NodeType.METRIC: + new_revision.columns[0].display_name = new_revision.display_name + + # Update the primary key if one was set in the input + if data is not None and (data.primary_key or data.primary_key == []): + pk_attribute = ( + await session.execute( + select(AttributeType).where( + AttributeType.name == ColumnAttributes.PRIMARY_KEY.value, + ), + ) + ).scalar_one() + if set(data.primary_key) - set(col.name for col in new_revision.columns): + raise DJInvalidInputException( # pragma: no cover + f"Primary key {data.primary_key} does not exist on {new_revision.name}", + ) + for col in new_revision.columns: + # Remove the primary key attribute if it's not in the updated PK + if col.has_primary_key_attribute() and col.name not in data.primary_key: + col.attributes = [ + attr + for attr in col.attributes + if attr.attribute_type.name + != ColumnAttributes.PRIMARY_KEY.value + ] + # Add (or keep) the primary key attribute if it is in the updated PK + if col.name in data.primary_key and not col.has_primary_key_attribute(): + col.attributes.append( + ColumnAttribute(column=col, attribute_type=pk_attribute), + ) + + # Update the required dimensions if one was set in the input and the node is a metric + if node_validator.required_dimensions and new_revision.type == NodeType.METRIC: + new_revision.required_dimensions = node_validator.required_dimensions + + # Set the node's validity status + invalid_primary_key = ( + new_revision.type == NodeType.DIMENSION and not new_revision.primary_key() + ) + if invalid_primary_key: + new_revision.status = NodeStatus.INVALID + + new_revision.missing_parents = [ + MissingParent(name=missing_parent) + for missing_parent in node_validator.missing_parents_map + ] + _logger.info( + "Parent nodes for %s (v%s): %s", + new_revision.name, + new_revision.version, + [p.name for p in new_revision.parents], + ) + return new_revision + + +async def save_column_level_lineage( + session: AsyncSession, + node_revision: NodeRevision, +): + """ + Saves the column-level lineage for a node + """ + node = await Node.get_by_name(session, node_revision.name) + column_level_lineage = await get_column_level_lineage(session, node.current) # type: ignore + node.current.lineage = [lineage.dict() for lineage in column_level_lineage] # type: ignore + session.add(node.current) # type: ignore + await session.commit() + + +async def save_query_ast( # pragma: no cover + session: AsyncSession, + node_name: str, +): + """ + Compile and save query AST for a node + """ + node = await Node.get_by_name(session, node_name) + + if ( + node + and node.current.query + and node.type + in { + NodeType.TRANSFORM, + NodeType.DIMENSION, + } + ): + node.current.query_ast = await compile_node_ast(session, node.current) + + session.add(node.current) # type: ignore + await session.commit() + + +async def get_column_level_lineage( + session: AsyncSession, + node_revision: NodeRevision, +) -> List[LineageColumn]: + """ + Gets the column-level lineage for the node + """ + if node_revision.status == NodeStatus.VALID and node_revision.type not in ( + NodeType.SOURCE, + NodeType.CUBE, + ): + return [ + await column_lineage( + session, + node_revision, + col.name, + ) + for col in node_revision.columns + ] + return [] + + +async def column_lineage( + session: AsyncSession, + node_rev: NodeRevision, + column_name: str, +) -> LineageColumn: + """ + Helper function to determine the lineage for a column on a node. + """ + if node_rev.type == NodeType.SOURCE: + return LineageColumn( + node_name=node_rev.name, + node_type=node_rev.type, + display_name=node_rev.display_name, + column_name=column_name, + lineage=[], + ) + + ctx = CompileContext(session, DJException()) + query = ( + NodeRevision.format_metric_alias( + node_rev.query, # type: ignore + node_rev.name, + ) + if node_rev.type == NodeType.METRIC + else node_rev.query + ) + query_ast = parse(query) + await query_ast.compile(ctx) + query_ast.select.add_aliases_to_unnamed_columns() + + lineage_column = LineageColumn( + column_name=column_name, + node_name=node_rev.name, + node_type=node_rev.type, + display_name=node_rev.display_name, + lineage=[], + ) + + # Find the expression AST for the column on the node + column = [ + col + for col in query_ast.select.projection + if ( # pragma: no cover + col != ast.Null() and col.alias_or_name.name == column_name # type: ignore + ) + ][0] + column_or_child = column.child if isinstance(column, ast.Alias) else column # type: ignore + column_expr = ( + column_or_child.expression # type: ignore + if hasattr(column_or_child, "expression") + else column_or_child + ) + + # At every layer, expand the lineage search tree with all columns referenced + # by the current column's expression. If we reach an actual table with a DJ + # node attached, save this to the lineage record. Otherwise, continue the search + processed = list(column_expr.find_all(ast.Column)) if column_expr else [] + seen = set() + while processed: + current = processed.pop() + if current in seen: + continue + if ( + hasattr(current, "table") + and isinstance(current.table, ast.Table) + and current.table.dj_node + ): + lineage_column.lineage.append( # type: ignore + await column_lineage( + session, + current.table.dj_node, + current.name.name + if not current.is_struct_ref + else current.struct_column_name, + ), + ) + else: + expr_column_deps = ( + list( + current.expression.find_all(ast.Column), + ) + if current.expression + else [] + ) + for col_dep in expr_column_deps: + processed.append(col_dep) + seen.update({current}) + return lineage_column + + +async def derive_sql_column( + cube_element: CubeElementMetadata, +) -> ColumnOutput: + """ + Derives the column name in the generated Cube SQL based on the CubeElement + """ + query_column_name = ( + cube_element.name + if cube_element.type == "metric" + else amenable_name( + f"{cube_element.node_name}{SEPARATOR}{cube_element.name}", + ) + ) + return ColumnOutput( + name=query_column_name, + display_name=cube_element.display_name, + type=cube_element.type, + ) + + +async def get_cube_revision_metadata(session: AsyncSession, name: str): + """ + Returns cube revision metadata for cube named `name`. + """ + statement = ( + select(NodeRevision) + .select_from(Node) + .join( + NodeRevision, + (NodeRevision.name == Node.name) + & (NodeRevision.version == Node.current_version), + ) + .where(Node.name == name) + .options( + selectinload(NodeRevision.columns), + selectinload(NodeRevision.availability), + selectinload(NodeRevision.materializations).selectinload( + Materialization.backfills, + ), + selectinload(NodeRevision.cube_elements) + .selectinload(Column.node_revisions) + .options( + joinedload(NodeRevision.node), + ), + selectinload(NodeRevision.node).options(selectinload(Node.tags)), + ) + ) + result = (await session.execute(statement)).unique().first() + if not result: + raise DJNodeNotFound( # pragma: no cover + message=f"A cube node with name `{name}` does not exist.", + http_status_code=404, + ) + cube = result[0] + + # Preserve the ordering of elements + element_ordering = {col.name: col.order for col in cube.columns} + cube.cube_elements = sorted( + cube.cube_elements, + key=lambda elem: element_ordering.get(from_amenable_name(elem.name), 0), + ) + + cube_metadata = CubeRevisionMetadata.from_orm(cube) + cube_metadata.tags = cube.node.tags + cube_metadata.sql_columns = [ + await derive_sql_column(element) for element in cube_metadata.cube_elements + ] + return cube_metadata + + +async def upsert_complex_dimension_link( + session: AsyncSession, + node_name: str, + link_input: JoinLinkInput, + current_user: User, +) -> ActivityType: + """ + Create or update a node-level dimension link. + + A dimension link is uniquely identified by the origin node, the dimension node being linked, + and the role, if any. If an existing dimension link identified by those fields already exists, + we'll update that dimension link. If no dimension link exists, we'll create a new one. + """ + node = await Node.get_by_name( + session, + node_name, + raise_if_not_exists=True, + ) + if node.type not in (NodeType.SOURCE, NodeType.DIMENSION, NodeType.TRANSFORM): # type: ignore + raise DJInvalidInputException( + message=f"Cannot link dimension to a node of type {node.type}. " # type: ignore + "Must be a source, dimension, or transform node.", + ) + + # Find the dimension node and check that the catalogs match + dimension_node = await Node.get_by_name( + session, + link_input.dimension_node, + ) + if ( + dimension_node.current.catalog_id != UNKNOWN_CATALOG_ID # type: ignore + and dimension_node.current.catalog is not None # type: ignore + and node.current.catalog.name != dimension_node.current.catalog.name # type: ignore + ): + raise DJException( # pragma: no cover + message=( + "Cannot link dimension to node, because catalogs do not match: " + f"{node.current.catalog.name}, " # type: ignore + f"{dimension_node.current.catalog.name}" # type: ignore + ), + ) + + # Parse the join query and do some basic verification of its validity + join_query = parse( + f"SELECT 1 FROM {node_name} " + f"{link_input.join_type} JOIN {link_input.dimension_node} " + + (f"ON {link_input.join_on}" if link_input.join_on else ""), + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await join_query.compile(ctx) + join_relation = join_query.select.from_.relations[0].extensions[0] # type: ignore + + # Verify that the query references both the node and the dimension being joined + expected_references = {node_name, link_input.dimension_node} + references = ( + { + table.name.namespace.identifier() # type: ignore + for table in join_relation.criteria.on.find_all(ast.Column) # type: ignore + } + if join_relation.criteria + else {} + ) + if ( + expected_references.difference(references) + and link_input.join_type != JoinType.CROSS + ): + raise DJInvalidInputException( + f"The join SQL provided does not reference both the origin node {node_name} and the " + f"dimension node {link_input.dimension_node} that it's being joined to.", + ) + + # Verify that the columns in the ON clause exist on both nodes + if ctx.exception.errors: + raise DJInvalidInputException( + message=f"Join query {link_input.join_on} is not valid", + errors=ctx.exception.errors, + ) + + # Find an existing dimension link if there is already one defined for this node + existing_link = [ + link # type: ignore + for link in node.current.dimension_links # type: ignore + if link.dimension_id == dimension_node.id and link.role == link_input.role # type: ignore + ] + activity_type = ActivityType.CREATE + + if existing_link: + # Update the existing dimension link + activity_type = ActivityType.UPDATE + dimension_link = existing_link[0] + dimension_link.join_sql = link_input.join_on + dimension_link.join_type = DimensionLink.parse_join_type( + join_relation.join_type, + ) + dimension_link.join_cardinality = link_input.join_cardinality + else: + # If there is no existing link, create new dimension link object + dimension_link = DimensionLink( + node_revision_id=node.current.id, # type: ignore + dimension_id=dimension_node.id, # type: ignore + join_sql=link_input.join_on, + join_type=DimensionLink.parse_join_type(join_relation.join_type), + join_cardinality=link_input.join_cardinality, + role=link_input.role, + ) + node.current.dimension_links.append(dimension_link) # type: ignore + + # Add/update the dimension link in the database + session.add(dimension_link) + session.add(node.current) # type: ignore + session.add( + History( + entity_type=EntityType.LINK, + entity_name=node.name, # type: ignore + node=node.name, # type: ignore + activity_type=activity_type, + details={ + "dimension": dimension_node.name, # type: ignore + "join_sql": link_input.join_on, + "join_cardinality": link_input.join_cardinality, + "role": link_input.role, + }, + user=current_user.username, + ), + ) + await session.commit() + await session.refresh(node) + return activity_type + + +async def remove_dimension_link( + session: AsyncSession, + node_name: str, + link_identifier: LinkDimensionIdentifier, + current_user: User, +): + """ + Removes the dimension link identified by the origin node, the dimension node, and its role. + """ + node = await Node.get_by_name(session, node_name) + + # Find the dimension node + dimension_node = await get_node_by_name( + session=session, + name=link_identifier.dimension_node, + node_type=NodeType.DIMENSION, + ) + removed = False + + # Find cubes that are affected by this dimension link removal and update their statuses + downstream_cubes = await get_downstream_nodes( + session, + node_name, + node_type=NodeType.CUBE, + ) + for cube in downstream_cubes: + for elem in cube.current.cube_elements: + await session.refresh(elem, ["node_revisions"]) + cube_dimension_nodes = [ + cube_elem_node.name + for (element, cube_elem_node) in cube.current.cube_elements_with_nodes() + if cube_elem_node.type == NodeType.DIMENSION + ] + if dimension_node.name in cube_dimension_nodes: + cube.current.status = NodeStatus.INVALID + session.add(cube) + session.add( + status_change_history( + cube.current, # type: ignore + NodeStatus.VALID, + NodeStatus.INVALID, + current_user=current_user, + ), + ) + await session.commit() + + # Delete the dimension link if one exists + for link in node.current.dimension_links: # type: ignore + if ( + link.dimension_id == dimension_node.id # pragma: no cover + and link.role == link_identifier.role # pragma: no cover + ): + removed = True + await session.delete(link) + if not removed: + return JSONResponse( + status_code=HTTPStatus.NOT_FOUND, + content={ + "message": f"Dimension link to node {link_identifier.dimension_node} " + + (f"with role {link_identifier.role} " if link_identifier.role else "") + + "not found", + }, + ) + + session.add( + History( + entity_type=EntityType.LINK, + entity_name=node.name, # type: ignore + node=node.name, # type: ignore + activity_type=ActivityType.DELETE, + details={ + "dimension": dimension_node.name, + "role": link_identifier.role, + }, + user=current_user.username, + ), + ) + await session.commit() + await session.refresh(node.current) # type: ignore + await session.refresh(node) + return JSONResponse( + status_code=201, + content={ + "message": ( + f"Dimension link {link_identifier.dimension_node} " + + (f"(role {link_identifier.role}) " if link_identifier.role else "") + + f"to node {node_name} has been removed." + ) + if removed + else ( + f"Dimension link {link_identifier.dimension_node} " + + (f"(role {link_identifier.role}) " if link_identifier.role else "") + + f"to node {node_name} does not exist!" + ), + }, + ) + + +async def propagate_valid_status( + session: AsyncSession, + valid_nodes: List[NodeRevision], + catalog_id: int, + current_user: User, +) -> None: + """ + Propagate a valid status by revalidating all downstream nodes + """ + while valid_nodes: + resolved_nodes = [] + for node_revision in valid_nodes: + if node_revision.status != NodeStatus.VALID: + raise DJException( + f"Cannot propagate valid status: Node `{node_revision.name}` is not valid", + ) + downstream_nodes = await get_downstream_nodes( + session=session, + node_name=node_revision.name, + ) + newly_valid_nodes = [] + for node in downstream_nodes: + node_validator = await validate_node_data( + data=node.current, + session=session, + ) + node.current.status = node_validator.status + if node_validator.status == NodeStatus.VALID: + node.current.columns = node_validator.columns or [] + node.current.status = NodeStatus.VALID + node.current.catalog_id = catalog_id + session.add( + status_change_history( + node.current, + NodeStatus.INVALID, + NodeStatus.VALID, + current_user=current_user, + ), + ) + newly_valid_nodes.append(node.current) + session.add(node.current) + await session.commit() + await session.refresh(node.current) + resolved_nodes.extend(newly_valid_nodes) + valid_nodes = resolved_nodes + + +async def deactivate_node( + session: AsyncSession, + name: str, + current_user: User, + message: str = None, +): + """ + Deactivates a node and propagates to all downstreams. + """ + node = await get_node_by_name(session, name, with_current=True) + + # Find all downstream nodes and mark them as invalid + downstreams = await get_downstream_nodes(session, node.name) + for downstream in downstreams: + if downstream.current.status != NodeStatus.INVALID: + downstream.current.status = NodeStatus.INVALID + session.add( + status_change_history( + downstream.current, + NodeStatus.VALID, + NodeStatus.INVALID, + parent_node=node.name, + current_user=current_user, + ), + ) + session.add(downstream) + + now = datetime.utcnow() + node.deactivated_at = UTCDatetime( + year=now.year, + month=now.month, + day=now.day, + hour=now.hour, + minute=now.minute, + second=now.second, + ) + session.add(node) + session.add( + History( + entity_type=EntityType.NODE, + entity_name=node.name, + node=node.name, + activity_type=ActivityType.DELETE, + details={"message": message} if message else {}, + user=current_user.username, + ), + ) + await session.commit() + await session.refresh(node, ["current"]) + + +async def activate_node( + session: AsyncSession, + name: str, + current_user: User, + message: str = None, +): + """Restores node and revalidate all downstreams.""" + node = await get_node_by_name( + session, + name, + with_current=True, + include_inactive=True, + ) + if not node.deactivated_at: + raise DJInvalidInputException( + http_status_code=HTTPStatus.BAD_REQUEST, + message=f"Cannot restore `{name}`, node already active.", + ) + node.deactivated_at = None # type: ignore + + # Find all downstream nodes and revalidate them + downstreams = await get_downstream_nodes(session, node.name) + for downstream in downstreams: + old_status = downstream.current.status + if downstream.type == NodeType.CUBE: + downstream.current.status = NodeStatus.VALID + for element in downstream.current.cube_elements: + await session.refresh(element, ["node_revisions"]) + if ( + element.node_revisions + and element.node_revisions[-1].status == NodeStatus.INVALID + ): # pragma: no cover + downstream.current.status = NodeStatus.INVALID + else: + # We should not fail node restoration just because of some nodes + # that have been invalid already and stay that way. + node_validator = await validate_node_data(downstream.current, session) + downstream.current.status = node_validator.status + if node_validator.errors: + downstream.current.status = NodeStatus.INVALID + session.add(downstream) + if old_status != downstream.current.status: + session.add( + status_change_history( + downstream.current, + old_status, + downstream.current.status, + parent_node=node.name, + current_user=current_user, + ), + ) + + session.add(node) + session.add( + History( + entity_type=EntityType.NODE, + entity_name=node.name, + node=node.name, + activity_type=ActivityType.RESTORE, + details={"message": message} if message else {}, + user=current_user.username, + ), + ) + await session.commit() + + +async def revalidate_node( + name: str, + session: AsyncSession, + current_user: User, + update_query_ast: bool = False, + background_tasks: BackgroundTasks = None, +) -> NodeValidator: + """ + Revalidate a single existing node and update its status appropriately + """ + node = await Node.get_by_name( + session, + name, + options=[ + joinedload(Node.current).options(*NodeRevision.default_load_options()), + joinedload(Node.tags), + ], + raise_if_not_exists=True, + ) + current_node_revision = node.current # type: ignore + + # Revalidate source node + if current_node_revision.type == NodeType.SOURCE: + if current_node_revision.status != NodeStatus.VALID: # pragma: no cover + current_node_revision.status = NodeStatus.VALID + session.add( + status_change_history( + current_node_revision, + NodeStatus.INVALID, + NodeStatus.VALID, + current_user=current_user, + ), + ) + session.add(current_node_revision) + await session.commit() + await session.refresh(current_node_revision) + return NodeValidator( + status=node.current.status, # type: ignore + columns=node.current.columns, # type: ignore + ) + + # Revalidate cube node + if current_node_revision.type == NodeType.CUBE: + cube_node = await Node.get_cube_by_name(session, name) + current_node_revision = cube_node.current # type: ignore + cube_metrics = [metric.name for metric in current_node_revision.cube_metrics()] + cube_dimensions = current_node_revision.cube_dimensions() + errors = [] + try: + await validate_cube( + session, + metric_names=cube_metrics, + dimension_names=cube_dimensions, + require_dimensions=True, + ) + current_node_revision.status = NodeStatus.VALID + except DJException as exc: # pragma: no cover + current_node_revision.status = NodeStatus.INVALID + if exc.errors: + errors.extend(exc.errors) + else: + errors.append( + DJError(code=ErrorCode.INVALID_DIMENSION, message=exc.message), + ) + session.add(current_node_revision) + await session.commit() + return NodeValidator( + status=current_node_revision.status, + columns=current_node_revision.columns, + errors=errors, + ) + + # Revalidate all other node types + node_validator = await validate_node_data(current_node_revision, session) + + # Compile and save query AST + if update_query_ast and background_tasks: + background_tasks.add_task( # pragma: no cover + save_query_ast, + session=session, + node_name=node.name, # type: ignore + ) + + # Update the status + node.current.status = node_validator.status # type: ignore + + existing_columns = {col.name: col for col in node.current.columns} # type: ignore + + # Validate dimension links + to_remove = set() + for link in node.current.dimension_links: # type: ignore + if not link.foreign_key_column_names.intersection(set(existing_columns)): + to_remove.add(link) # pragma: no cover + await session.delete(link) # pragma: no cover + + # Check if any columns have been updated + updated_columns = False + for col in node_validator.columns: + if existing_col := existing_columns.get(col.name): + if existing_col.type != col.type: + existing_col.type = col.type + updated_columns = True + else: + node.current.columns.append(col) # type: ignore # pragma: no cover + updated_columns = True # pragma: no cover + + # Only create a new revision if the columns have been updated + if updated_columns: # type: ignore + new_revision = copy_existing_node_revision(node.current, current_user) # type: ignore + new_revision.version = str( + Version.parse(node.current.version).next_major_version(), # type: ignore + ) + + new_revision.status = node_validator.status + new_revision.lineage = [ + lineage.dict() + for lineage in await get_column_level_lineage(session, new_revision) + ] + + # Save which columns were modified and update the columns with the changes + node_validator.updated_columns = node_validator.modified_columns( + new_revision, # type: ignore + ) + new_revision.columns = node_validator.columns + + # Save the new revision of the child + node.current_version = new_revision.version # type: ignore + new_revision.node_id = node.id # type: ignore + session.add(node) + session.add(new_revision) + await session.commit() + await session.refresh(node.current) # type: ignore + await session.refresh(node, ["current"]) + return node_validator + + +async def hard_delete_node( + name: str, + session: AsyncSession, + current_user: User, +): + """ + Hard delete a node, destroying all links and invalidating all downstream nodes. + This should be used with caution, deactivating a node is preferred. + """ + node = await Node.get_by_name( + session, + name, + options=[joinedload(Node.current), joinedload(Node.revisions)], + include_inactive=True, + raise_if_not_exists=False, + ) + downstream_nodes = await get_downstream_nodes(session=session, node_name=name) + + linked_nodes = [] + if node.type == NodeType.DIMENSION: # type: ignore + linked_nodes = await get_nodes_with_dimension( + session=session, + dimension_node=node, # type: ignore + ) + + await session.delete(node) + await session.commit() + impact = [] # Aggregate all impact of this deletion to include in response + + # Revalidate all downstream nodes + for node in downstream_nodes: + session.add( # Capture this in the downstream node's history + History( + entity_type=EntityType.DEPENDENCY, + entity_name=name, + node=node.name, + activity_type=ActivityType.DELETE, + user=current_user.username, + ), + ) + node_validator = await revalidate_node( + name=node.name, + session=session, + current_user=current_user, + update_query_ast=False, + ) + impact.append( + { + "name": node.name, + "status": node_validator.status, + "effect": "downstream node is now invalid", + }, + ) + + # Revalidate all linked nodes + for node in linked_nodes: + session.add( # Capture this in the downstream node's history + History( + entity_type=EntityType.LINK, + entity_name=name, + node=node.name, + activity_type=ActivityType.DELETE, + user=current_user.username, + ), + ) + node_validator = await revalidate_node( + name=node.name, + session=session, + current_user=current_user, + update_query_ast=False, + ) + impact.append( + { + "name": node.name, + "status": node_validator.status, + "effect": "broken link", + }, + ) + session.add( # Capture this in the downstream node's history + History( + entity_type=EntityType.NODE, + entity_name=name, + node=name, + activity_type=ActivityType.DELETE, + details={ + "impact": impact, + }, + user=current_user.username, + ), + ) + await session.commit() # Commit the history events + return impact diff --git a/datajunction-server/datajunction_server/internal/templates/client_setup.j2 b/datajunction-server/datajunction_server/internal/templates/client_setup.j2 new file mode 100644 index 000000000..baa9bdf3c --- /dev/null +++ b/datajunction-server/datajunction_server/internal/templates/client_setup.j2 @@ -0,0 +1,9 @@ +from datajunction import ( + DJBuilder, Source, Dimension, Transform, Metric, + Namespace, MetricUnit, MetricDirection, ColumnAttribute, +) + +DJ_URL = "{{ request_url }}" + +dj = DJBuilder(DJ_URL) +dj.basic_login("dj", "dj") diff --git a/datajunction-server/datajunction_server/internal/templates/create_cube.j2 b/datajunction-server/datajunction_server/internal/templates/create_cube.j2 new file mode 100644 index 000000000..12b577477 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/templates/create_cube.j2 @@ -0,0 +1,19 @@ +{{ short_name }} = dj.create_cube( + name={% if '{' in name %}f{% else %}{% endif %}"{{ name }}", + display_name="{{ display_name }}", + description="""{{ description }}""", + dimensions=[{%- for dimension in dimensions %} + {% if '{' in dimension %}f{% else %}{% endif %}"{{ dimension }}", + {%- endfor %} + ], + metrics=[{%- for metric in metrics %} + {% if '{' in metric %}f{% else %}{% endif %}"{{ metric }}", + {%- endfor %} + ], + mode="{{ mode }}", + tags=[{%- for tag in tags %} + dj.tag("{{ tag }}"), + {%- endfor %}{% if tags %} + {% else %}{% endif %}], + update_if_exists=True, +) diff --git a/datajunction-server/datajunction_server/internal/templates/create_dimension.j2 b/datajunction-server/datajunction_server/internal/templates/create_dimension.j2 new file mode 100644 index 000000000..b860120ca --- /dev/null +++ b/datajunction-server/datajunction_server/internal/templates/create_dimension.j2 @@ -0,0 +1,16 @@ +{{ short_name }} = dj.create_dimension( + name={% if '{' in name %}f{% else %}{% endif %}"{{ name }}", + display_name="{{ display_name }}", + description="""{{ description }}""", + mode="{{ mode }}", + primary_key=[{%- for column in primary_key %} + "{{ column }}", + {%- endfor %}{% if primary_key %} + {% else %}{% endif %}], + tags=[{%- for tag in tags %} + dj.tag("{{ tag }}"), + {%- endfor %}{% if tags %} + {% else %}{% endif %}], + query={% if '{' in query %}f{% else %}{% endif %}"""{{ query }}""", + update_if_exists=True, +) diff --git a/datajunction-server/datajunction_server/internal/templates/create_metric.j2 b/datajunction-server/datajunction_server/internal/templates/create_metric.j2 new file mode 100644 index 000000000..042d9addb --- /dev/null +++ b/datajunction-server/datajunction_server/internal/templates/create_metric.j2 @@ -0,0 +1,18 @@ +{{ short_name }} = dj.create_metric( + name={% if '{' in name %}f{% else %}{% endif %}"{{ name }}", + display_name="{{ display_name }}", + description="""{{ description }}""", + mode="{{ mode }}", + required_dimensions=[{%- for dimension in required_dimensions %} + {% if '{' in dimension %}f{% else %}{% endif %}"{{ dimension }}", + {%- endfor %}{% if required_dimensions %} + {% else %}{% endif %}], + tags=[{%- for tag in tags %} + dj.tag("{{ tag }}"), + {%- endfor %}{% if tags %} + {% else %}{% endif %}], + query={% if '{' in query %}f{% else %}{% endif %}"""{{ query }}""",{% if direction %} + direction={{ direction }},{% else %}{% endif %}{% if unit %} + unit={{ unit }},{% else %}{% endif %} + update_if_exists=True, +) diff --git a/datajunction-server/datajunction_server/internal/templates/create_transform.j2 b/datajunction-server/datajunction_server/internal/templates/create_transform.j2 new file mode 100644 index 000000000..7d4acfd02 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/templates/create_transform.j2 @@ -0,0 +1,16 @@ +{{ short_name }} = dj.create_transform( + name={% if '{' in name %}f{% else %}{% endif %}"{{ name }}", + display_name="{{ display_name }}", + description="""{{ description }}""", + mode="{{ mode }}", + primary_key=[{%- for column in primary_key %} + "{{ column }}", + {%- endfor %}{% if primary_key %} + {% else %}{% endif %}], + tags=[{%- for tag in tags %} + dj.tag("{{ tag }}"), + {%- endfor %}{% if tags %} + {% else %}{% endif %}], + query={% if '{' in query %}f{% else %}{% endif %}"""{{ query }}""", + update_if_exists=True, +) diff --git a/datajunction-server/datajunction_server/internal/templates/link_dimension.j2 b/datajunction-server/datajunction_server/internal/templates/link_dimension.j2 new file mode 100644 index 000000000..4e9a58c48 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/templates/link_dimension.j2 @@ -0,0 +1,6 @@ +{{ node_short_name }}.link_complex_dimension( + dimension_node=f"{{ dimension_node }}", + join_on=f"{{ join_on }}", + join_type="{{ join_type }}",{% if role %} + role="{{ role }}",{% else %}{% endif %} +) diff --git a/datajunction-server/datajunction_server/internal/templates/namespace_mapping.j2 b/datajunction-server/datajunction_server/internal/templates/namespace_mapping.j2 new file mode 100644 index 000000000..e2e3ad34b --- /dev/null +++ b/datajunction-server/datajunction_server/internal/templates/namespace_mapping.j2 @@ -0,0 +1,9 @@ +# A mapping from current namespaces to new namespaces +# Note: Editing the mapping will result in the nodes under that namespace getting +# copied to the new namespace + +NAMESPACE_MAPPING = { + {%- for namespace in namespaces %} + "{{ namespace }}": "{{ namespace }}", + {%- endfor %} +} diff --git a/datajunction-server/datajunction_server/internal/templates/register_table.j2 b/datajunction-server/datajunction_server/internal/templates/register_table.j2 new file mode 100644 index 000000000..34c59b1dc --- /dev/null +++ b/datajunction-server/datajunction_server/internal/templates/register_table.j2 @@ -0,0 +1,5 @@ +dj.register_table( + catalog="{{ catalog }}", + schema="{{ schema }}", + table="{{ table }}", +) diff --git a/datajunction-server/datajunction_server/internal/templates/set_column_attributes.j2 b/datajunction-server/datajunction_server/internal/templates/set_column_attributes.j2 new file mode 100644 index 000000000..a17556976 --- /dev/null +++ b/datajunction-server/datajunction_server/internal/templates/set_column_attributes.j2 @@ -0,0 +1,7 @@ +{{ node_short_name }}.set_column_attributes( + "{{ column_name }}", + [{%- for attribute in attributes %} + ColumnAttribute(namespace="{{ attribute.namespace }}", name="{{ attribute.name }}"), + {%- endfor %}{% if attributes %} + {% else %}{% endif %}], +) diff --git a/datajunction-server/datajunction_server/internal/validation.py b/datajunction-server/datajunction_server/internal/validation.py new file mode 100644 index 000000000..3ac6cc5ef --- /dev/null +++ b/datajunction-server/datajunction_server/internal/validation.py @@ -0,0 +1,215 @@ +"""Node validation functions.""" + +from dataclasses import dataclass, field +from typing import Dict, List, Set, Union + +from sqlalchemy.exc import MissingGreenlet +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.api.helpers import find_bound_dimensions +from datajunction_server.database import Node, NodeRevision +from datajunction_server.database.column import Column +from datajunction_server.errors import DJError, DJException, ErrorCode +from datajunction_server.models.base import labelize +from datajunction_server.models.node import NodeRevisionBase, NodeStatus +from datajunction_server.models.node_type import NodeType +from datajunction_server.sql.parsing import ast +from datajunction_server.sql.parsing.backends.antlr4 import SqlSyntaxError, parse +from datajunction_server.sql.parsing.backends.exceptions import DJParseException + + +@dataclass +class NodeValidator: + """ + Node validation + """ + + status: NodeStatus = NodeStatus.VALID + columns: List[Column] = field(default_factory=list) + required_dimensions: List[Column] = field(default_factory=list) + dependencies_map: Dict[NodeRevision, List[ast.Table]] = field(default_factory=dict) + missing_parents_map: Dict[str, List[ast.Table]] = field(default_factory=dict) + type_inference_failures: List[str] = field(default_factory=list) + errors: List[DJError] = field(default_factory=list) + updated_columns: List[str] = field(default_factory=list) + + def modified_columns(self, node_revision: NodeRevision) -> Set[str]: + """ + Compared to the provided node revision, returns the modified columns + """ + initial_node_columns = {col.name: col for col in node_revision.columns} + updated_columns = set(initial_node_columns.keys()).difference( + {n.name for n in self.columns}, + ) + for column in self.columns: + if column.name in initial_node_columns: + if initial_node_columns[column.name].type != column.type: + updated_columns.add(column.name) # pragma: no cover + else: # pragma: no cover + updated_columns.add(column.name) # pragma: no cover + return updated_columns + + +async def validate_node_data( + data: Union[NodeRevisionBase, NodeRevision], + session: AsyncSession, +) -> NodeValidator: + """ + Validate a node. This function should never raise any errors. + It will build the lists of issues (including errors) and return them all + for the caller to decide what to do. + """ + node_validator = NodeValidator() + + if isinstance(data, NodeRevision): + validated_node = data + else: + node = Node(name=data.name, type=data.type) + validated_node = NodeRevision(**data.dict()) + validated_node.node = node + + ctx = ast.CompileContext(session=session, exception=DJException()) + + # Try to parse the node's query, extract dependencies and missing parents + try: + formatted_query = ( + NodeRevision.format_metric_alias( + validated_node.query, # type: ignore + validated_node.name, + ) + if validated_node.type == NodeType.METRIC + else validated_node.query + ) + query_ast = parse(formatted_query) # type: ignore + ( + dependencies_map, + missing_parents_map, + ) = await query_ast.bake_ctes().extract_dependencies(ctx) + node_validator.dependencies_map = dependencies_map + node_validator.missing_parents_map = missing_parents_map + except (DJParseException, ValueError, SqlSyntaxError) as raised_exceptions: + node_validator.status = NodeStatus.INVALID + node_validator.errors.append( + DJError(code=ErrorCode.INVALID_SQL_QUERY, message=str(raised_exceptions)), + ) + return node_validator + + # Add aliases for any unnamed columns and confirm that all column types can be inferred + query_ast.select.add_aliases_to_unnamed_columns() + + # Invalid parents will invalidate this node + # Note: we include source nodes here because they sometimes appear to be invalid, but + # this is a bug that needs to be fixed + invalid_parents = { + parent.name + for parent in node_validator.dependencies_map + if parent.type != NodeType.SOURCE and parent.status == NodeStatus.INVALID + } + if invalid_parents: + node_validator.errors.append( + DJError( + code=ErrorCode.INVALID_PARENT, + message=f"References invalid parent node(s) {','.join(invalid_parents)}", + ), + ) + node_validator.status = NodeStatus.INVALID + + try: + column_mapping = {col.name: col for col in validated_node.columns} + except MissingGreenlet: # pragma: no cover + column_mapping = {} # pragma: no cover + node_validator.columns = [] + type_inference_failures = {} + for idx, col in enumerate(query_ast.select.projection): + column = None + column_name = col.alias_or_name.name # type: ignore + existing_column = column_mapping.get(column_name) + try: + column_type = str(col.type) # type: ignore + column = Column( + name=column_name, + display_name=labelize(column_name), + type=column_type, + attributes=existing_column.attributes if existing_column else [], + dimension=existing_column.dimension if existing_column else None, + order=idx, + ) + except DJParseException as parse_exc: + type_inference_failures[column_name] = parse_exc.message + node_validator.status = NodeStatus.INVALID + except TypeError: # pragma: no cover + type_inference_failures[column_name] = ( + f"Unknown TypeError on column {column_name}." + ) + node_validator.status = NodeStatus.INVALID + if column: + node_validator.columns.append(column) + + # check that bound dimensions are from parent nodes + try: + invalid_required_dimensions, matched_bound_columns = find_bound_dimensions( + validated_node, + dependencies_map, + ) + node_validator.required_dimensions = matched_bound_columns + except MissingGreenlet: + invalid_required_dimensions = set() + node_validator.required_dimensions = [] + + if missing_parents_map or type_inference_failures or invalid_required_dimensions: + # update status + node_validator.status = NodeStatus.INVALID + # build errors + missing_parents_error = ( + [ + DJError( + code=ErrorCode.MISSING_PARENT, + message=f"Node definition contains references to nodes that do not " + f"exist: {','.join(missing_parents_map.keys())}", + debug={"missing_parents": list(missing_parents_map.keys())}, + ), + ] + if missing_parents_map + else [] + ) + type_inference_error = ( + [ + DJError( + code=ErrorCode.TYPE_INFERENCE, + message=message, + debug={ + "columns": [column], + "errors": ctx.exception.errors, + }, + ) + for column, message in type_inference_failures.items() + ] + if type_inference_failures + else [] + ) + invalid_required_dimensions_error = ( + [ + DJError( + code=ErrorCode.INVALID_COLUMN, + message=( + "Node definition contains references to columns as " + "required dimensions that are not on parent nodes." + ), + debug={ + "invalid_required_dimensions": list( + invalid_required_dimensions, + ), + }, + ), + ] + if invalid_required_dimensions + else [] + ) + errors = ( + missing_parents_error + + type_inference_error + + invalid_required_dimensions_error + ) + node_validator.errors.extend(errors) + + return node_validator diff --git a/datajunction-server/datajunction_server/materialization/__init__.py b/datajunction-server/datajunction_server/materialization/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/datajunction_server/materialization/jobs/__init__.py b/datajunction-server/datajunction_server/materialization/jobs/__init__.py new file mode 100644 index 000000000..e8ccd4412 --- /dev/null +++ b/datajunction-server/datajunction_server/materialization/jobs/__init__.py @@ -0,0 +1,20 @@ +""" +Available materialization jobs. +""" + +__all__ = [ + "MaterializationJob", + "SparkSqlMaterializationJob", + "DefaultCubeMaterialization", + "DruidMeasuresCubeMaterializationJob", + "DruidMetricsCubeMaterializationJob", +] +from datajunction_server.materialization.jobs.cube_materialization import ( + DefaultCubeMaterialization, + DruidMeasuresCubeMaterializationJob, + DruidMetricsCubeMaterializationJob, +) +from datajunction_server.materialization.jobs.materialization_job import ( + MaterializationJob, + SparkSqlMaterializationJob, +) diff --git a/datajunction-server/datajunction_server/materialization/jobs/cube_materialization.py b/datajunction-server/datajunction_server/materialization/jobs/cube_materialization.py new file mode 100644 index 000000000..a2fcac6f7 --- /dev/null +++ b/datajunction-server/datajunction_server/materialization/jobs/cube_materialization.py @@ -0,0 +1,251 @@ +""" +Cube materialization jobs +""" + +import logging +from typing import Dict, Optional + +from datajunction_server.database.materialization import Materialization +from datajunction_server.database.node import NodeRevision +from datajunction_server.errors import DJInvalidInputException +from datajunction_server.materialization.jobs.materialization_job import ( + MaterializationJob, +) +from datajunction_server.models.cube_materialization import ( + DruidCubeConfig, + DruidCubeMaterializationInput, +) +from datajunction_server.models.engine import Dialect +from datajunction_server.models.materialization import ( + DruidMaterializationInput, + DruidMeasuresCubeConfig, + DruidMetricsCubeConfig, + MaterializationInfo, + MaterializationStrategy, +) +from datajunction_server.naming import amenable_name +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.sql.parsing import ast +from datajunction_server.sql.parsing.backends.antlr4 import parse + +_logger = logging.getLogger(__name__) + + +class DefaultCubeMaterialization(MaterializationJob): + """ + Dummy job that is not meant to be executed but contains all the + settings needed for to materialize a generic cube. + """ + + def schedule( + self, + materialization: Materialization, + query_service_client: QueryServiceClient, + ): + """ + Since this is a settings-only dummy job, we do nothing in this stage. + """ + return # pragma: no cover + + +class DruidMaterializationJob(MaterializationJob): + """ + Generic Druid materialization job, irrespective of measures or aggregation load. + """ + + config_class = None + + def schedule( + self, + materialization: Materialization, + query_service_client: QueryServiceClient, + request_headers: Optional[Dict[str, str]] = None, + ) -> MaterializationInfo: + """ + Use the query service to kick off the materialization setup. + """ + if not self.config_class: + raise DJInvalidInputException( # pragma: no cover + "The materialization job config class must be defined!", + ) + cube_config = self.config_class.parse_obj(materialization.config) + druid_spec = cube_config.build_druid_spec( + materialization.node_revision, + ) + temporal_partition = cube_config.temporal_partition( + materialization.node_revision, + ) + categorical_partitions = cube_config.categorical_partitions( + materialization.node_revision, + ) + final_query = build_materialization_query( + cube_config.query, + materialization, + materialization.node_revision, + ) + return query_service_client.materialize( + DruidMaterializationInput( + name=materialization.name, + node_name=materialization.node_revision.name, + node_version=materialization.node_revision.version, + node_type=materialization.node_revision.type, + schedule=materialization.schedule, + query=str(final_query), + spark_conf=cube_config.spark.__root__ if cube_config.spark else {}, + druid_spec=druid_spec, + upstream_tables=cube_config.upstream_tables or [], + columns=cube_config.columns, + partitions=temporal_partition + categorical_partitions, + job=materialization.job, + strategy=materialization.strategy, + lookback_window=cube_config.lookback_window, + ), + request_headers=request_headers, + ) + + +class DruidMetricsCubeMaterializationJob(DruidMaterializationJob, MaterializationJob): + """ + Druid materialization (aggregations aka metrics) for a cube node. + """ + + config_class = DruidMetricsCubeConfig # type: ignore + + +class DruidMeasuresCubeMaterializationJob(DruidMaterializationJob, MaterializationJob): + """ + Druid materialization (measures) for a cube node. + """ + + dialect = Dialect.DRUID + config_class = DruidMeasuresCubeConfig # type: ignore + + +class DruidCubeMaterializationJob(DruidMaterializationJob, MaterializationJob): + """ + Druid materialization (aggregations aka metrics) for a cube node. + """ + + dialect = Dialect.DRUID + config_class = DruidCubeConfig # type: ignore + + def schedule( + self, + materialization: Materialization, + query_service_client: QueryServiceClient, + request_headers: Optional[Dict[str, str]] = None, + ) -> MaterializationInfo: + """ + Use the query service to kick off the materialization setup. + """ + if not self.config_class: # type: ignore + raise DJInvalidInputException( # pragma: no cover + "The materialization job config class must be defined!", + ) + cube_config = self.config_class.parse_obj(materialization.config) # type: ignore + _logger.info( + "Scheduling DruidCubeMaterializationJob for node=%s", + cube_config.cube, + ) + return query_service_client.materialize_cube( + materialization_input=DruidCubeMaterializationInput( + id=materialization.id, + name=materialization.name, + cube=cube_config.cube, + dimensions=cube_config.dimensions, + metrics=cube_config.metrics, + strategy=materialization.strategy, + schedule=materialization.schedule, + job=materialization.job, + measures_materializations=cube_config.measures_materializations, + combiners=cube_config.combiners, + ), + request_headers=request_headers, + ) + + +def build_materialization_query( + base_cube_query: str, + materialization: Materialization, + node_revision: NodeRevision, +) -> ast.Query: + """ + Build materialization query (based on configured temporal partitions). + """ + cube_materialization_query_ast = parse( + base_cube_query.replace("${dj_logical_timestamp}", "DJ_LOGICAL_TIMESTAMP()"), + ) + final_query = ast.Query( + select=ast.Select( + projection=[ + ast.Column(name=ast.Name(col.alias_or_name.name)) # type: ignore + for col in cube_materialization_query_ast.select.projection + ], + ), + ctes=cube_materialization_query_ast.ctes, + ) + + if materialization.strategy == MaterializationStrategy.INCREMENTAL_TIME: + temporal_partitions = node_revision.temporal_partition_columns() + temporal_partition_col = [ + col + for col in cube_materialization_query_ast.select.projection + if col.alias_or_name.name == amenable_name(temporal_partitions[0].name) # type: ignore + ] + temporal_op = ( + ast.BinaryOp( + left=ast.Column( + name=ast.Name(temporal_partition_col[0].alias_or_name.name), # type: ignore + ), + right=temporal_partitions[0].partition.temporal_expression(), + op=ast.BinaryOpKind.Eq, + ) + if not materialization.config["lookback_window"] + else ast.Between( + expr=ast.Column( + name=ast.Name( + temporal_partition_col[0].alias_or_name.name, # type: ignore + ), + ), + low=temporal_partitions[0].partition.temporal_expression( + interval=materialization.config["lookback_window"], + ), + high=temporal_partitions[0].partition.temporal_expression(), + ) + ) + + categorical_partitions = node_revision.categorical_partition_columns() + if categorical_partitions: + categorical_partition_col = [ + col + for col in cube_materialization_query_ast.select.projection + if col.alias_or_name.name # type: ignore + == amenable_name(categorical_partitions[0].name) # type: ignore + ] + categorical_op = ast.BinaryOp( + left=ast.Column( + name=ast.Name(categorical_partition_col[0].alias_or_name.name), # type: ignore + ), + right=categorical_partitions[0].partition.categorical_expression(), + op=ast.BinaryOpKind.Eq, + ) + final_query.select.where = ast.BinaryOp( + left=temporal_op, + right=categorical_op, + op=ast.BinaryOpKind.And, + ) + else: + final_query.select.where = temporal_op + + combiner_cte = ast.Query(select=cube_materialization_query_ast.select).set_alias( + ast.Name("combiner_query"), + ) + combiner_cte.parenthesized = True + combiner_cte.as_ = True + combiner_cte.parent = final_query + combiner_cte.parent_key = "ctes" + final_query.ctes += [combiner_cte] + final_query.select.from_ = ast.From( + relations=[ast.Relation(primary=ast.Table(name=ast.Name("combiner_query")))], + ) + return final_query diff --git a/datajunction-server/datajunction_server/materialization/jobs/job_types.py b/datajunction-server/datajunction_server/materialization/jobs/job_types.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/datajunction_server/materialization/jobs/materialization_job.py b/datajunction-server/datajunction_server/materialization/jobs/materialization_job.py new file mode 100644 index 000000000..31e780fff --- /dev/null +++ b/datajunction-server/datajunction_server/materialization/jobs/materialization_job.py @@ -0,0 +1,187 @@ +""" +Available materialization jobs. +""" + +import abc +from typing import Dict, List, Optional + +from datajunction_server.database.materialization import Materialization +from datajunction_server.models.engine import Dialect +from datajunction_server.models.materialization import ( + GenericMaterializationConfig, + GenericMaterializationInput, + MaterializationInfo, + MaterializationStrategy, +) +from datajunction_server.models.partition import PartitionBackfill +from datajunction_server.naming import amenable_name +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.sql.parsing import ast +from datajunction_server.sql.parsing.backends.antlr4 import parse +from datajunction_server.utils import get_settings + +settings = get_settings() + + +class MaterializationJob(abc.ABC): + """ + Base class for a materialization job + """ + + dialect: Optional[Dialect] = None + + def __init__(self): ... + + def run_backfill( + self, + materialization: Materialization, + partitions: List[PartitionBackfill], + query_service_client: QueryServiceClient, + request_headers: Optional[Dict[str, str]] = None, + ) -> MaterializationInfo: + """ + Kicks off a backfill based on the spec using the query service + """ + return query_service_client.run_backfill( + materialization.node_revision.name, + materialization.node_revision.version, + materialization.node_revision.type, + materialization.name, # type: ignore + partitions, + request_headers=request_headers, + ) + + @abc.abstractmethod + def schedule( + self, + materialization: Materialization, + query_service_client: QueryServiceClient, + ) -> MaterializationInfo: + """ + Schedules the materialization job, typically done by calling a separate service + with the configured materialization parameters. + """ + + +class SparkSqlMaterializationJob( # pragma: no cover + MaterializationJob, +): + """ + Spark SQL materialization job. + """ + + dialect = Dialect.SPARK + + def schedule( + self, + materialization: Materialization, + query_service_client: QueryServiceClient, + request_headers: Optional[Dict[str, str]] = None, + ) -> MaterializationInfo: + """ + Placeholder for the actual implementation. + """ + generic_config = GenericMaterializationConfig.parse_obj(materialization.config) + temporal_partitions = materialization.node_revision.temporal_partition_columns() + query_ast = parse( + generic_config.query.replace( + settings.dj_logical_timestamp_format, + "DJ_LOGICAL_TIMESTAMP()", + ), + ) + final_query = query_ast + if ( + temporal_partitions + and materialization.strategy == MaterializationStrategy.INCREMENTAL_TIME + ): + temporal_partition_col = [ + col + for col in query_ast.select.projection + if col.alias_or_name.name.endswith(temporal_partitions[0].name) # type: ignore + ] + temporal_op = ( + ast.BinaryOp( + left=ast.Column( + name=ast.Name( + temporal_partition_col[0].alias_or_name.name, # type: ignore + ), + ), + right=temporal_partitions[0].partition.temporal_expression(), + op=ast.BinaryOpKind.Eq, + ) + if not generic_config.lookback_window + else ast.Between( + expr=ast.Column( + name=ast.Name( + temporal_partition_col[0].alias_or_name.name, # type: ignore + ), + ), + low=temporal_partitions[0].partition.temporal_expression( + interval=generic_config.lookback_window, + ), + high=temporal_partitions[0].partition.temporal_expression(), + ) + ) + final_query = ast.Query( + select=ast.Select( + projection=[ + ast.Column(name=ast.Name(col.alias_or_name.name)) # type: ignore + for col in query_ast.select.projection + ], + from_=query_ast.select.from_, + where=temporal_op, + ), + ctes=query_ast.ctes, + ) + + categorical_partitions = ( + materialization.node_revision.categorical_partition_columns() + ) + if categorical_partitions: + categorical_partition_col = [ + col + for col in final_query.select.projection + if col.alias_or_name.name # type: ignore + == amenable_name(categorical_partitions[0].name) # type: ignore + ] + categorical_op = ast.BinaryOp( + left=ast.Column( + name=ast.Name( + categorical_partition_col[0].alias_or_name.name, # type: ignore + ), + ), + right=categorical_partitions[0].partition.categorical_expression(), + op=ast.BinaryOpKind.Eq, + ) + final_query.select.where = ast.BinaryOp( + left=temporal_op, + right=categorical_op, + op=ast.BinaryOpKind.And, + ) + + result = query_service_client.materialize( + GenericMaterializationInput( + name=materialization.name, # type: ignore + node_name=materialization.node_revision.name, + node_version=materialization.node_revision.version, + node_type=materialization.node_revision.type.value, + job=materialization.job, + strategy=materialization.strategy, + lookback_window=generic_config.lookback_window, + schedule=materialization.schedule, + query=str(final_query), + upstream_tables=generic_config.upstream_tables, + spark_conf=generic_config.spark.__root__ + if generic_config.spark + else {}, + columns=generic_config.columns, + partitions=( + generic_config.temporal_partition(materialization.node_revision) + + generic_config.categorical_partitions( + materialization.node_revision, + ) + ), + ), + request_headers=request_headers, + ) + return result diff --git a/datajunction-server/datajunction_server/models/__init__.py b/datajunction-server/datajunction_server/models/__init__.py new file mode 100644 index 000000000..42cef24bb --- /dev/null +++ b/datajunction-server/datajunction_server/models/__init__.py @@ -0,0 +1,3 @@ +""" +All models. +""" diff --git a/datajunction-server/datajunction_server/models/access.py b/datajunction-server/datajunction_server/models/access.py new file mode 100644 index 000000000..10bcc38a4 --- /dev/null +++ b/datajunction-server/datajunction_server/models/access.py @@ -0,0 +1,288 @@ +""" +Models for authorization +""" + +from copy import deepcopy +from enum import Enum +from typing import TYPE_CHECKING, Callable, Iterable, Optional, Set, Union + +from pydantic import BaseModel, Field +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.construction.utils import try_get_dj_node +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.errors import DJAuthorizationException, DJError, ErrorCode +from datajunction_server.models.user import UserOutput + +if TYPE_CHECKING: + from datajunction_server.sql.parsing.ast import Column + + +class ResourceType(Enum): + """ + Types of resources + """ + + NODE = "node" + NAMESPACE = "namespace" + + +class ResourceRequestVerb(Enum): + """ + Types of actions for a request + """ + + BROWSE = "browse" + READ = "read" + WRITE = "write" + USE = "use" + EXECUTE = "execute" + DELETE = "delete" + + +class Resource(BaseModel): + """ + Base class for resource objects + that are passed to injected validation logic + """ + + name: str # name of the node + resource_type: ResourceType + owner: str + + def __hash__(self) -> int: + return hash((self.name, self.resource_type, self.owner)) + + @classmethod + def from_node(cls, node: Union[NodeRevision, Node]) -> "Resource": + """ + Create a resource object from a DJ Node + """ + return cls(name=node.name, resource_type=ResourceType.NODE, owner="") + + @classmethod + def from_namespace(cls, namespace: str) -> "Resource": + """ + Create a resource object from a namespace + """ + return cls( + name=namespace, + resource_type=ResourceType.NAMESPACE, + owner="", + ) + + +class ResourceRequest(BaseModel): + """ + Resource Requests provide the information + that is available to grant access to a resource + """ + + verb: ResourceRequestVerb + access_object: Resource + approved: Optional[bool] = None + + def approve(self): + """ + Approve the request + """ + self.approved = True + + def deny(self): + """ + Deny the request + """ + self.approved = False + + def __hash__(self) -> int: + return hash((self.verb, self.access_object)) + + def __eq__(self, other) -> bool: + return self.verb == other.verb and self.access_object == other.access_object + + def __str__(self) -> str: + return ( # pragma: no cover + f"{self.verb.value}:" + f"{self.access_object.resource_type.value}/" + f"{self.access_object.name}" + ) + + +class AccessControlState(Enum): + """ + State values used by the ACS function to track when + """ + + DIRECT = "direct" + INDIRECT = "indirect" + + +class AccessControl(BaseModel): + """ + An access control provides all the information + necessary to deny or approve a request + """ + + user: str + state: AccessControlState + direct_requests: Set[ResourceRequest] + indirect_requests: Set[ResourceRequest] + validation_request_count: int + + @property + def requests(self) -> Set[ResourceRequest]: + """ + Get all direct and indirect requests as a single set + """ + return self.direct_requests | self.indirect_requests + + def approve_all(self): + """ + Approve all requests + """ + for request in self.requests: + request.approve() + + def deny_all(self): + """ + Deny all requests + """ + for request in self.requests: + request.deny() + + +ValidateAccessFn = Callable[[AccessControl], None] + + +class AccessControlStore(BaseModel): + """ + An access control store tracks all ResourceRequests + """ + + validate_access: Callable[["AccessControl"], bool] + user: Optional[UserOutput] + base_verb: Optional[ResourceRequestVerb] = None + state: AccessControlState = AccessControlState.DIRECT + direct_requests: Set[ResourceRequest] = Field(default_factory=set) + indirect_requests: Set[ResourceRequest] = Field(default_factory=set) + validation_request_count: int = 0 + validation_results: Set[ResourceRequest] = Field(default_factory=set) + + def add_request(self, request: ResourceRequest): + """ + Add a resource request to the store + """ + if self.state == AccessControlState.DIRECT: + self.direct_requests.add(request) + else: + self.indirect_requests.add(request) # pragma: no cover + + async def add_request_by_node_name( + self, + session: AsyncSession, + node_name: Union[str, "Column"], + verb: Optional[ResourceRequestVerb] = None, + ): + """ + Add a request using a node's name + """ + node = await try_get_dj_node(session, node_name) + if node is not None: + self.add_request_by_node(node, verb) + return node + + def add_request_by_node( + self, + node: Union[NodeRevision, Node], + verb: Optional[ResourceRequestVerb] = None, + ): + """ + Add a request using a node + """ + self.add_request( + ResourceRequest( + verb=verb or self.base_verb, + access_object=Resource.from_node(node), + ), + ) + + def add_request_by_nodes( + self, + nodes: Iterable[Union[NodeRevision, Node]], + verb: Optional[ResourceRequestVerb] = None, + ): + """ + Add a request using a node + """ + for node in nodes: # pragma: no cover + self.add_request( # pragma: no cover + ResourceRequest( + verb=verb or self.base_verb, + access_object=Resource.from_node(node), + ), + ) + + def raise_if_invalid_requests(self, show_denials: bool = True): + """ + Raises if validate has ever given any invalid requests + """ + denied = ", ".join( + [ + str(request) + for request in self.validation_results + if not request.approved + ], + ) + if denied: + message = ( + f"Authorization of User `{self.user.username if self.user else 'no user'}` " + "for this request failed." + f"\nThe following requests were denied:\n{denied}." + if show_denials + else "" + ) + raise DJAuthorizationException( + errors=[ + DJError( + code=ErrorCode.UNAUTHORIZED_ACCESS, + message=message, + ), + ], + ) + + def validate(self) -> Set[ResourceRequest]: + """ + Checks with ACS and stores any returned invalid requests + """ + self.validation_request_count += 1 + + access_control = AccessControl( + user=self.user.username if self.user is not None else "", + state=self.state, + direct_requests=deepcopy(self.direct_requests), + indirect_requests=deepcopy(self.indirect_requests), + validation_request_count=self.validation_request_count, + ) + + self.validate_access(access_control) # type: ignore + + self.validation_results = access_control.requests + + if any((result.approved is None for result in self.validation_results)): + raise DJAuthorizationException( + errors=[ + DJError( + code=ErrorCode.INCOMPLETE_AUTHORIZATION, + message="Injected `validate_access` must approve or deny all requests.", + ), + ], + ) + + return self.validation_results + + def validate_and_raise(self): + """ + Validates with ACS and raises if any resources were denied + """ + self.validate() + self.raise_if_invalid_requests() diff --git a/datajunction-server/datajunction_server/models/attribute.py b/datajunction-server/datajunction_server/models/attribute.py new file mode 100644 index 000000000..04993d556 --- /dev/null +++ b/datajunction-server/datajunction_server/models/attribute.py @@ -0,0 +1,60 @@ +""" +Models for attributes. +""" + +from enum import Enum +from typing import List, Optional + +from pydantic.main import BaseModel + +from datajunction_server.enum import StrEnum +from datajunction_server.models.node_type import NodeType + +RESERVED_ATTRIBUTE_NAMESPACE = "system" + + +class AttributeTypeIdentifier(BaseModel): + """ + Fields that can be used to identify an attribute type. + """ + + namespace: str = "system" + name: str + + +class UniquenessScope(StrEnum): + """ + The scope at which this attribute needs to be unique. + """ + + NODE = "node" + COLUMN_TYPE = "column_type" + + +class MutableAttributeTypeFields(AttributeTypeIdentifier): + """ + Fields on attribute types that users can set. + """ + + description: str + allowed_node_types: List[NodeType] + uniqueness_scope: Optional[List[UniquenessScope]] + + +class AttributeTypeBase(MutableAttributeTypeFields): + """Base attribute type.""" + + id: int + + class Config: + orm_mode = True + + +class ColumnAttributes(str, Enum): + """ + Managed by default column attributes + """ + + PRIMARY_KEY = "primary_key" + DIMENSION = "dimension" + HIDDEN = "hidden" diff --git a/datajunction-server/datajunction_server/models/base.py b/datajunction-server/datajunction_server/models/base.py new file mode 100644 index 000000000..13a4f4feb --- /dev/null +++ b/datajunction-server/datajunction_server/models/base.py @@ -0,0 +1,19 @@ +""" +A base SQLModel class with a default naming convention. +""" + +NAMING_CONVENTION = { + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(auto_constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s", +} + + +def labelize(value: str) -> str: + """ + Turn a system name into a human-readable name. + """ + + return value.replace(".", ": ").replace("_", " ").title() diff --git a/datajunction-server/datajunction_server/models/catalog.py b/datajunction-server/datajunction_server/models/catalog.py new file mode 100644 index 000000000..633a16553 --- /dev/null +++ b/datajunction-server/datajunction_server/models/catalog.py @@ -0,0 +1,24 @@ +""" +Models for columns. +""" + +from typing import TYPE_CHECKING, List, Optional + +from pydantic.main import BaseModel + +from datajunction_server.models.engine import EngineInfo + +if TYPE_CHECKING: + pass + + +class CatalogInfo(BaseModel): + """ + Class for catalog creation + """ + + name: str + engines: Optional[List[EngineInfo]] = [] + + class Config: + orm_mode = True diff --git a/datajunction-server/datajunction_server/models/collection.py b/datajunction-server/datajunction_server/models/collection.py new file mode 100644 index 000000000..99be447d8 --- /dev/null +++ b/datajunction-server/datajunction_server/models/collection.py @@ -0,0 +1,36 @@ +""" +Models for collections +""" + +from typing import Optional + +from pydantic.main import BaseModel + +from datajunction_server.models.node import NodeNameOutput + + +class CollectionInfo(BaseModel): + """ + Class for a collection information + """ + + id: Optional[int] + name: str + description: str + + class Config: + orm_mode = True + + +class CollectionDetails(CollectionInfo): + """ + Collection information with details + """ + + id: Optional[int] + name: str + description: str + nodes: list[NodeNameOutput] + + class Config: + orm_mode = True diff --git a/datajunction-server/datajunction_server/models/column.py b/datajunction-server/datajunction_server/models/column.py new file mode 100644 index 000000000..89d6c486e --- /dev/null +++ b/datajunction-server/datajunction_server/models/column.py @@ -0,0 +1,61 @@ +""" +Models for columns. +""" + +from typing import Optional, TypedDict + +from pydantic.main import BaseModel +from sqlalchemy import TypeDecorator +from sqlalchemy.types import Text + +from datajunction_server.enum import StrEnum +from datajunction_server.sql.parsing.types import ColumnType + + +class ColumnYAML(TypedDict, total=False): + """ + Schema of a column in the YAML file. + """ + + type: str + dimension: str + + +class ColumnTypeDecorator(TypeDecorator): + """ + Converts a column type from the database to a `ColumnType` class + """ + + impl = Text + cache_ok = True + + def process_bind_param(self, value: ColumnType, dialect): + return str(value) + + def process_result_value(self, value, dialect): + from datajunction_server.sql.parsing.backends.antlr4 import parse_rule + + if not value: + return value + return parse_rule(value, "dataType") + + +class ColumnAttributeInput(BaseModel): + """ + A column attribute input + """ + + attribute_type_namespace: Optional[str] = "system" + attribute_type_name: str + column_name: str + + +class SemanticType(StrEnum): + """ + Semantic type of a column + """ + + MEASURE = "measure" + METRIC = "metric" + DIMENSION = "dimension" + TIMESTAMP = "timestamp" diff --git a/datajunction-server/datajunction_server/models/cube.py b/datajunction-server/datajunction_server/models/cube.py new file mode 100644 index 000000000..4c478fd79 --- /dev/null +++ b/datajunction-server/datajunction_server/models/cube.py @@ -0,0 +1,99 @@ +""" +Models for cubes. +""" + +from typing import List, Optional + +from pydantic import Field, root_validator +from pydantic.main import BaseModel + +from datajunction_server.models.materialization import MaterializationConfigOutput +from datajunction_server.models.node import ( + AvailabilityStateBase, + ColumnOutput, + NodeMode, + NodeStatus, +) +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.partition import PartitionOutput +from datajunction_server.models.tag import TagOutput +from datajunction_server.typing import UTCDatetime + + +class CubeElementMetadata(BaseModel): + """ + Metadata for an element in a cube + """ + + name: str + display_name: str + node_name: str + type: str + partition: Optional[PartitionOutput] + + @root_validator(pre=True) + def type_string(cls, values): + """ + Extracts the type as a string + """ + values = dict(values) + if "node_revisions" in values: + values["node_name"] = values["node_revisions"][0].name + values["type"] = ( + values["node_revisions"][0].type + if values["node_revisions"][0].type == NodeType.METRIC + else NodeType.DIMENSION + ) + return values + + class Config: + orm_mode = True + + +class CubeRevisionMetadata(BaseModel): + """ + Metadata for a cube node + """ + + id: int = Field(alias="node_revision_id") + node_id: int + type: NodeType + name: str + display_name: str + version: str + status: NodeStatus + mode: NodeMode + description: str = "" + availability: Optional[AvailabilityStateBase] = None + cube_elements: List[CubeElementMetadata] + cube_node_metrics: List[str] + cube_node_dimensions: List[str] + query: Optional[str] + columns: List[ColumnOutput] + sql_columns: Optional[List[ColumnOutput]] + updated_at: UTCDatetime + materializations: List[MaterializationConfigOutput] + tags: Optional[List[TagOutput]] + + class Config: + allow_population_by_field_name = True + orm_mode = True + + +class DimensionValue(BaseModel): + """ + Dimension value and count + """ + + value: List[str] + count: Optional[int] + + +class DimensionValues(BaseModel): + """ + Dimension values + """ + + dimensions: List[str] + values: List[DimensionValue] + cardinality: int diff --git a/datajunction-server/datajunction_server/models/cube_materialization.py b/datajunction-server/datajunction_server/models/cube_materialization.py new file mode 100644 index 000000000..3d6d892cf --- /dev/null +++ b/datajunction-server/datajunction_server/models/cube_materialization.py @@ -0,0 +1,421 @@ +"""Models related to cube materialization""" + +import hashlib +from typing import Any, Dict, List, Optional, Union + +from pydantic import BaseModel, Field, validator + +from datajunction_server.enum import StrEnum +from datajunction_server.errors import DJInvalidInputException +from datajunction_server.models.column import SemanticType +from datajunction_server.models.materialization import ( + DRUID_AGG_MAPPING, + MaterializationJobTypeEnum, + MaterializationStrategy, +) +from datajunction_server.models.node_type import NodeNameVersion +from datajunction_server.models.partition import Granularity +from datajunction_server.models.query import ColumnMetadata + + +class Aggregability(StrEnum): + """ + Type of allowed aggregation for a given measure. + """ + + FULL = "full" + LIMITED = "limited" + NONE = "none" + + +class AggregationRule(BaseModel): + """ + The aggregation rule for the measure. If the Aggregability type is LIMITED, the `level` should + be specified to highlight the level at which the measure needs to be aggregated in order to + support the specified aggregation function. + + For example, consider a metric like COUNT(DISTINCT user_id). It can be decomposed into a + single measure with LIMITED aggregability, i.e., it is only aggregatable if the measure is + calculated at the `user_id` level: + - name: num_users + expression: DISTINCT user_id + aggregation: COUNT + rule: + type: LIMITED + level: ["user_id"] + """ + + type: Aggregability = Aggregability.NONE + level: list[str] | None = None + + +class Measure(BaseModel): + """ + Measures are aggregated facts (e.g. SUM(view_secs)). They can be optionally combined + to build derived metrics, e.g. SUM(clicks) / SUM(view_secs). Combining is optional because + a stand-alone measure can itself be a metric. + """ + + name: str + expression: str # A SQL expression for defining the measure + aggregation: str + rule: AggregationRule + + +class MetricMeasures(BaseModel): + """ + Represent a metric as a set of measures, along with the expression for + combining the measures to make the metric. + """ + + metric: str + measures: List[Measure] + combiner: str + + +class DruidSpec(BaseModel): + """ + Represents Druid-specific configuration for a MeasuresMaterialization. + """ + + datasource: str = Field(description="The Druid datasource name.") + ingestion_spec: Dict = Field( + description="Druid ingestion spec for the materialization.", + ) + + +class MeasuresMaterialization(BaseModel): + """ + Represents a single pre-aggregation transform query for materializing a partition. + """ + + node: NodeNameVersion = Field(description="The node being materialized") + grain: list[str] = Field( + description="The grain at which the node is being materialized.", + ) + dimensions: list[str] = Field( + description="List of dimensions included in this materialization.", + ) + measures: list[Measure] = Field( + description="List of measures included in this materialization.", + ) + query: str = Field( + description="The query used for each materialization run.", + ) + + columns: list[ColumnMetadata] + + timestamp_column: str | None = Field(description="Timestamp column name") + timestamp_format: str | None = Field( + description="Timestamp format. Example: `yyyyMMdd`", + ) + + granularity: Granularity | None = Field( + description="The time granularity for each materialization run. Examples: DAY, HOUR", + ) + + spark_conf: Dict[str, str] | None = Field( + description="Spark config for this materialization.", + ) + upstream_tables: list[str] = Field( + description="List of upstream tables used in this materialization.", + ) + + def table_ast(self): + """ + Generate a unique output table name based on the parameters. + """ + from datajunction_server.sql.parsing import ast + + return ast.Table(name=ast.Name(self.output_table_name)) + + @property + def output_table_name(self) -> str: + """ + Generate a unique output table name based on the parameters. + """ + unique_string = "|".join( + [ + self.node.name, + str(self.node.version), + self.timestamp_column or "", + ",".join(sorted(self.grain)), + ",".join(sorted(self.dimensions)), + ",".join(sorted([measure.name for measure in self.measures])), + ], + ) + unique_hash = hashlib.sha256(unique_string.encode()).hexdigest()[:16] + return f"{self.node.name}_{self.node.version}_{unique_hash}".replace(".", "_") + + def dict(self, **kwargs): + base = super().dict(**kwargs) + base["output_table_name"] = self.output_table_name + # base["druid_spec"] = self.build_druid_spec() + return base + + @classmethod + def from_measures_query(cls, measures_query, temporal_partition): + """ + Builds a MeasuresMaterialization object from a measures query. + """ + return MeasuresMaterialization( + node=measures_query.node, + grain=measures_query.grain, + columns=measures_query.columns, + timestamp_column=[ + col.name + for col in measures_query.columns + if col.semantic_entity == temporal_partition.name + ][0], + timestamp_format=temporal_partition.partition.format, + granularity=temporal_partition.partition.granularity, + query=measures_query.sql, + dimensions=[ + col.name + for col in measures_query.columns # type: ignore + if col.semantic_type == SemanticType.DIMENSION + ], + measures=list( + { + measure.name: measure + for metric, (measures, combiner) in measures_query.metrics.items() + for measure in measures + }.values(), + ), + spark_conf=measures_query.spark_conf, + upstream_tables=measures_query.upstream_tables, + ) + + +class MeasureKey(BaseModel): + """ + Lookup key for a measure + """ + + node: NodeNameVersion + measure_name: str + + +class CubeMetric(BaseModel): + """ + Represents a metric belonging to a cube. + """ + + metric: NodeNameVersion = Field(description="The name and version of the metric.") + required_measures: list[MeasureKey] = Field( + description="List of measures required by this metric.", + ) + derived_expression: str = Field( + description=( + "The expression for rewriting the original metric query " + "using the materialized measures." + ), + ) + + +class UpsertCubeMaterialization(BaseModel): + """ + An upsert object for cube materializations + """ + + # For cubes this is the only materialization type we support + job: MaterializationJobTypeEnum = MaterializationJobTypeEnum.DRUID_CUBE + + # Only FULL or INCREMENTAL_TIME is available for cubes + strategy: MaterializationStrategy = MaterializationStrategy.INCREMENTAL_TIME + + # Cron schedule + schedule: str + + # Lookback window, only relevant if materialization strategy is INCREMENTAL_TIME + lookback_window: str | None = "1 DAY" + + @validator("job", pre=True) + def validate_job( + cls, + job: Union[str, MaterializationJobTypeEnum], + ) -> MaterializationJobTypeEnum: + """ + Validates the `job` field. Converts to an enum if `job` is a string. + """ + if isinstance(job, str): # pragma: no cover + job_name = job.upper() + options = MaterializationJobTypeEnum._member_names_ + if job_name not in options: + raise DJInvalidInputException( + http_status_code=404, + message=f"Materialization job type `{job.upper()}` not found. " + f"Available job types: {[job.name for job in MaterializationJobTypeEnum]}", + ) + return MaterializationJobTypeEnum[job_name] + return job + + +class CombineMaterialization(BaseModel): + """ + Stage for combining measures datasets at their shared grain and ingesting to Druid. + Note that if there is only one upstream measures dataset, the Spark combining stage will + be skipped and we ingest the aggregated measures directly to Druid. + """ + + node: NodeNameVersion + query: str | None + columns: List[ColumnMetadata] + grain: list[str] = Field( + description="The grain at which the node is being materialized.", + ) + dimensions: list[str] = Field( + description="List of dimensions included in this materialization.", + ) + measures: list[Measure] = Field( + description="List of measures included in this materialization.", + ) + + timestamp_column: str | None = Field(description="Timestamp column name") + timestamp_format: str | None = Field( + description="Timestamp format. Example: `yyyyMMdd`", + ) + + granularity: Granularity | None = Field( + description="The time granularity for each materialization run. Examples: DAY, HOUR", + ) + upstream_tables: list[str] = [] + + @property + def output_table_name(self) -> str: + """ + Builds an output table name based on the node and a hash of its unique key. + """ + unique_string = "|".join( + [ + self.node.name, + str(self.node.version), + self.timestamp_column or "", + ",".join(sorted(self.grain)), + ",".join(sorted(self.dimensions)), + ",".join(sorted([measure.name for measure in self.measures])), + ], + ) + unique_hash = hashlib.sha256(unique_string.encode()).hexdigest()[:16] + return f"{self.node.name}_{self.node.version}_{unique_hash}".replace(".", "_") + + def metrics_spec(self) -> list[dict[str, Any]]: + """ + Returns the Druid metrics spec for ingestion + """ + column_mapping = {col.name: col.type for col in self.columns} # type: ignore + return [ + { + "fieldName": measure.name, + "name": measure.name, + "type": DRUID_AGG_MAPPING[ + (column_mapping[measure.name], measure.aggregation.lower()) + ], + } + for measure in self.measures + if (column_mapping.get(measure.name), measure.aggregation.lower()) + in DRUID_AGG_MAPPING + ] + + def build_druid_spec(self): + """ + Builds the Druid ingestion spec from a materialization config. + """ + # A temporal partition should be configured on the cube, raise an error if not + if not self.timestamp_column: + raise DJInvalidInputException( # pragma: no cover + "There must be at least one time-based partition configured" + " on this cube or it cannot be materialized to Druid.", + ) + + druid_datasource_name = f"dj__{self.output_table_name}" + + # if there are categorical partitions, we can additionally include one of them + # in the partitionDimension field under partitionsSpec + druid_spec: Dict = { + "dataSchema": { + "dataSource": druid_datasource_name, + "parser": { + "parseSpec": { + "format": "parquet", + "dimensionsSpec": { + "dimensions": sorted( + list(set(self.dimensions)), # type: ignore + ), + }, + "timestampSpec": { + "column": self.timestamp_column, + "format": self.timestamp_format, + }, + }, + }, + "metricsSpec": self.metrics_spec(), + "granularitySpec": { + "type": "uniform", + "segmentGranularity": str(self.granularity).upper(), + "intervals": [], # this should be set at runtime + }, + }, + "tuningConfig": { + "partitionsSpec": { + "targetPartitionSize": 5000000, + "type": "hashed", + }, + "useCombiner": True, + "type": "hadoop", + }, + } + return druid_spec + + def dict(self, **kwargs): + base = super().dict(**kwargs) + base["druid_spec"] = self.build_druid_spec() + base["output_table_name"] = self.output_table_name + return base + + +class DruidCubeConfig(BaseModel): + """ + Represents a DruidCube job that contains multiple MeasuresMaterialization configurations. + """ + + cube: NodeNameVersion + + dimensions: list[str] + metrics: list[CubeMetric] + + # List of MeasuresMaterialization configurations. + measures_materializations: List[MeasuresMaterialization] + + # List of materializations used to combine measures outputs. For hyper efficient + # Druid queries, there should ideally only be a single one, but this may not be + # possible for metrics at different levels. + combiners: list[CombineMaterialization] + + +class DruidCubeMaterializationInput(BaseModel): + """ + Materialization info as passed to the query service. + """ + + id: int | None = None + name: str + + # Frozen cube info at the time of materialization + cube: NodeNameVersion + dimensions: list[str] + metrics: list[CubeMetric] + + # Materialization metadata + strategy: MaterializationStrategy + schedule: str + job: str + lookback_window: Optional[str] = "1 DAY" + + # List of measures materializations + measures_materializations: List[MeasuresMaterialization] + + # List of materializations used to combine measures outputs. For hyper efficient + # Druid queries, there should ideally only be a single one, but this may not be + # possible for metrics at different levels. + combiners: list[CombineMaterialization] diff --git a/datajunction-server/datajunction_server/models/database.py b/datajunction-server/datajunction_server/models/database.py new file mode 100644 index 000000000..04bda26e0 --- /dev/null +++ b/datajunction-server/datajunction_server/models/database.py @@ -0,0 +1,28 @@ +""" +Models for databases. +""" + +from typing import TypedDict +from uuid import UUID + +from pydantic.main import BaseModel + +# Schema of a database in the YAML file. +DatabaseYAML = TypedDict( + "DatabaseYAML", + {"description": str, "URI": str, "read-only": bool, "async_": bool, "cost": float}, + total=False, +) + + +class DatabaseOutput(BaseModel): + """ + Output for database information. + """ + + uuid: UUID + name: str + description: str + URI: str + async_: bool + cost: float diff --git a/datajunction-server/datajunction_server/models/dimensionlink.py b/datajunction-server/datajunction_server/models/dimensionlink.py new file mode 100644 index 000000000..832700e28 --- /dev/null +++ b/datajunction-server/datajunction_server/models/dimensionlink.py @@ -0,0 +1,68 @@ +"""Models for dimension links""" + +from typing import Dict, Optional + +from pydantic import BaseModel + +from datajunction_server.enum import StrEnum +from datajunction_server.models.node_type import NodeNameOutput + + +class JoinCardinality(StrEnum): + """ + The version upgrade type + """ + + ONE_TO_ONE = "one_to_one" + ONE_TO_MANY = "one_to_many" + MANY_TO_ONE = "many_to_one" + MANY_TO_MANY = "many_to_many" + + +class JoinType(StrEnum): + """ + Join type + """ + + LEFT = "left" + RIGHT = "right" + INNER = "inner" + FULL = "full" + CROSS = "cross" + + +class LinkDimensionIdentifier(BaseModel): + """ + Input for linking a dimension to a node + """ + + dimension_node: str + role: Optional[str] + + +class JoinLinkInput(BaseModel): + """ + Input for creating a join link between a dimension node and node + """ + + dimension_node: str + join_type: Optional[JoinType] = JoinType.LEFT + join_on: Optional[str] + join_cardinality: Optional[JoinCardinality] = JoinCardinality.MANY_TO_ONE + role: Optional[str] + + +class LinkDimensionOutput(BaseModel): + """ + Input for linking a dimension to a node + """ + + dimension: NodeNameOutput + join_type: JoinType + join_sql: str + join_cardinality: Optional[JoinCardinality] + role: Optional[str] + foreign_keys: Dict[str, str | None] + + class Config: + orm_mode = True diff --git a/datajunction-server/datajunction_server/models/engine.py b/datajunction-server/datajunction_server/models/engine.py new file mode 100644 index 000000000..5465df8f0 --- /dev/null +++ b/datajunction-server/datajunction_server/models/engine.py @@ -0,0 +1,42 @@ +""" +Models for columns. +""" + +from typing import Optional + +from pydantic.main import BaseModel + +from datajunction_server.enum import StrEnum + + +class Dialect(StrEnum): + """ + SQL dialect + """ + + SPARK = "spark" + TRINO = "trino" + DRUID = "druid" + + +class EngineInfo(BaseModel): + """ + Class for engine creation + """ + + name: str + version: str + uri: Optional[str] + dialect: Optional[Dialect] + + class Config: + orm_mode = True + + +class EngineRef(BaseModel): + """ + Basic reference to an engine + """ + + name: str + version: str diff --git a/datajunction-server/datajunction_server/models/history.py b/datajunction-server/datajunction_server/models/history.py new file mode 100644 index 000000000..6f11d9d7f --- /dev/null +++ b/datajunction-server/datajunction_server/models/history.py @@ -0,0 +1,57 @@ +""" +Model for history. +""" + +from typing import TYPE_CHECKING, Any, Dict, Optional + +from pydantic.main import BaseModel + +from datajunction_server.database.history import ActivityType, EntityType, History +from datajunction_server.typing import UTCDatetime + +if TYPE_CHECKING: + from datajunction_server.database.node import NodeRevision + from datajunction_server.database.user import User + from datajunction_server.models.node import NodeStatus + + +class HistoryOutput(BaseModel): + """ + Output history event + """ + + id: int + entity_type: Optional[EntityType] + entity_name: Optional[str] + node: Optional[str] + activity_type: Optional[ActivityType] + user: Optional[str] + pre: Dict[str, Any] + post: Dict[str, Any] + details: Dict[str, Any] + created_at: UTCDatetime + + class Config: + orm_mode = True + + +def status_change_history( + node_revision: "NodeRevision", + start_status: "NodeStatus", + end_status: "NodeStatus", + current_user: "User", + parent_node: str = None, +) -> History: + """ + Returns a status change history activity entry + """ + return History( + entity_type=EntityType.NODE, + entity_name=node_revision.name, + node=node_revision.name, + activity_type=ActivityType.STATUS_CHANGE, + pre={"status": start_status}, + post={"status": end_status}, + details={"upstream_node": parent_node if parent_node else None}, + user=current_user.username, + ) diff --git a/datajunction-server/datajunction_server/models/materialization.py b/datajunction-server/datajunction_server/models/materialization.py new file mode 100644 index 000000000..c7debbe0f --- /dev/null +++ b/datajunction-server/datajunction_server/models/materialization.py @@ -0,0 +1,512 @@ +"""Models for materialization""" + +import enum +from typing import TYPE_CHECKING, Dict, List, Optional, Union + +from pydantic import AnyHttpUrl, BaseModel, validator + +from datajunction_server.enum import StrEnum +from datajunction_server.errors import DJInvalidInputException +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.partition import ( + BackfillOutput, + Granularity, + PartitionColumnOutput, + PartitionType, +) +from datajunction_server.models.query import ColumnMetadata +from datajunction_server.naming import amenable_name + +if TYPE_CHECKING: + from datajunction_server.database.node import NodeRevision + +DRUID_AGG_MAPPING = { + ("bigint", "sum"): "longSum", + ("int", "sum"): "longSum", + ("double", "sum"): "doubleSum", + ("float", "sum"): "floatSum", + ("double", "min"): "doubleMin", + ("double", "max"): "doubleMax", + ("float", "min"): "floatMin", + ("float", "max"): "floatMax", + ("bigint", "min"): "longMin", + ("int", "min"): "longMin", + ("bigint", "max"): "longMax", + ("int", "max"): "longMax", + ("bigint", "count"): "longSum", + ("int", "count"): "longSum", + ("double", "count"): "longSum", + ("float", "count"): "longSum", +} + + +class MaterializationStrategy(StrEnum): + """ + Materialization strategies + """ + + # Replace the target dataset in full. Typically used for smaller transforms tables + # -> Availability state: single table + FULL = "full" + + # Full materialization into a new snapshot for each scheduled materialization run. + # DJ will manage the location of the snapshot tables and return the appropriate snapshot + # when requested + # -> Availability state: multiple tables, one per snapshot. The snapshot key should be based on + # when the upstream tables were last updated, with a new snapshot generated only if they were + # updated + SNAPSHOT = "snapshot" + + # Snapshot the query as a new partition in the target dataset, stamped with the current + # date/time. This may be used to produce dimensional snapshot tables. The materialized table + # will contain an appropriate snapshot column. + # -> Availability state: single table. + SNAPSHOT_PARTITION = "snapshot_partition" + + # Materialize incrementally based on the temporal partition column(s). This strategy will + # process the last N periods (defined by lookback_window) of data and load the results into + # corresponding target partitions. Requires a time column. + # -> Availability state: single table + INCREMENTAL_TIME = "incremental_time" + + # Create a database view from the query, doesn't materialize any data + # -> Availability state: single view + VIEW = "view" + + +class GenericMaterializationInput(BaseModel): + """ + The input when calling the query service's materialization + API endpoint for a generic node. + """ + + name: str + node_name: str + node_version: str + node_type: str + job: str + strategy: MaterializationStrategy + schedule: str + query: str + upstream_tables: List[str] + spark_conf: Optional[Dict] = None + partitions: Optional[List[Dict]] = None + columns: List[ColumnMetadata] + lookback_window: Optional[str] = "1 DAY" + + +class DruidMaterializationInput(GenericMaterializationInput): + """ + The input when calling the query service's materialization + API endpoint for a cube node. + """ + + druid_spec: Dict + + +class MaterializationInfo(BaseModel): + """ + The output when calling the query service's materialization + API endpoint for a cube node. + """ + + output_tables: List[str] + urls: List[AnyHttpUrl] + + +class MaterializationConfigOutput(BaseModel): + """ + Output for materialization config. + """ + + id: int + name: Optional[str] + config: Dict + schedule: str + job: Optional[str] + backfills: List[BackfillOutput] + strategy: Optional[str] + + class Config: + orm_mode = True + + +class MaterializationConfigInfoUnified( + MaterializationInfo, + MaterializationConfigOutput, +): + """ + Materialization config + info + """ + + +class SparkConf(BaseModel): + """Spark configuration""" + + __root__: Dict[str, str] = {} + + +class GenericMaterializationConfigInput(BaseModel): + """ + User-input portions of the materialization config + """ + + # Spark config + spark: Optional[SparkConf] + + # The time window to lookback when overwriting materialized datasets + # This will only be used if a time partition was set on the node and + # the materialization strategy is INCREMENTAL_TIME + lookback_window: Optional[str] + + +class GenericMaterializationConfig(GenericMaterializationConfigInput): + """ + Generic node materialization config needed by any materialization choices + and engine combinations + """ + + query: Optional[str] + columns: Optional[List[ColumnMetadata]] + upstream_tables: Optional[List[str]] + + def temporal_partition( + self, + node_revision: "NodeRevision", + ) -> List[PartitionColumnOutput]: + """ + The temporal partition column names on the intermediate measures table + """ + user_defined_temporal_columns = node_revision.temporal_partition_columns() + if not user_defined_temporal_columns: + return [] + user_defined_temporal_column = user_defined_temporal_columns[0] + return [ + PartitionColumnOutput( + name=col.name, + format=user_defined_temporal_column.partition.format, + type_=user_defined_temporal_column.partition.type_, + expression=str( + user_defined_temporal_column.partition.temporal_expression(), + ), + ) + for col in self.columns # type: ignore + if user_defined_temporal_column.name in (col.semantic_entity, col.name) + ] + + def categorical_partitions( + self, + node_revision: "NodeRevision", + ) -> List[PartitionColumnOutput]: + """ + The categorical partition column names on the intermediate measures table + """ + user_defined_categorical_columns = { + col.name for col in node_revision.categorical_partition_columns() + } + return [ + PartitionColumnOutput( + name=col.name, + type_=PartitionType.CATEGORICAL, + ) + for col in self.columns # type: ignore + if col.semantic_entity in user_defined_categorical_columns + ] + + +class DruidConf(BaseModel): + """Druid configuration""" + + granularity: Optional[str] + intervals: Optional[List[str]] + timestamp_column: Optional[str] + timestamp_format: Optional[str] + parse_spec_format: Optional[str] + + +class Measure(BaseModel): + """ + A measure with a simple aggregation + """ + + name: str + field_name: str + agg: str + type: str + + def __eq__(self, other): + return tuple(self.__dict__.items()) == tuple( + other.__dict__.items(), + ) # pragma: no cover + + def __hash__(self): + return hash(tuple(self.__dict__.items())) # pragma: no cover + + +class MetricMeasures(BaseModel): + """ + Represent a metric as a set of measures, along with the expression for + combining the measures to make the metric. + """ + + metric: str + measures: List[Measure] # + combiner: str + + +class GenericCubeConfigInput(GenericMaterializationConfigInput): + """ + Generic cube materialization config fields that require user input + """ + + dimensions: Optional[List[str]] + measures: Optional[Dict[str, MetricMeasures]] + metrics: Optional[List[ColumnMetadata]] + + +class GenericCubeConfig(GenericCubeConfigInput, GenericMaterializationConfig): + """ + Generic cube materialization config needed by any materialization + choices and engine combinations + """ + + +class DruidCubeConfigInput(GenericCubeConfigInput): + """ + Specific Druid cube materialization fields that require user input + """ + + prefix: Optional[str] = "" + suffix: Optional[str] = "" + druid: Optional[DruidConf] + + +class DruidMeasuresCubeConfig(DruidCubeConfigInput, GenericCubeConfig): + """ + Specific cube materialization implementation with Spark and Druid ingestion and + optional prefix and/or suffix to include with the materialized entity's name. + """ + + def metrics_spec(self) -> Dict: + """ + Returns the Druid metrics spec for ingestion + """ + self.dimensions += [ # type: ignore + measure.field_name + for measure_group in self.measures.values() # type: ignore + for measure in measure_group.measures + if (measure.type.lower(), measure.agg.lower()) not in DRUID_AGG_MAPPING + ] + return { + measure.name: { + "fieldName": measure.field_name, + "name": measure.field_name, + "type": DRUID_AGG_MAPPING[(measure.type.lower(), measure.agg.lower())], + } + for measure_group in self.measures.values() # type: ignore + for measure in measure_group.measures + if (measure.type.lower(), measure.agg.lower()) in DRUID_AGG_MAPPING + } + + def build_druid_spec(self, node_revision: "NodeRevision"): + """ + Builds the Druid ingestion spec from a materialization config. + """ + node_name = node_revision.name + metrics_spec = list(self.metrics_spec().values()) + + # A temporal partition should be configured on the cube, raise an error if not + user_defined_temporal_partitions = node_revision.temporal_partition_columns() + if not user_defined_temporal_partitions: + raise DJInvalidInputException( # pragma: no cover + "There must be at least one time-based partition configured" + " on this cube or it cannot be materialized to Druid.", + ) + + # Use the user-defined temporal partition if it exists + user_defined_temporal_partition = None + user_defined_temporal_partition = user_defined_temporal_partitions[0] + timestamp_column = [ + col.name + for col in self.columns # type: ignore + if col.semantic_entity == user_defined_temporal_partition.name + ][0] + + druid_datasource_name = ( + self.prefix # type: ignore + + amenable_name(node_name) # type: ignore + + self.suffix # type: ignore + ) + # if there are categorical partitions, we can additionally include one of them + # in the partitionDimension field under partitionsSpec + druid_spec: Dict = { + "dataSchema": { + "dataSource": druid_datasource_name, + "parser": { + "parseSpec": { + "format": "parquet", + "dimensionsSpec": { + "dimensions": sorted( + list(set(self.dimensions)), # type: ignore + ), + }, + "timestampSpec": { + "column": timestamp_column, + "format": ( + user_defined_temporal_partition.partition.format + if user_defined_temporal_partition + else "millis" + ), + }, + }, + }, + "metricsSpec": metrics_spec, + "granularitySpec": { + "type": "uniform", + "segmentGranularity": str( + user_defined_temporal_partition.partition.granularity + if user_defined_temporal_partition + else Granularity.DAY, + ).upper(), + "intervals": [], # this should be set at runtime + }, + }, + "tuningConfig": { + "partitionsSpec": { + "targetPartitionSize": 5000000, + "type": "hashed", + }, + "useCombiner": True, + "type": "hadoop", + }, + } + return druid_spec + + +class DruidMetricsCubeConfig(DruidMeasuresCubeConfig): + """ + Specific cube materialization implementation with Spark and Druid ingestion and + optional prefix and/or suffix to include with the materialized entity's name. + """ + + def metrics_spec(self) -> Dict: + """ + Returns the Druid metrics spec for ingestion + """ + return { + metric.name: { + "fieldName": metric.name, + "name": metric.name, + "type": DRUID_AGG_MAPPING[(metric.type.lower(), "sum")], + } + for metric in self.metrics # type: ignore + if (metric.type.lower(), "sum") in DRUID_AGG_MAPPING + } + + +class MaterializationJobType(BaseModel): + """ + Materialization job types. These job types will map to their implementations + under the subclasses of `MaterializationJob`. + """ + + name: str + label: str + description: str + + # Node types that can be materialized with this job type + allowed_node_types: List[NodeType] + + # The class that implements this job type, must subclass `MaterializationJob` + job_class: str + + +class MaterializationJobTypeEnum(enum.Enum): + """ + Available materialization job types + """ + + SPARK_SQL = MaterializationJobType( + name="spark_sql", + label="Spark SQL", + description="Spark SQL materialization job", + allowed_node_types=[NodeType.TRANSFORM, NodeType.DIMENSION, NodeType.CUBE], + job_class="SparkSqlMaterializationJob", + ) + + DRUID_MEASURES_CUBE = MaterializationJobType( + name="druid_measures_cube", + label="Druid Measures Cube (Pre-Agg Cube)", + description=( + "Used to materialize a cube's measures to Druid for low-latency access to a set of " + "metrics and dimensions. While the logical cube definition is at the level of metrics " + "and dimensions, this materialized Druid cube will contain measures and dimensions," + " with rollup configured on the measures where appropriate." + ), + allowed_node_types=[NodeType.CUBE], + job_class="DruidMeasuresCubeMaterializationJob", + ) + + DRUID_METRICS_CUBE = MaterializationJobType( + name="druid_metrics_cube", + label="Druid Metrics Cube (Post-Agg Cube)", + description=( + "Used to materialize a cube of metrics and dimensions to Druid for low-latency access." + " The materialized cube is at the metric level, meaning that all metrics will be " + "aggregated to the level of the cube's dimensions." + ), + allowed_node_types=[NodeType.CUBE], + job_class="DruidMetricsCubeMaterializationJob", + ) + + DRUID_CUBE = MaterializationJobType( + name="druid_cube", + label="Druid Cube", + description=( + "Used to materialize a cube of metrics and dimensions to Druid for low-latency access." + "Will replace the other cube materialization types." + ), + allowed_node_types=[NodeType.CUBE], + job_class="DruidCubeMaterializationJob", + ) + + @classmethod + def find_match(cls, job_name: str) -> "MaterializationJobTypeEnum": + """Find a matching enum value for the given job name""" + return [job_type for job_type in cls if job_type.value.job_class == job_name][0] + + +class UpsertMaterialization(BaseModel): + """ + An upsert object for materialization configs + """ + + name: Optional[str] + job: MaterializationJobTypeEnum + config: Union[ + DruidCubeConfigInput, + GenericCubeConfigInput, + GenericMaterializationConfigInput, + ] + schedule: str + strategy: MaterializationStrategy + + @validator("job", pre=True) + def validate_job( + cls, + job: Union[str, MaterializationJobTypeEnum], + ) -> MaterializationJobTypeEnum: + """ + Validates the `job` field. Converts to an enum if `job` is a string. + """ + if isinstance(job, str): + job_name = job.upper() + options = MaterializationJobTypeEnum._member_names_ + if job_name not in options: + raise DJInvalidInputException( + http_status_code=404, + message=f"Materialization job type `{job.upper()}` not found. " + f"Available job types: {[job.name for job in MaterializationJobTypeEnum]}", + ) + return MaterializationJobTypeEnum[job_name] + return job diff --git a/datajunction-server/datajunction_server/models/measure.py b/datajunction-server/datajunction_server/models/measure.py new file mode 100644 index 000000000..fa807a71f --- /dev/null +++ b/datajunction-server/datajunction_server/models/measure.py @@ -0,0 +1,94 @@ +""" +Models for measures. +""" + +from typing import TYPE_CHECKING, List, Optional + +from pydantic.class_validators import root_validator +from pydantic.main import BaseModel + +from datajunction_server.enum import StrEnum + +if TYPE_CHECKING: + pass + + +class AggregationRule(StrEnum): + """ + Type of allowed aggregation for a given measure. + """ + + ADDITIVE = "additive" + NON_ADDITIVE = "non-additive" + SEMI_ADDITIVE = "semi-additive" + + +class NodeColumn(BaseModel): + """ + Defines a column on a node + """ + + node: str + column: str + + +class CreateMeasure(BaseModel): + """ + Input for creating a measure + """ + + name: str + display_name: Optional[str] + description: Optional[str] + columns: List[NodeColumn] + additive: AggregationRule = AggregationRule.NON_ADDITIVE + + +class EditMeasure(BaseModel): + """ + Editable fields on a measure + """ + + display_name: Optional[str] + description: Optional[str] + columns: Optional[List[NodeColumn]] + additive: Optional[AggregationRule] + + +class ColumnOutput(BaseModel): + """ + A simplified column schema, without ID or dimensions. + """ + + name: str + type: str + node: str + + @root_validator(pre=True) + def transform(cls, values): + """ + Transforms the values for output + """ + return { + "name": values.get("name"), + "type": str(values.get("type")), + "node": values.get("node_revisions")[0].name, + } + + class Config: + orm_mode = True + + +class MeasureOutput(BaseModel): + """ + Output model for measures + """ + + name: str + display_name: Optional[str] + description: Optional[str] + columns: List[ColumnOutput] + additive: AggregationRule + + class Config: + orm_mode = True diff --git a/datajunction-server/datajunction_server/models/metric.py b/datajunction-server/datajunction_server/models/metric.py new file mode 100644 index 000000000..aaeb35d3d --- /dev/null +++ b/datajunction-server/datajunction_server/models/metric.py @@ -0,0 +1,117 @@ +""" +Models for metrics. +""" + +from typing import List, Optional + +from pydantic.class_validators import root_validator +from pydantic.main import BaseModel + +from datajunction_server.database.node import Node +from datajunction_server.models.cube_materialization import Measure +from datajunction_server.models.engine import Dialect +from datajunction_server.models.node import ( + DimensionAttributeOutput, + MetricMetadataOutput, +) +from datajunction_server.models.query import ColumnMetadata +from datajunction_server.sql.decompose import MeasureExtractor +from datajunction_server.sql.parsing.backends.antlr4 import ast, parse +from datajunction_server.transpilation import get_transpilation_plugin +from datajunction_server.typing import UTCDatetime +from datajunction_server.utils import get_settings + + +class Metric(BaseModel): + """ + Class for a metric. + """ + + id: int + name: str + display_name: str + current_version: str + description: str = "" + + created_at: UTCDatetime + updated_at: UTCDatetime + + query: str + upstream_node: str + expression: str + + dimensions: List[DimensionAttributeOutput] + metric_metadata: Optional[MetricMetadataOutput] = None + required_dimensions: List[str] + + incompatible_druid_functions: List[str] + + measures: List[Measure] + derived_query: str + derived_expression: str + + @classmethod + def parse_node(cls, node: Node, dims: List[DimensionAttributeOutput]) -> "Metric": + """ + Parses a node into a metric. + """ + query_ast = parse(node.current.query) + functions = [func.function() for func in query_ast.find_all(ast.Function)] + incompatible_druid_functions = [ + func.__name__.upper() + for func in functions + if Dialect.DRUID not in func.dialects + ] + extractor = MeasureExtractor.from_query_string(node.current.query) + measures, derived_sql = extractor.extract() + return cls( + id=node.id, + name=node.name, + display_name=node.current.display_name, + current_version=node.current_version, + description=node.current.description, + created_at=node.created_at, + updated_at=node.current.updated_at, + query=node.current.query, + upstream_node=node.current.parents[0].name, + expression=str(query_ast.select.projection[0]), + dimensions=dims, + metric_metadata=node.current.metric_metadata, + required_dimensions=[dim.name for dim in node.current.required_dimensions], + incompatible_druid_functions=incompatible_druid_functions, + measures=measures, + derived_query=str(derived_sql).strip(), + derived_expression=str(derived_sql.select.projection[0]).strip(), + ) + + +class TranslatedSQL(BaseModel): + """ + Class for SQL generated from a given metric. + """ + + # TODO: once type-inference is added to /query/ endpoint + # columns attribute can be required + sql: str + columns: Optional[List[ColumnMetadata]] = None # pragma: no-cover + dialect: Optional[Dialect] = None + upstream_tables: Optional[List[str]] = None + + @root_validator(pre=False) + def transpile_sql( + cls, + values, + ) -> "TranslatedSQL": + """ + Transpiles SQL to the specified dialect with the configured transpilation plugin. + If no plugin is configured, it will just return the original generated query. + """ + settings = get_settings() + if settings.sql_transpilation_library: # pragma: no cover + plugin = get_transpilation_plugin(settings.sql_transpilation_library) + values["sql"] = plugin.transpile_sql( + values["sql"], + input_dialect=Dialect.SPARK, + output_dialect=values["dialect"], + ) + return values diff --git a/datajunction-server/datajunction_server/models/node.py b/datajunction-server/datajunction_server/models/node.py new file mode 100644 index 000000000..90ac9db3a --- /dev/null +++ b/datajunction-server/datajunction_server/models/node.py @@ -0,0 +1,977 @@ +""" +Model for nodes. +""" + +import enum +import sys +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple, Union, cast + +from pydantic import BaseModel, Extra, root_validator, validator +from pydantic.fields import Field +from pydantic.utils import GetterDict +from sqlalchemy.orm import joinedload, selectinload +from sqlalchemy.sql.schema import Column as SqlaColumn +from sqlalchemy.types import Enum +from typing_extensions import TypedDict + +from datajunction_server.api.graphql.scalars import Cursor +from datajunction_server.enum import StrEnum +from datajunction_server.errors import DJError +from datajunction_server.models.base import labelize +from datajunction_server.models.catalog import CatalogInfo +from datajunction_server.models.column import ColumnYAML +from datajunction_server.models.database import DatabaseOutput +from datajunction_server.models.dimensionlink import LinkDimensionOutput +from datajunction_server.models.engine import Dialect +from datajunction_server.models.materialization import MaterializationConfigOutput +from datajunction_server.models.node_type import NodeNameOutput, NodeType +from datajunction_server.models.partition import PartitionOutput +from datajunction_server.models.tag import TagMinimum, TagOutput +from datajunction_server.models.user import UserNameOnly +from datajunction_server.sql.parsing.types import ColumnType +from datajunction_server.typing import UTCDatetime +from datajunction_server.utils import Version + +DEFAULT_DRAFT_VERSION = Version(major=0, minor=1) +DEFAULT_PUBLISHED_VERSION = Version(major=1, minor=0) +MIN_VALID_THROUGH_TS = -sys.maxsize - 1 + + +@dataclass(frozen=True) +class BuildCriteria: + """ + Criterion used for building + - used to deterimine whether to use an availability state + """ + + timestamp: Optional[UTCDatetime] = None + dialect: Dialect = Dialect.SPARK + target_node_name: Optional[str] = None + + +class NodeMode(StrEnum): + """ + Node mode. + + A node can be in one of the following modes: + + 1. PUBLISHED - Must be valid and not cause any child nodes to be invalid + 2. DRAFT - Can be invalid, have invalid parents, and include dangling references + """ + + PUBLISHED = "published" + DRAFT = "draft" + + +class NodeStatus(StrEnum): + """ + Node status. + + A node can have one of the following statuses: + + 1. VALID - All references to other nodes and node columns are valid + 2. INVALID - One or more parent nodes are incompatible or do not exist + """ + + VALID = "valid" + INVALID = "invalid" + + +class NodeValidationError(BaseModel): + """ + Validation error + """ + + type: str + message: str + + +class NodeStatusDetails(BaseModel): + """ + Node status details. Contains a list of node errors or an empty list of the node status is valid + """ + + status: NodeStatus + errors: List[NodeValidationError] + + +class NodeYAML(TypedDict, total=False): + """ + Schema of a node in the YAML file. + """ + + description: str + display_name: str + type: NodeType + query: str + columns: Dict[str, ColumnYAML] + + +class NodeBase(BaseModel): + """ + A base node. + """ + + name: str + type: NodeType + display_name: Optional[str] = Field(max_length=100) + + +class NodeRevisionBase(BaseModel): + """ + A base node revision. + """ + + name: str + display_name: Optional[str] + type: NodeType + description: Optional[str] = "" + query: Optional[str] = None + mode: NodeMode = NodeMode.PUBLISHED + + +class TemporalPartitionRange(BaseModel): + """ + Any temporal partition range with a min and max partition. + """ + + min_temporal_partition: Optional[List[str]] = None + max_temporal_partition: Optional[List[str]] = None + + def is_outside(self, other) -> bool: + """ + Whether this temporal range is outside the time bounds of the other temporal range + """ + return ( + self.min_temporal_partition < other.min_temporal_partition + or self.max_temporal_partition > other.max_temporal_partition + ) + + +class PartitionAvailability(TemporalPartitionRange): + """ + Partition-level availability + """ + + # This list maps to the ordered list of categorical partitions at the node level. + # For example, if the node's `categorical_partitions` are configured as ["country", "group_id"], + # a valid entry for `value` may be ["DE", null]. + value: List[Optional[str]] + + # Valid through timestamp + valid_through_ts: Optional[int] + + +class AvailabilityNode(TemporalPartitionRange): + """A node in the availability trie tracker""" + + children: Dict = {} + valid_through_ts: Optional[int] = Field(default=MIN_VALID_THROUGH_TS) + + def merge_temporal(self, other: "AvailabilityNode"): + """ + Merge the temporal ranges with each other by saving the largest + possible time range. + """ + self.min_temporal_partition = min( # type: ignore + self.min_temporal_partition, + other.min_temporal_partition, + ) + self.max_temporal_partition = max( # type: ignore + self.max_temporal_partition, + other.max_temporal_partition, + ) + self.valid_through_ts = max( # type: ignore + self.valid_through_ts, + other.valid_through_ts, + ) + + +class AvailabilityTracker: + """ + Tracks the availability of the partitions in a trie, which is used for merging + availability states across categorical and temporal partitions. + """ + + def __init__(self): + self.root = AvailabilityNode() + + def insert(self, partition): + """ + Inserts the partition availability into the availability tracker trie. + """ + current = self.root + for value in partition.value: + next_item = AvailabilityNode( + min_temporal_partition=partition.min_temporal_partition, + max_temporal_partition=partition.max_temporal_partition, + valid_through_ts=partition.valid_through_ts, + ) + # If a wildcard is found, only add this specific partition's + # time range if it's wider than the range of the wildcard + if None in current.children and partition.is_outside( + current.children[None], + ): + wildcard_partition = current.children[None] + next_item.merge_temporal(wildcard_partition) + current.children[value] = next_item + else: + # Add if partition doesn't match any existing, otherwise merge with existing + if value not in current.children: + current.children[value] = next_item + else: + next_item = current.children[value] + next_item.merge_temporal(partition) + + # Remove extraneous partitions at this level if this partition value is a wildcard + if value is None: + child_keys = [key for key in current.children if key is not None] + for child in child_keys: + child_partition = current.children[child] + if not child_partition.is_outside(partition): + del current.children[child] + current = next_item + + def get_partition_range(self) -> List[PartitionAvailability]: + """ + Gets the final set of merged partitions. + """ + candidates: List[Tuple[AvailabilityNode, List[str]]] = [(self.root, [])] + final_partitions = [] + while candidates: + current, partition_list = candidates.pop() + if current.children: + for key, value in current.children.items(): + candidates.append((value, partition_list + [key])) + else: + final_partitions.append( + PartitionAvailability( + value=partition_list, + min_temporal_partition=current.min_temporal_partition, + max_temporal_partition=current.max_temporal_partition, + valid_through_ts=current.valid_through_ts, + ), + ) + return final_partitions + + +class AvailabilityStateBase(TemporalPartitionRange): + """ + An availability state base + """ + + catalog: str + schema_: Optional[str] = Field(default=None) + table: str + valid_through_ts: int + materialization_id: Optional[int] = Field(default=None) + custom_metadata: Optional[Dict[str, Any]] = Field(default={}) + + # An ordered list of categorical partitions like ["country", "group_id"] + # or ["region_id", "age_group"] + categorical_partitions: Optional[List[str]] = Field(default=[]) + + # An ordered list of temporal partitions like ["date", "hour"] or ["date"] + temporal_partitions: Optional[List[str]] = Field(default=[]) + + # Node-level temporal ranges + min_temporal_partition: Optional[List[str]] = Field(default=[]) + max_temporal_partition: Optional[List[str]] = Field(default=[]) + + # Partition-level availabilities + partitions: Optional[List[PartitionAvailability]] = Field(default=[]) + + class Config: + orm_mode = True + + @validator("partitions") + def validate_partitions(cls, partitions): + """ + Validator for partitions + """ + return [partition.dict() for partition in partitions] if partitions else [] + + def merge(self, other: "AvailabilityStateBase"): + """ + Merge this availability state with another. + """ + all_partitions = [ + PartitionAvailability(**partition) + if isinstance(partition, dict) + else partition + for partition in self.partitions + other.partitions # type: ignore + ] + min_range = [ + x for x in (self.min_temporal_partition, other.min_temporal_partition) if x + ] + max_range = [ + x for x in (self.max_temporal_partition, other.max_temporal_partition) if x + ] + top_level_partition = PartitionAvailability( + value=[None for _ in other.categorical_partitions] + if other.categorical_partitions + else [], + min_temporal_partition=min(min_range) if min_range else None, + max_temporal_partition=max(max_range) if max_range else None, + valid_through_ts=max(self.valid_through_ts, other.valid_through_ts), + ) + all_partitions += [top_level_partition] + + tracker = AvailabilityTracker() + for partition in all_partitions: + tracker.insert(partition) + final_partitions = tracker.get_partition_range() + + self.partitions = [ + partition + for partition in final_partitions + if not all(val is None for val in partition.value) + ] + merged_top_level = [ + partition + for partition in final_partitions + if all(val is None for val in partition.value) + ] + + if merged_top_level: # pragma: no cover + self.min_temporal_partition = ( + top_level_partition.min_temporal_partition + or merged_top_level[0].min_temporal_partition + ) + self.max_temporal_partition = ( + top_level_partition.max_temporal_partition + or merged_top_level[0].max_temporal_partition + ) + self.valid_through_ts = ( + top_level_partition.valid_through_ts + or merged_top_level[0].valid_through_ts + or MIN_VALID_THROUGH_TS + ) + + return self + + +class MetricDirection(StrEnum): + """ + The direction of the metric that's considered good, i.e., higher is better + """ + + HIGHER_IS_BETTER = "higher_is_better" + LOWER_IS_BETTER = "lower_is_better" + NEUTRAL = "neutral" + + +class Unit(BaseModel): + """ + Metric unit + """ + + name: str + label: Optional[str] + category: Optional[str] + abbreviation: Optional[str] + description: Optional[str] + + def __str__(self): + return self.name # pragma: no cover + + def __repr__(self): + return self.name + + @validator("label", always=True) + def get_label( + cls, + label: str, + values: Dict[str, Any], + ) -> str: + """Generate a default label if one was not provided.""" + if not label and values: + return labelize(values["name"]) + return label + + class Config: + orm_mode = True + + +class MetricUnit(enum.Enum): + """ + Available units of measure for metrics + TODO: Eventually this can be recorded in a database, + since measurement units can be customized depending on the metric + (i.e., clicks/hour). For the time being, this enum provides some basic units. + """ + + UNKNOWN = Unit(name="unknown", category="") + UNITLESS = Unit(name="unitless", category="") + + PERCENTAGE = Unit( + name="percentage", + category="", + abbreviation="%", + description="A ratio expressed as a number out of 100. Values range from 0 to 100.", + ) + + PROPORTION = Unit( + name="proportion", + category="", + abbreviation="", + description="A ratio that compares a part to a whole. Values range from 0 to 1.", + ) + + # Monetary + DOLLAR = Unit(name="dollar", label="Dollar", category="currency", abbreviation="$") + + # Time + SECOND = Unit(name="second", category="time", abbreviation="s") + MINUTE = Unit(name="minute", category="time", abbreviation="m") + HOUR = Unit(name="hour", category="time", abbreviation="h") + DAY = Unit(name="day", category="time", abbreviation="d") + WEEK = Unit(name="week", category="time", abbreviation="w") + MONTH = Unit(name="month", category="time", abbreviation="mo") + YEAR = Unit(name="year", category="time", abbreviation="y") + + +class MetricMetadataOptions(BaseModel): + """ + Metric metadata options list + """ + + directions: List[MetricDirection] + units: List[Unit] + + +class MetricMetadataBase(BaseModel): # type: ignore + """ + Base class for additional metric metadata + """ + + direction: Optional[MetricDirection] = Field( + sa_column=SqlaColumn(Enum(MetricDirection)), + default=MetricDirection.NEUTRAL, + ) + unit: Optional[MetricUnit] = Field( + sa_column=SqlaColumn(Enum(MetricUnit)), + default=MetricUnit.UNKNOWN, + ) + + +class MetricMetadataOutput(BaseModel): + """ + Metric metadata output + """ + + direction: Optional[MetricDirection] + unit: Optional[Unit] + + class Config: + orm_mode = True + + +class MetricMetadataInput(BaseModel): + """ + Metric metadata output + """ + + direction: Optional[MetricDirection] + unit: Optional[str] + + +class ImmutableNodeFields(BaseModel): + """ + Node fields that cannot be changed + """ + + name: str + namespace: str = "default" + + +class MutableNodeFields(BaseModel): + """ + Node fields that can be changed. + """ + + display_name: Optional[str] + description: str + mode: NodeMode + primary_key: Optional[List[str]] + custom_metadata: Optional[Dict] + + +class MutableNodeQueryField(BaseModel): + """ + Query field for node. + """ + + query: str + + +class NodeNameList(BaseModel): + """ + List of node names + """ + + __root__: List[str] + + +class NodeIndexItem(BaseModel): + """ + Node details used for indexing purposes + """ + + name: str + display_name: str + description: str + type: NodeType + + +class NodeMinimumDetail(BaseModel): + """ + List of high level node details + """ + + name: str + display_name: str + description: str + version: str + type: NodeType + status: NodeStatus + mode: NodeMode + updated_at: UTCDatetime + tags: Optional[List[TagMinimum]] + edited_by: Optional[List[str]] + + class Config: + orm_mode = True + + +class AttributeTypeName(BaseModel): + """ + Attribute type name. + """ + + namespace: str + name: str + + class Config: + orm_mode = True + + +class AttributeOutput(BaseModel): + """ + Column attribute output. + """ + + attribute_type: AttributeTypeName + + class Config: + orm_mode = True + + +class DimensionAttributeOutput(BaseModel): + """ + Dimension attribute output should include the name and type + """ + + name: str + node_name: Optional[str] + node_display_name: Optional[str] + properties: list[str] | None + type: str + path: List[str] + filter_only: bool = False + + +class ColumnOutput(BaseModel): + """ + A simplified column schema, without ID or dimensions. + """ + + name: str + display_name: Optional[str] + type: str + attributes: Optional[List[AttributeOutput]] + dimension: Optional[NodeNameOutput] + partition: Optional[PartitionOutput] + + class Config: + """ + Should perform validation on assignment + """ + + orm_mode = True + validate_assignment = True + + _extract_type = validator("type", pre=True, allow_reuse=True)( + lambda raw: str(raw), + ) + + +class SourceColumnOutput(BaseModel): + """ + A column used in creation of a source node + """ + + name: str + type: ColumnType + attributes: Optional[List[AttributeOutput]] + dimension: Optional[str] + + class Config: + """ + Should perform validation on assignment + """ + + validate_assignment = True + + @root_validator + def type_string(cls, values): + """ + Extracts the type as a string + """ + values["type"] = str(values.get("type")) + return values + + +class SourceNodeFields(BaseModel): + """ + Source node fields that can be changed. + """ + + catalog: str + schema_: str + table: str + columns: List["SourceColumnOutput"] + missing_table: bool = False + + +class CubeNodeFields(BaseModel): + """ + Cube-specific fields that can be changed + """ + + metrics: List[str] + dimensions: List[str] + filters: Optional[List[str]] + orderby: Optional[List[str]] + limit: Optional[int] + description: str + mode: NodeMode + + +class MetricNodeFields(BaseModel): + """ + Metric node fields that can be changed + """ + + required_dimensions: Optional[List[str]] + metric_metadata: Optional[MetricMetadataInput] + + +# +# Create and Update objects +# + + +class CreateNode( + ImmutableNodeFields, + MutableNodeFields, + MutableNodeQueryField, + MetricNodeFields, +): + """ + Create non-source node object. + """ + + +class CreateSourceNode(ImmutableNodeFields, MutableNodeFields, SourceNodeFields): + """ + A create object for source nodes + """ + + query: Optional[str] = None + + +class CreateCubeNode(ImmutableNodeFields, MutableNodeFields, CubeNodeFields): + """ + A create object for cube nodes + """ + + +class UpdateNode( + MutableNodeFields, + SourceNodeFields, + MutableNodeQueryField, + MetricNodeFields, + CubeNodeFields, +): + """ + Update node object where all fields are optional + """ + + __annotations__ = { + k: Optional[v] + for k, v in { + **SourceNodeFields.__annotations__, + **MutableNodeFields.__annotations__, + **MutableNodeQueryField.__annotations__, + **CubeNodeFields.__annotations__, + }.items() + } + + class Config: + """ + Do not allow fields other than the ones defined here. + """ + + extra = Extra.forbid + + +# +# Response output objects +# + + +class GenericNodeOutputModel(BaseModel): + """ + A generic node output model that flattens the current node revision info + into the top-level fields on the output model. + """ + + @root_validator(pre=True) + def flatten_current( + cls, + values: GetterDict, + ) -> Union[GetterDict, Dict[str, Any]]: + """ + Flatten the current node revision into top-level fields. + """ + current = values.get("current") + if current is None: + return values + current_dict = dict(current.__dict__.items()) + final_dict = { + "namespace": values.get("namespace"), + "created_at": values.get("created_at"), + "deactivated_at": values.get("deactivated_at"), + "current_version": values.get("current_version"), + "catalog": values.get("catalog"), + "missing_table": values.get("missing_table"), + "tags": values.get("tags"), + "created_by": values.get("created_by").__dict__, + } + for k, v in current_dict.items(): + final_dict[k] = v + final_dict["node_revision_id"] = final_dict["id"] + return final_dict + + +class TableOutput(BaseModel): + """ + Output for table information. + """ + + id: Optional[int] + catalog: Optional[CatalogInfo] + schema_: Optional[str] + table: Optional[str] + database: Optional[DatabaseOutput] + + +class NodeRevisionOutput(BaseModel): + """ + Output for a node revision with information about columns and if it is a metric. + """ + + id: int + node_id: int + type: NodeType + name: str + display_name: str + version: str + status: NodeStatus + mode: NodeMode + catalog: Optional[CatalogInfo] + schema_: Optional[str] + table: Optional[str] + description: str = "" + query: Optional[str] = None + availability: Optional[AvailabilityStateBase] = None + columns: List[ColumnOutput] + updated_at: UTCDatetime + materializations: List[MaterializationConfigOutput] + parents: List[NodeNameOutput] + metric_metadata: Optional[MetricMetadataOutput] = None + dimension_links: Optional[List[LinkDimensionOutput]] + custom_metadata: Optional[Dict] = None + + class Config: + orm_mode = True + + +class NodeOutput(GenericNodeOutputModel): + """ + Output for a node that shows the current revision. + """ + + namespace: str + id: int = Field(alias="node_revision_id") + node_id: int + type: NodeType + name: str + display_name: str + version: str + status: NodeStatus + mode: NodeMode + catalog: Optional[CatalogInfo] + schema_: Optional[str] + table: Optional[str] + description: str = "" + query: Optional[str] = None + availability: Optional[AvailabilityStateBase] = None + columns: List[ColumnOutput] + updated_at: UTCDatetime + materializations: List[MaterializationConfigOutput] + parents: List[NodeNameOutput] + metric_metadata: Optional[MetricMetadataOutput] = None + dimension_links: Optional[List[LinkDimensionOutput]] + created_at: UTCDatetime + created_by: UserNameOnly + tags: List[TagOutput] = [] + current_version: str + missing_table: Optional[bool] = False + custom_metadata: Optional[Dict] = None + + class Config: + orm_mode = True + + @classmethod + def load_options(cls): + """ + ORM options to successfully load this object + """ + from datajunction_server.database.node import ( + Node, + NodeRevision, + ) + + return [ + selectinload(Node.current).options(*NodeRevision.default_load_options()), + joinedload(Node.tags), + selectinload(Node.created_by), + ] + + +class DAGNodeRevisionOutput(BaseModel): + """ + Output for a node revision with information about columns and if it is a metric. + """ + + id: int = Field(alias="node_revision_id") + node_id: int + type: NodeType + name: str + display_name: str + version: str + status: NodeStatus + mode: NodeMode + catalog: Optional[CatalogInfo] + schema_: Optional[str] + table: Optional[str] + description: str = "" + columns: List[ColumnOutput] + updated_at: UTCDatetime + parents: List[NodeNameOutput] + dimension_links: List[LinkDimensionOutput] + + class Config: + allow_population_by_field_name = True + orm_mode = True + + +class DAGNodeOutput(GenericNodeOutputModel): + """ + Output for a node in another node's DAG + """ + + namespace: str + id: int = Field(alias="node_revision_id") + node_id: int + type: NodeType + name: str + display_name: str + version: str + status: NodeStatus + mode: NodeMode + catalog: Optional[CatalogInfo] + schema_: Optional[str] + table: Optional[str] + description: str = "" + columns: List[ColumnOutput] + updated_at: UTCDatetime + parents: List[NodeNameOutput] + dimension_links: List[LinkDimensionOutput] + created_at: UTCDatetime + tags: List[TagOutput] = [] + current_version: str + + class Config: + orm_mode = True + + +class NodeValidation(BaseModel): + """ + A validation of a provided node definition + """ + + message: str + status: NodeStatus + dependencies: List[NodeRevisionOutput] + columns: List[ColumnOutput] + errors: List[DJError] + missing_parents: List[str] + + +class LineageColumn(BaseModel): + """ + Column in lineage graph + """ + + column_name: str + node_name: Optional[str] = None + node_type: Optional[str] = None + display_name: Optional[str] = None + lineage: Optional[List["LineageColumn"]] = None + + +LineageColumn.update_forward_refs() + + +class NamespaceOutput(BaseModel): + """ + Output for a namespace that includes the number of nodes + """ + + namespace: str + num_nodes: int + + +class NodeIndegreeOutput(BaseModel): + """ + Node indegree output + """ + + name: str + indegree: int + + +@dataclass +class NodeCursor(Cursor): + """Cursor that represents a node in a paginated list.""" + + created_at: UTCDatetime + id: int + + @classmethod + def decode(cls, serialized: str) -> "NodeCursor": + return cast(NodeCursor, super().decode(serialized)) diff --git a/datajunction-server/datajunction_server/models/node_type.py b/datajunction-server/datajunction_server/models/node_type.py new file mode 100644 index 000000000..9f91fab5b --- /dev/null +++ b/datajunction-server/datajunction_server/models/node_type.py @@ -0,0 +1,48 @@ +"""Node type""" + +from pydantic import BaseModel + +from datajunction_server.enum import StrEnum + + +class NodeType(StrEnum): + """ + Node type. + + A node can have 4 types, currently: + + 1. SOURCE nodes are root nodes in the DAG, and point to tables or views in a DB. + 2. TRANSFORM nodes are SQL transformations, reading from SOURCE/TRANSFORM nodes. + 3. METRIC nodes are leaves in the DAG, and have a single aggregation query. + 4. DIMENSION nodes are special SOURCE nodes that can be auto-joined with METRICS. + 5. CUBE nodes contain a reference to a set of METRICS and a set of DIMENSIONS. + """ + + SOURCE = "source" + TRANSFORM = "transform" + METRIC = "metric" + DIMENSION = "dimension" + CUBE = "cube" + + +class NodeNameOutput(BaseModel): + """ + Node name only + """ + + name: str + + class Config: + orm_mode = True + + +class NodeNameVersion(BaseModel): + """ + Node name and version + """ + + name: str + version: str + + class Config: + orm_mode = True diff --git a/datajunction-server/datajunction_server/models/partition.py b/datajunction-server/datajunction_server/models/partition.py new file mode 100644 index 000000000..9a088935e --- /dev/null +++ b/datajunction-server/datajunction_server/models/partition.py @@ -0,0 +1,107 @@ +"""Partition-related models.""" + +from typing import TYPE_CHECKING, List, Optional + +from pydantic.main import BaseModel + +from datajunction_server.enum import StrEnum + +if TYPE_CHECKING: + pass + + +class PartitionType(StrEnum): + """ + Partition type. + + A partition can be temporal or categorical + """ + + TEMPORAL = "temporal" + CATEGORICAL = "categorical" + + +class Granularity(StrEnum): + """ + Time dimension granularity. + """ + + SECOND = "second" + MINUTE = "minute" + HOUR = "hour" + DAY = "day" + WEEK = "week" + MONTH = "month" + QUARTER = "quarter" + YEAR = "year" + + +class PartitionInput(BaseModel): + """ + Expected settings for specifying a partition column + """ + + type_: PartitionType + + # + # Temporal partitions will additionally have the following properties: + # + # Timestamp granularity + granularity: Optional[Granularity] + # Timestamp format + format: Optional[str] + + +class PartitionBackfill(BaseModel): + """ + Used for setting backfilled values + """ + + column_name: str + + # Backfilled values and range. Most temporal partitions will just use `range`, but some may + # optionally use `values` to specify specific values + # Ex: values: [20230901] + # range: [20230901, 20231001] + values: Optional[List] + range: Optional[List] + + class Config: + orm_mode = True + + +class PartitionOutput(BaseModel): + """ + Output for partition + """ + + type_: PartitionType + format: Optional[str] + granularity: Optional[str] + expression: Optional[str] + + class Config: + orm_mode = True + + +class PartitionColumnOutput(BaseModel): + """ + Output for partition columns + """ + + name: str + type_: PartitionType + format: Optional[str] + expression: Optional[str] + + +class BackfillOutput(BaseModel): + """ + Output model for backfills + """ + + spec: Optional[List[PartitionBackfill]] + urls: Optional[List[str]] + + class Config: + orm_mode = True diff --git a/datajunction-server/datajunction_server/models/query.py b/datajunction-server/datajunction_server/models/query.py new file mode 100644 index 000000000..bdb5968d7 --- /dev/null +++ b/datajunction-server/datajunction_server/models/query.py @@ -0,0 +1,164 @@ +""" +Models for queries. +""" + +from datetime import datetime +from typing import Any, List, Optional + +import msgpack +from pydantic import AnyHttpUrl, validator +from pydantic.fields import Field +from pydantic.main import BaseModel + +from datajunction_server.enum import IntEnum +from datajunction_server.typing import QueryState, Row + + +class BaseQuery(BaseModel): + """ + Base class for query models. + """ + + catalog_name: Optional[str] + engine_name: Optional[str] = None + engine_version: Optional[str] = None + + class Config: + allow_population_by_field_name = True + + +class QueryCreate(BaseQuery): + """ + Model for submitted queries. + """ + + engine_name: str + engine_version: str + catalog_name: str + submitted_query: str + async_: bool = False + + +class ColumnMetadata(BaseModel): + """ + A simple model for column metadata. + """ + + name: str + type: str + column: Optional[str] + node: Optional[str] + semantic_entity: Optional[str] + semantic_type: Optional[str] + + def __hash__(self): + return hash((self.name, self.type)) # pragma: no cover + + +class StatementResults(BaseModel): + """ + Results for a given statement. + + This contains the SQL, column names and types, and rows + """ + + sql: str + columns: List[ColumnMetadata] + rows: List[Row] + + # this indicates the total number of rows, and is useful for paginated requests + row_count: int = 0 + + +class QueryResults(BaseModel): + """ + Results for a given query. + """ + + __root__: List[StatementResults] + + +class TableRef(BaseModel): + """ + Table reference + """ + + catalog: str + schema_: str = Field(alias="schema") + table: str + + +class QueryWithResults(BaseModel): + """ + Model for query with results. + """ + + id: str + engine_name: Optional[str] = None + engine_version: Optional[str] = None + submitted_query: str + executed_query: Optional[str] = None + + scheduled: Optional[datetime] = None + started: Optional[datetime] = None + finished: Optional[datetime] = None + + state: QueryState = QueryState.UNKNOWN + progress: float = 0.0 + + output_table: Optional[TableRef] + results: QueryResults + next: Optional[AnyHttpUrl] = None + previous: Optional[AnyHttpUrl] = None + errors: List[str] + links: Optional[List[AnyHttpUrl]] = None + + @validator("scheduled", pre=True) + def parse_scheduled_date_string(cls, value): + """ + Convert string date values to datetime + """ + return datetime.fromisoformat(value) if isinstance(value, str) else value + + @validator("started", pre=True) + def parse_started_date_string(cls, value): + """ + Convert string date values to datetime + """ + return datetime.fromisoformat(value) if isinstance(value, str) else value + + @validator("finished", pre=True) + def parse_finisheddate_string(cls, value): + """ + Convert string date values to datetime + """ + return datetime.fromisoformat(value) if isinstance(value, str) else value + + +class QueryExtType(IntEnum): + """ + Custom ext type for msgpack. + """ + + UUID = 1 + TIMESTAMP = 2 + + +def encode_results(obj: Any) -> Any: + """ + Custom msgpack encoder for ``QueryWithResults``. + """ + if isinstance(obj, datetime): + return msgpack.ExtType(QueryExtType.TIMESTAMP, obj.isoformat().encode("utf-8")) + + return obj + + +def decode_results(code: int, data: bytes) -> Any: + """ + Custom msgpack decoder for ``QueryWithResults``. + """ + if code == QueryExtType.TIMESTAMP: + return datetime.fromisoformat(data.decode()) + + return msgpack.ExtType(code, data) diff --git a/datajunction-server/datajunction_server/models/sql.py b/datajunction-server/datajunction_server/models/sql.py new file mode 100644 index 000000000..8cd0c388b --- /dev/null +++ b/datajunction-server/datajunction_server/models/sql.py @@ -0,0 +1,52 @@ +""" +Models for generated SQL +""" + +from typing import List, Optional + +from pydantic.class_validators import root_validator +from pydantic.main import BaseModel + +from datajunction_server.errors import DJQueryBuildError +from datajunction_server.models.cube_materialization import Measure +from datajunction_server.models.engine import Dialect +from datajunction_server.models.node_type import NodeNameVersion +from datajunction_server.models.query import ColumnMetadata +from datajunction_server.transpilation import get_transpilation_plugin + + +class GeneratedSQL(BaseModel): + """ + Generated SQL for a given node, the output of a QueryBuilder(...).build() call. + """ + + node: NodeNameVersion + sql: str + sql_transpilation_library: Optional[str] = None + columns: Optional[List[ColumnMetadata]] = None # pragma: no-cover + grain: list[str] | None = None + dialect: Optional[Dialect] = None + upstream_tables: Optional[List[str]] = None + metrics: dict[str, tuple[list[Measure], str]] | None = None + spark_conf: dict[str, str] | None = None + errors: Optional[List[DJQueryBuildError]] = None + + @root_validator(pre=False) + def transpile_sql( + cls, + values, + ): + """ + Transpiles SQL to the specified dialect with the configured transpilation plugin. + If no plugin is configured, it will just return the original generated query. + """ + if values.get("sql_transpilation_library"): + plugin = get_transpilation_plugin( # pragma: no cover + values.get("sql_transpilation_library"), + ) + values["sql"] = plugin.transpile_sql( # pragma: no cover + values["sql"], + input_dialect=Dialect.SPARK, + output_dialect=values["dialect"], + ) + return values diff --git a/datajunction-server/datajunction_server/models/table.py b/datajunction-server/datajunction_server/models/table.py new file mode 100644 index 000000000..54d63830f --- /dev/null +++ b/datajunction-server/datajunction_server/models/table.py @@ -0,0 +1,48 @@ +""" +Models for tables. +""" + +from typing import List, Optional, TypedDict + +from pydantic import Field +from pydantic.main import BaseModel + + +class TableYAML(TypedDict, total=False): + """ + Schema of a table in the YAML file. + """ + + catalog: Optional[str] + schema: Optional[str] + table: str + cost: float + + +class TableBase(BaseModel): + """ + A base table. + """ + + schema_: Optional[str] = Field(default=None, alias="schema") + table: str + cost: float = 1.0 + + +class CreateColumn(BaseModel): + """ + A column creation request + """ + + name: str + type: str + + +class CreateTable(TableBase): + """ + Create table input + """ + + database_name: str + catalog_name: str + columns: List[CreateColumn] diff --git a/datajunction-server/datajunction_server/models/tag.py b/datajunction-server/datajunction_server/models/tag.py new file mode 100644 index 000000000..5163420d5 --- /dev/null +++ b/datajunction-server/datajunction_server/models/tag.py @@ -0,0 +1,83 @@ +""" +Models for tags. +""" + +from typing import TYPE_CHECKING, Any, Dict, Optional + +from pydantic import Extra +from pydantic.main import BaseModel + +if TYPE_CHECKING: + pass + + +class MutableTagFields(BaseModel): + """ + Tag fields that can be changed. + """ + + description: Optional[str] + display_name: Optional[str] + tag_metadata: Optional[Dict[str, Any]] = {} + + class Config: + """ + Allow types for tag metadata. + """ + + arbitrary_types_allowed = True + + +class ImmutableTagFields(BaseModel): + """ + Tag fields that cannot be changed. + """ + + name: str + tag_type: str + + +class CreateTag(ImmutableTagFields, MutableTagFields): + """ + Create tag model. + """ + + +class TagMinimum(BaseModel): + """ + Output tag model. + """ + + name: str + + class Config: + orm_mode = True + + +class TagOutput(ImmutableTagFields, MutableTagFields): + """ + Output tag model. + """ + + class Config: + orm_mode = True + + +class UpdateTag(MutableTagFields): + """ + Update tag model. Only works on mutable fields. + """ + + __annotations__ = { + k: Optional[v] + for k, v in { + **MutableTagFields.__annotations__, + }.items() + } + + class Config: + """ + Do not allow fields other than the ones defined here. + """ + + extra = Extra.forbid diff --git a/datajunction-server/datajunction_server/models/user.py b/datajunction-server/datajunction_server/models/user.py new file mode 100644 index 000000000..404bcb038 --- /dev/null +++ b/datajunction-server/datajunction_server/models/user.py @@ -0,0 +1,72 @@ +""" +Models for users and auth +""" + +from typing import List, Optional + +from pydantic import BaseModel + +from datajunction_server.database.user import OAuthProvider +from datajunction_server.models.catalog import CatalogInfo +from datajunction_server.models.collection import CollectionInfo +from datajunction_server.models.node import NodeType +from datajunction_server.models.tag import TagOutput +from datajunction_server.typing import UTCDatetime + + +class CreatedNode(BaseModel): + """ + A node created by a user + """ + + namespace: str + type: NodeType + name: str + catalog: Optional[CatalogInfo] + schema_: Optional[str] + table: Optional[str] + description: str = "" + query: Optional[str] = None + created_at: UTCDatetime + current_version: str + missing_table: Optional[bool] = False + + class Config: + orm_mode = True + + +class UserOutput(BaseModel): + """User information to be included in responses""" + + id: int + username: str + email: Optional[str] + name: Optional[str] + oauth_provider: OAuthProvider + is_admin: bool = False + created_collections: Optional[List[CollectionInfo]] = [] + created_nodes: Optional[List[CreatedNode]] = [] + created_tags: Optional[List[TagOutput]] = [] + + class Config: + orm_mode = True + + +class UserNameOnly(BaseModel): + """ + Username only + """ + + username: str + + class Config: + orm_mode = True + + +class UserActivity(BaseModel): + """ + User activity info + """ + + username: str + count: int diff --git a/datajunction-server/datajunction_server/naming.py b/datajunction-server/datajunction_server/naming.py new file mode 100644 index 000000000..52c84654b --- /dev/null +++ b/datajunction-server/datajunction_server/naming.py @@ -0,0 +1,59 @@ +"""Naming related utils.""" + +from string import ascii_letters, digits +from typing import List + +SEPARATOR = "." + +ACCEPTABLE_CHARS = set(ascii_letters + digits + "_") +LOOKUP_CHARS = { + ".": "DOT", + "'": "QUOTE", + '"': "DQUOTE", + "`": "BTICK", + "!": "EXCL", + "@": "AT", + "#": "HASH", + "$": "DOLLAR", + "%": "PERC", + "^": "CARAT", + "&": "AMP", + "*": "STAR", + "(": "LPAREN", + ")": "RPAREN", + "[": "LBRACK", + "]": "RBRACK", + "-": "MINUS", + "+": "PLUS", + "=": "EQ", + "/": "FSLSH", + "\\": "BSLSH", + "|": "PIPE", + "~": "TILDE", +} + + +def amenable_name(name: str) -> str: + """Takes a string and makes it have only alphanumerics""" + ret: List[str] = [] + cont: List[str] = [] + for char in name: + if char in ACCEPTABLE_CHARS: + cont.append(char) + else: + ret.append("".join(cont)) + ret.append(LOOKUP_CHARS.get(char, "UNK")) + cont = [] + + return ("_".join(ret) + "_" + "".join(cont)).strip("_") + + +def from_amenable_name(name: str) -> str: + """ + Takes a string and converts it back to a namespaced name + """ + for replacement, to_replace in LOOKUP_CHARS.items(): + name = name.replace(f"_{to_replace}_", replacement) + name = name.replace(f"_{to_replace}", replacement) + name = name.replace(f"{to_replace}_", replacement) + return name diff --git a/datajunction-server/datajunction_server/service_clients.py b/datajunction-server/datajunction_server/service_clients.py new file mode 100644 index 000000000..86cc8cba6 --- /dev/null +++ b/datajunction-server/datajunction_server/service_clients.py @@ -0,0 +1,439 @@ +"""Clients for various configurable services.""" + +import logging +from http import HTTPStatus +from typing import TYPE_CHECKING, Dict, List, Optional, Union +from urllib.parse import urljoin + +import requests +from requests.adapters import HTTPAdapter +from urllib3 import Retry + +from datajunction_server.database.column import Column +from datajunction_server.errors import ( + DJDoesNotExistException, + DJError, + DJQueryServiceClientEntityNotFound, + DJQueryServiceClientException, + ErrorCode, +) +from datajunction_server.models.cube_materialization import ( + DruidCubeMaterializationInput, +) +from datajunction_server.models.materialization import ( + DruidMaterializationInput, + GenericMaterializationInput, + MaterializationInfo, +) +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.partition import PartitionBackfill +from datajunction_server.models.query import QueryCreate, QueryWithResults +from datajunction_server.sql.parsing.types import ColumnType + +if TYPE_CHECKING: + from datajunction_server.database.engine import Engine + +_logger = logging.getLogger(__name__) + + +class RequestsSessionWithEndpoint(requests.Session): + """ + Creates a requests session that comes with an endpoint that all + subsequent requests will use as a prefix. + """ + + def __init__(self, endpoint: str = None, retry_strategy: Retry = None): + super().__init__() + self.endpoint = endpoint + self.mount("http://", HTTPAdapter(max_retries=retry_strategy)) + self.mount("https://", HTTPAdapter(max_retries=retry_strategy)) + + def request(self, method, url, *args, **kwargs): + """ + Make the request with the full URL. + """ + url = self.construct_url(url) + return super().request(method, url, *args, **kwargs) + + def prepare_request(self, request, *args, **kwargs): + """ + Prepare the request with the full URL. + """ + request.url = self.construct_url(request.url) + return super().prepare_request( + request, + *args, + **kwargs, + ) + + def construct_url(self, url): + """ + Construct full URL based off the endpoint. + """ + return urljoin(self.endpoint, url) + + +class QueryServiceClient: + """ + Client for the query service. + """ + + HEADERS_TO_IGNORE = ("accept-encoding",) + + def __init__(self, uri: str, retries: int = 0): + self.uri = uri + retry_strategy = Retry( + total=retries, + backoff_factor=1.5, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "POST", "PUT", "PATCH"], + ) + self.requests_session = RequestsSessionWithEndpoint( + endpoint=self.uri, + retry_strategy=retry_strategy, + ) + + @staticmethod + def filtered_headers(request_headers: Dict[str, str]): + """ + The request headers with the headers to ignore filtered out. + """ + return { + key: value + for key, value in request_headers.items() + if key.lower() not in QueryServiceClient.HEADERS_TO_IGNORE + } + + def get_columns_for_table( + self, + catalog: str, + schema: str, + table: str, + request_headers: Optional[Dict[str, str]] = None, + engine: Optional["Engine"] = None, + ) -> List[Column]: + """ + Retrieves columns for a table. + """ + response = self.requests_session.get( + f"/table/{catalog}.{schema}.{table}/columns/", + params={ + "engine": engine.name, + "engine_version": engine.version, + } + if engine + else {}, + headers={ + **self.requests_session.headers, + **QueryServiceClient.filtered_headers(request_headers), + } + if request_headers + else self.requests_session.headers, + ) + if response.status_code not in (200, 201): + if response.status_code == HTTPStatus.NOT_FOUND: + raise DJDoesNotExistException( + message=f"Table not found: {response.text}", + ) + raise DJQueryServiceClientException( + message=f"Error response from query service: {response.text}", + ) + table_columns = response.json()["columns"] + if not table_columns: + raise DJDoesNotExistException( + message=f"No columns found: {response.text}", + ) + return [ + Column(name=column["name"], type=ColumnType(column["type"]), order=idx) + for idx, column in enumerate(table_columns) + ] + + def create_view( + self, + view_name: str, + query_create: QueryCreate, + request_headers: Optional[Dict[str, str]] = None, + ) -> str: + """ + Re-create a view using the query service. + """ + response = self.requests_session.post( + "/queries/", + headers={ + **self.requests_session.headers, + **QueryServiceClient.filtered_headers(request_headers), + } + if request_headers + else self.requests_session.headers, + json=query_create.dict(), + ) + response_data = response.json() + if response.status_code not in (200, 201): + raise DJQueryServiceClientException( + message=f"Error response from query service: {response_data['message']}", + errors=[ + DJError(code=ErrorCode.QUERY_SERVICE_ERROR, message=error) + for error in response_data["errors"] + ], + http_status_code=response.status_code, + ) + return f"View '{view_name}' created successfully." + + def submit_query( + self, + query_create: QueryCreate, + request_headers: Optional[Dict[str, str]] = None, + ) -> QueryWithResults: + """ + Submit a query to the query service + """ + response = self.requests_session.post( + "/queries/", + headers={ + **self.requests_session.headers, + **QueryServiceClient.filtered_headers(request_headers), + } + if request_headers + else self.requests_session.headers, + json=query_create.dict(), + ) + response_data = response.json() + if response.status_code not in (200, 201): + raise DJQueryServiceClientException( + message=f"Error response from query service: {response_data['message']}", + errors=[ + DJError(code=ErrorCode.QUERY_SERVICE_ERROR, message=error) + for error in response_data["errors"] + ], + http_status_code=response.status_code, + ) + query_info = response.json() + return QueryWithResults(**query_info) + + def get_query( + self, + query_id: str, + request_headers: Optional[Dict[str, str]] = None, + ) -> QueryWithResults: + """ + Get a previously submitted query + """ + get_query_endpoint = f"/queries/{query_id}/" + response = self.requests_session.get( + get_query_endpoint, + headers={**self.requests_session.headers, **request_headers} + if request_headers + else self.requests_session.headers, + ) + if response.status_code == 404: + _logger.exception( + "[DJQS] Failed to get query_id=%s with `GET %s`", + query_id, + get_query_endpoint, + exc_info=True, + ) + raise DJQueryServiceClientEntityNotFound( # pragma: no cover + message=f"Error response from query service: {response.text}", + ) + if response.status_code not in (200, 201): + raise DJQueryServiceClientException( + message=f"Error response from query service: {response.text}", + ) + query_info = response.json() + _logger.info( + "[DJQS] Retrieved query_id=%s with `GET %s`", + query_id, + get_query_endpoint, + ) + return QueryWithResults(**query_info) + + def materialize( + self, + materialization_input: Union[ + GenericMaterializationInput, + DruidMaterializationInput, + ], + request_headers: Optional[Dict[str, str]] = None, + ) -> MaterializationInfo: + """ + Post a request to the query service asking it to set up a scheduled materialization + for the node. The query service is expected to manage all reruns of this job. Note + that this functionality may be moved to the materialization service at a later point. + """ + response = self.requests_session.post( + "/materialization/", + json=materialization_input.dict(), + headers={ + **self.requests_session.headers, + **QueryServiceClient.filtered_headers(request_headers), + } + if request_headers + else self.requests_session.headers, + ) + if response.status_code not in (200, 201): # pragma: no cover + _logger.exception( + "[DJQS] Failed to materialize node=%s with `POST /materialization/`: %s", + materialization_input.node_name, + materialization_input.dict(), + exc_info=True, + ) + return MaterializationInfo(urls=[], output_tables=[]) + result = response.json() + _logger.info( + "[DJQS] Scheduled materialization for node=%s with `POST /materialization/`", + materialization_input.node_name, + ) + return MaterializationInfo(**result) + + def materialize_cube( + self, + materialization_input: DruidCubeMaterializationInput, + request_headers: Optional[Dict[str, str]] = None, + ) -> MaterializationInfo: + """ + Post a request to the query service asking it to set up a scheduled materialization + for the node. The query service is expected to manage all reruns of this job. Note + that this functionality may be moved to the materialization service at a later point. + """ + response = self.requests_session.post( + "/cubes/materialize", + json=materialization_input.dict(), + headers={ + **self.requests_session.headers, + **QueryServiceClient.filtered_headers(request_headers), + } + if request_headers + else self.requests_session.headers, + timeout=20, + ) + if response.status_code not in (200, 201): # pragma: no cover + _logger.exception( + "[DJQS] Failed to schedule cube materialization for" + " node=%s with `POST /cubes/materialize`", + materialization_input.cube, + exc_info=True, + ) + return MaterializationInfo(urls=[], output_tables=[]) # pragma: no cover + result = response.json() # pragma: no cover + _logger.info( + "[DJQS] Scheduled cube materialization for node=%s with `POST /cubes/materialize`", + materialization_input.cube, + ) + return MaterializationInfo(**result) # pragma: no cover + + def deactivate_materialization( + self, + node_name: str, + materialization_name: str, + request_headers: Optional[Dict[str, str]] = None, + ) -> MaterializationInfo: + """ + Deactivates the specified node materialization + """ + deactivate_endpoint = f"/materialization/{node_name}/{materialization_name}/" + response = self.requests_session.delete( + deactivate_endpoint, + headers={ + **self.requests_session.headers, + **QueryServiceClient.filtered_headers(request_headers), + } + if request_headers + else self.requests_session.headers, + ) + if response.status_code not in (200, 201): # pragma: no cover + _logger.exception( + "[DJQS] Failed to deactivate materialization for node=%s with `DELETE %s`", + node_name, + deactivate_endpoint, + exc_info=True, + ) + return MaterializationInfo(urls=[], output_tables=[]) + result = response.json() + _logger.info( + "[DJQS] Deactivated materialization for node=%s with `DELETE %s`", + node_name, + deactivate_endpoint, + ) + return MaterializationInfo(**result) + + def get_materialization_info( + self, + node_name: str, + node_version: str, + node_type: NodeType, + materialization_name: str, + request_headers: Optional[Dict[str, str]] = None, + ) -> MaterializationInfo: + """ + Gets materialization info for the node and materialization config name. + """ + info_endpoint = ( + f"/materialization/{node_name}/{node_version}/{materialization_name}/" + f"?node_type={node_type}" + ) + response = self.requests_session.get( + info_endpoint, + timeout=3, + headers={ + **self.requests_session.headers, + **QueryServiceClient.filtered_headers(request_headers), + } + if request_headers + else self.requests_session.headers, + ) + if response.status_code not in (200, 201): + _logger.exception( + "[DJQS] Failed to get materialization info for node=%s with `GET %s`", + node_name, + info_endpoint, + exc_info=True, + ) + return MaterializationInfo(output_tables=[], urls=[]) + + _logger.info( + "[DJQS] Retrieved materialization info for node=%s with `GET %s`", + node_name, + info_endpoint, + ) + return MaterializationInfo(**response.json()) + + def run_backfill( + self, + node_name: str, + node_version: str, + node_type: NodeType, + materialization_name: str, + partitions: List[PartitionBackfill], + request_headers: Optional[Dict[str, str]] = None, + ) -> MaterializationInfo: + """Kicks off a backfill with the given backfill spec""" + backfill_endpoint = ( + f"/materialization/run/{node_name}/{materialization_name}" + f"/?node_version={node_version}&node_type={node_type}" + ) + response = self.requests_session.post( + backfill_endpoint, + json=[partition.dict() for partition in partitions], + headers={ + **self.requests_session.headers, + **QueryServiceClient.filtered_headers(request_headers), + } + if request_headers + else self.requests_session.headers, + timeout=20, + ) + if response.status_code not in (200, 201): + _logger.exception( # pragma: no cover + "[DJQS] Failed to run backfill for node=%s with `POST %s`", + node_name, + backfill_endpoint, + exc_info=True, + ) + return MaterializationInfo(output_tables=[], urls=[]) # pragma: no cover + + _logger.info( + "[DJQS] Ran backfill for node=%s with `POST %s`", + node_name, + backfill_endpoint, + ) + return MaterializationInfo(**response.json()) diff --git a/datajunction-server/datajunction_server/sql/__init__.py b/datajunction-server/datajunction_server/sql/__init__.py new file mode 100644 index 000000000..008fb504a --- /dev/null +++ b/datajunction-server/datajunction_server/sql/__init__.py @@ -0,0 +1,3 @@ +""" +Common dj sql imports +""" diff --git a/datajunction-server/datajunction_server/sql/dag.py b/datajunction-server/datajunction_server/sql/dag.py new file mode 100644 index 000000000..b47a85924 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/dag.py @@ -0,0 +1,863 @@ +""" +DAG related functions. +""" + +import itertools +from typing import Dict, List, Optional, Set, Union + +from sqlalchemy import and_, func, join, literal, or_, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import aliased, contains_eager, joinedload, selectinload +from sqlalchemy.sql.operators import is_ + +from datajunction_server.database.attributetype import AttributeType, ColumnAttribute +from datajunction_server.database.column import Column +from datajunction_server.database.dimensionlink import DimensionLink +from datajunction_server.database.node import ( + Node, + NodeColumns, + NodeRelationship, + NodeRevision, +) +from datajunction_server.errors import DJDoesNotExistException, DJGraphCycleException +from datajunction_server.models.attribute import ColumnAttributes +from datajunction_server.models.node import DimensionAttributeOutput +from datajunction_server.models.node_type import NodeType +from datajunction_server.utils import SEPARATOR, get_settings + +settings = get_settings() + + +def _node_output_options(): + """ + Statement options to retrieve all NodeOutput objects in one query + """ + return [ + selectinload(Node.current).options( + selectinload(NodeRevision.columns).options( + selectinload(Column.attributes).joinedload( + ColumnAttribute.attribute_type, + ), + selectinload(Column.dimension), + selectinload(Column.partition), + ), + selectinload(NodeRevision.catalog), + selectinload(NodeRevision.parents), + selectinload(NodeRevision.dimension_links).options( + selectinload(DimensionLink.dimension).options( + selectinload(Node.current), + ), + ), + ), + selectinload(Node.tags), + ] + + +async def get_downstream_nodes( + session: AsyncSession, + node_name: str, + node_type: NodeType = None, + include_deactivated: bool = True, + include_cubes: bool = True, + depth: int = -1, +) -> List[Node]: + """ + Gets all downstream children of the given node, filterable by node type. + Uses a recursive CTE query to build out all descendants from the node. + """ + node = await Node.get_by_name( + session, + node_name, + options=_node_output_options(), + ) + if not node: + return [] + initial_dag = ( + select( + NodeRelationship.parent_id, + NodeRevision.node_id, + literal(0).label("depth"), + ) + .where(NodeRelationship.parent_id == node.id) + .join(NodeRevision, NodeRelationship.child_id == NodeRevision.id) + .join( + Node, + (Node.id == NodeRevision.node_id) + & (Node.current_version == NodeRevision.version), + ) + ) + if not include_cubes: + initial_dag = initial_dag.where((NodeRevision.type != NodeType.CUBE)) + dag = initial_dag.cte("downstreams", recursive=True).suffix_with( + "CYCLE node_id SET is_cycle USING path", + ) + + next_layer = ( + select( + dag.c.parent_id, + NodeRevision.node_id, + (dag.c.depth + literal(1)).label("depth"), + ) + .join(NodeRelationship, dag.c.node_id == NodeRelationship.parent_id) + .join(NodeRevision, NodeRelationship.child_id == NodeRevision.id) + .join(Node, Node.id == NodeRevision.node_id) + ) + if not include_cubes: + next_layer = next_layer.where(NodeRevision.type != NodeType.CUBE) + + paths = dag.union_all(next_layer) + + # Calculate the maximum depth for each node + max_depths = ( + select( + paths.c.node_id, + func.max(paths.c.depth).label("max_depth"), + ) + .group_by(paths.c.node_id) + .cte("max_depths") + ) + + # Select nodes with the maximum depth + final_select = select(Node, max_depths.c.max_depth).join( + max_depths, + max_depths.c.node_id == Node.id, + ) + + if not include_deactivated: + final_select = final_select.where(is_(Node.deactivated_at, None)) + + # Add depth filter + if depth > -1: + final_select = final_select.where(max_depths.c.max_depth < depth) + + statement = final_select.order_by(max_depths.c.max_depth, Node.id).options( + *_node_output_options() + ) + results = (await session.execute(statement)).unique().scalars().all() + return [ + downstream + for downstream in results + if downstream.type == node_type or node_type is None + ] + + +async def get_upstream_nodes( + session: AsyncSession, + node_name: str, + node_type: NodeType = None, + include_deactivated: bool = True, +) -> List[Node]: + """ + Gets all upstreams of the given node, filterable by node type. + Uses a recursive CTE query to build out all parents of the node. + """ + node = ( + ( + await session.execute( + select(Node) + .where( + (Node.name == node_name) & (is_(Node.deactivated_at, None)), + ) + .options(joinedload(Node.current)), + ) + ) + .unique() + .scalar() + ) + if not node: + raise DJDoesNotExistException( # pragma: no cover + message=f"Node with name {node_name} does not exist", + ) + + dag = ( + ( + select( + NodeRelationship.child_id, + NodeRevision.id, + NodeRevision.node_id, + ) + .where(NodeRelationship.child_id == node.current.id) + .join(Node, NodeRelationship.parent_id == Node.id) + .join( + NodeRevision, + (Node.id == NodeRevision.node_id) + & (Node.current_version == NodeRevision.version), + ) + ) + .cte("upstreams", recursive=True) + .suffix_with( + "CYCLE node_id SET is_cycle USING path", + ) + ) + + paths = dag.union_all( + select( + dag.c.child_id, + NodeRevision.id, + NodeRevision.node_id, + ) + .join(NodeRelationship, dag.c.id == NodeRelationship.child_id) + .join(Node, NodeRelationship.parent_id == Node.id) + .join( + NodeRevision, + (Node.id == NodeRevision.node_id) + & (Node.current_version == NodeRevision.version), + ), + ) + + node_selector = select(Node) + if not include_deactivated: + node_selector = node_selector.where(is_(Node.deactivated_at, None)) + statement = ( + node_selector.join(paths, paths.c.node_id == Node.id) + .join( + NodeRevision, + (Node.current_version == NodeRevision.version) + & (Node.id == NodeRevision.node_id), + ) + .options(*_node_output_options()) + ) + + results = (await session.execute(statement)).unique().scalars().all() + return [ + upstream + for upstream in results + if upstream.type == node_type or node_type is None + ] + + +async def get_dimensions_dag( + session: AsyncSession, + node_revision: NodeRevision, + with_attributes: bool = True, + depth: int = 30, +) -> List[Union[DimensionAttributeOutput, Node]]: + """ + Gets the dimensions graph of the given node revision with a single recursive CTE query. + This graph is split out into dimension attributes or dimension nodes depending on the + `with_attributes` flag. + """ + + initial_node = aliased(NodeRevision, name="initial_node") + dimension_node = aliased(Node, name="dimension_node") + dimension_rev = aliased(NodeRevision, name="dimension_rev") + current_node = aliased(Node, name="current_node") + current_rev = aliased(NodeRevision, name="current_rev") + next_node = aliased(Node, name="next_node") + next_rev = aliased(NodeRevision, name="next_rev") + column = aliased(Column, name="c") + + # Merge both branching points of the dimensions graph (the column -> dimension + # branch and the node -> dimension branch) into a single CTE. We do this merge because + # subqueries with UNION are not allowed in the recursive CTE. + graph_branches = ( + ( + select( + NodeColumns.node_id.label("node_revision_id"), + Column.dimension_id, + Column.name, + Column.dimension_column, + ) + .select_from(NodeColumns) + .join(Column, NodeColumns.column_id == Column.id) + ) + .union_all( + select( + DimensionLink.node_revision_id, + DimensionLink.dimension_id, + ( + literal("[") + + func.coalesce( + DimensionLink.role, + literal(""), + ) + + literal("]") + ).label("name"), + literal(None).label("dimension_column"), + ).select_from(DimensionLink), + ) + .cte("graph_branches") + ) + + # Recursive CTE + dimensions_graph = ( + select( + initial_node.id.label("path_start"), + graph_branches.c.name.label("col_name"), + graph_branches.c.dimension_column.label("dimension_column"), + dimension_node.id.label("path_end"), + ( + initial_node.name + + "." + + graph_branches.c.name + + "," + + dimension_node.name + ).label( + "join_path", + ), + dimension_node.name.label("node_name"), + dimension_rev.id.label("node_revision_id"), + dimension_rev.display_name.label("node_display_name"), + literal(0).label("depth"), + ) + .select_from(initial_node) + .join(graph_branches, node_revision.id == graph_branches.c.node_revision_id) + .join( + dimension_node, + (dimension_node.id == graph_branches.c.dimension_id) + & (is_(dimension_node.deactivated_at, None)), + ) + .join( + dimension_rev, + and_( + dimension_rev.version == dimension_node.current_version, + dimension_rev.node_id == dimension_node.id, + ), + ) + .where(initial_node.id == node_revision.id) + ).cte("dimensions_graph", recursive=True) + dimensions_graph = dimensions_graph.suffix_with( + "CYCLE node_revision_id SET is_cycle USING path", + ) + + paths = dimensions_graph.union_all( + select( + dimensions_graph.c.path_start, + graph_branches.c.name.label("col_name"), + graph_branches.c.dimension_column.label("dimension_column"), + next_node.id.label("path_end"), + ( + dimensions_graph.c.join_path + + "." + + graph_branches.c.name + + "," + + next_node.name + ).label( + "join_path", + ), + next_node.name.label("node_name"), + next_rev.id.label("node_revision_id"), + next_rev.display_name.label("node_display_name"), + (dimensions_graph.c.depth + literal(1)).label("depth"), + ) + .select_from( + dimensions_graph.join( + current_node, + (dimensions_graph.c.path_end == current_node.id), + ) + .join( + current_rev, + and_( + current_rev.version == current_node.current_version, + current_rev.node_id == current_node.id, + ), + ) + .join( + graph_branches, + (current_rev.id == graph_branches.c.node_revision_id) + & (is_(graph_branches.c.dimension_column, None)), + ) + .join( + next_node, + (next_node.id == graph_branches.c.dimension_id) + & (is_(graph_branches.c.dimension_column, None)) + & (is_(next_node.deactivated_at, None)), + ) + .join( + next_rev, + and_( + next_rev.version == next_node.current_version, + next_rev.node_id == next_node.id, + ), + ), + ) + .where(dimensions_graph.c.depth <= depth), + ) + + # Final SELECT statements + # ---- + # If attributes was set to False, we only need to return the dimension nodes + if not with_attributes: + result = await session.execute( + select(Node) + .select_from(paths) + .join(Node, paths.c.node_name == Node.name) + .options(*_node_output_options()), + ) + return result.unique().scalars().all() + + # Otherwise return the dimension attributes, which include both the dimension + # attributes on the dimension nodes in the DAG as well as the local dimension + # attributes on the initial node + group_concat = ( + func.group_concat + if session.bind.dialect.name in ("sqlite",) + else func.string_agg + ) + final_query = ( + select( + paths.c.node_name, + paths.c.node_display_name, + column.name, + column.type, + group_concat(AttributeType.name, ",").label( + "column_attribute_type_name", + ), + paths.c.join_path, + ) + .select_from(paths) + .join(NodeColumns, NodeColumns.node_id == paths.c.node_revision_id) + .join( + column, + and_( + NodeColumns.column_id == column.id, + or_( + is_(paths.c.dimension_column, None), + paths.c.dimension_column == column.name, + ), + ), + ) + .join(ColumnAttribute, column.id == ColumnAttribute.column_id, isouter=True) + .join( + AttributeType, + ColumnAttribute.attribute_type_id == AttributeType.id, + isouter=True, + ) + .group_by( + paths.c.node_name, + paths.c.node_display_name, + column.name, + column.type, + paths.c.join_path, + ) + .union_all( + select( + NodeRevision.name, + NodeRevision.display_name, + Column.name, + Column.type, + group_concat(AttributeType.name, ",").label( + "column_attribute_type_name", + ), + literal("").label("join_path"), + ) + .select_from(NodeRevision) + .join(NodeColumns, NodeColumns.node_id == NodeRevision.id) + .join(Column, NodeColumns.column_id == Column.id) + .join( + ColumnAttribute, + Column.id == ColumnAttribute.column_id, + isouter=True, + ) + .join( + AttributeType, + ColumnAttribute.attribute_type_id == AttributeType.id, + isouter=True, + ) + .group_by( + NodeRevision.name, + NodeRevision.display_name, + Column.name, + Column.type, + "join_path", + ) + .where(NodeRevision.id == node_revision.id), + ) + ) + + def _extract_roles_from_path(join_path) -> str: + """Extracts dimension roles from the query results' join path""" + roles = [ + path.replace("[", "").replace("]", "").split(".")[-1] + for path in join_path.split(",") + if "[" in path # this indicates that this a role + ] + non_empty_roles = [role for role in roles if role] + return f"[{'->'.join(non_empty_roles)}]" if non_empty_roles else "" + + # Only include a given column it's an attribute on a dimension node or + # if the column is tagged with the attribute type 'dimension' + dimension_attributes = (await session.execute(final_query)).all() + return sorted( + [ + DimensionAttributeOutput( + name=f"{node_name}.{column_name}{_extract_roles_from_path(join_path)}", + node_name=node_name, + node_display_name=node_display_name, + properties=attribute_types.split(",") if attribute_types else [], + type=str(column_type), + path=[ + (path.replace("[", "").replace("]", "")[:-1]) + if path.replace("[", "").replace("]", "").endswith(".") + else path.replace("[", "").replace("]", "") + for path in join_path.split(",")[:-1] + ] + if join_path + else [], + ) + for ( + node_name, + node_display_name, + column_name, + column_type, + attribute_types, + join_path, + ) in dimension_attributes + if ( # column has dimension attribute + join_path == "" + and attribute_types is not None + and ( + ColumnAttributes.DIMENSION.value in attribute_types + or ColumnAttributes.PRIMARY_KEY.value in attribute_types + ) + ) + or ( # column is on dimension node + join_path != "" + or ( + node_name == node_revision.name + and node_revision.type == NodeType.DIMENSION + ) + ) + ], + key=lambda x: (x.name, ",".join(x.path)), + ) + + +async def get_dimensions( + session: AsyncSession, + node: Node, + with_attributes: bool = True, + depth: int = 30, +) -> List[Union[DimensionAttributeOutput, Node]]: + """ + Return all available dimensions for a given node. + * Setting `attributes` to True will return a list of dimension attributes, + * Setting `attributes` to False will return a list of dimension nodes + """ + if node.type == NodeType.METRIC: + dag = await get_dimensions_dag( + session, + node.current.parents[0].current, + with_attributes, + depth=depth, + ) + else: + await session.refresh(node, attribute_names=["current"]) + dag = await get_dimensions_dag( + session, + node.current, + with_attributes, + depth=depth, + ) + return dag + + +async def get_filter_only_dimensions( + session: AsyncSession, + node_name: str, +): + """ + Get dimensions for this node that can only be filtered by and cannot be grouped by + or retrieved as a part of the node's SELECT clause. + """ + filter_only_dimensions = [] + upstreams = await get_upstream_nodes(session, node_name, node_type=NodeType.SOURCE) + for upstream in upstreams: + await session.refresh(upstream.current, ["dimension_links"]) + for link in upstream.current.dimension_links: + await session.refresh(link.dimension, ["current"]) + await session.refresh(link.dimension.current, ["columns"]) + column_mapping = {col.name: col for col in link.dimension.current.columns} + filter_only_dimensions.extend( + [ + DimensionAttributeOutput( + name=dim, + node_name=link.dimension.name, + node_display_name=link.dimension.current.display_name, + type=str(column_mapping[dim.split(SEPARATOR)[-1]].type), + path=[upstream.name], + filter_only=True, + properties=column_mapping[ + dim.split(SEPARATOR)[-1] + ].attribute_names(), + ) + for dim in link.foreign_keys.values() + ], + ) + return filter_only_dimensions + + +async def group_dimensions_by_name( + session: AsyncSession, + node: Node, +) -> Dict[str, List[DimensionAttributeOutput]]: + """ + Group the dimensions for the node by the dimension attribute name + """ + return { + k: list(v) + for k, v in itertools.groupby( + await get_dimensions(session, node), + key=lambda dim: dim.name, + ) + } + + +async def get_shared_dimensions( + session: AsyncSession, + metric_nodes: List[Node], +) -> List[DimensionAttributeOutput]: + """ + Return a list of dimensions that are common between the metric nodes. + """ + parents = await get_metric_parents(session, metric_nodes) + return await get_common_dimensions(session, parents) + + +async def get_metric_parents( + session: AsyncSession, + metric_nodes: list[Node], +) -> list[Node]: + """ + Return a list of parent nodes of the metrics + """ + find_latest_node_revisions = [ + and_( + NodeRevision.name == metric_node.name, + NodeRevision.version == metric_node.current_version, + ) + for metric_node in metric_nodes + ] + statement = ( + select(Node) + .where(or_(*find_latest_node_revisions)) + .select_from( + join( + join( + NodeRevision, + NodeRelationship, + ), + Node, + NodeRelationship.parent_id == Node.id, + ), + ) + ) + return list(set((await session.execute(statement)).scalars().all())) + + +async def get_common_dimensions(session: AsyncSession, nodes: list[Node]): + """ + Return a list of dimensions that are common between the nodes. + """ + metric_nodes = [node for node in nodes if node.type == NodeType.METRIC] + other_nodes = [node for node in nodes if node.type != NodeType.METRIC] + if metric_nodes: + nodes = list(set(other_nodes + await get_metric_parents(session, metric_nodes))) + + common = await group_dimensions_by_name(session, nodes[0]) + for node in nodes[1:]: + node_dimensions = await group_dimensions_by_name(session, node) + + # Merge each set of dimensions based on the name and path + to_delete = set(common.keys() - node_dimensions.keys()) + common_dim_keys = common.keys() & list(node_dimensions.keys()) + if not common_dim_keys: + return [] + for dim_key in to_delete: + del common[dim_key] # pragma: no cover + return sorted( + [y for x in common.values() for y in x], + key=lambda x: (x.name, x.path), + ) + + +async def get_nodes_with_dimension( + session: AsyncSession, + dimension_node: Node, + node_types: Optional[List[NodeType]] = None, +) -> List[NodeRevision]: + """ + Find all nodes that can be joined to a given dimension + """ + to_process = [dimension_node] + processed: Set[str] = set() + final_set: Set[NodeRevision] = set() + while to_process: + current_node = to_process.pop() + processed.add(current_node.name) + + # Dimension nodes are used to expand the searchable graph by finding + # the next layer of nodes that are linked to this dimension + if current_node.type == NodeType.DIMENSION: + statement = ( + select(NodeRevision) + .join( + Node, + onclause=( + (NodeRevision.node_id == Node.id) + & (Node.current_version == NodeRevision.version) + ), + ) + .join( + NodeColumns, + onclause=(NodeRevision.id == NodeColumns.node_id), + ) + .join( + Column, + onclause=(NodeColumns.column_id == Column.id), + ) + .where( + Column.dimension_id.in_( # type: ignore + [current_node.id], + ), + ) + ) + node_revisions = ( + ( + await session.execute( + statement.options(contains_eager(NodeRevision.node)), + ) + ) + .unique() + .scalars() + .all() + ) + + dim_link_statement = ( + select(NodeRevision) + .select_from(DimensionLink) + .join( + NodeRevision, + onclause=(DimensionLink.node_revision_id == NodeRevision.id), + ) + .join( + Node, + onclause=( + (NodeRevision.node_id == Node.id) + & (Node.current_version == NodeRevision.version) + ), + ) + .where(DimensionLink.dimension_id.in_([current_node.id])) + ) + nodes_via_dimension_link = ( + ( + await session.execute( + dim_link_statement.options(contains_eager(NodeRevision.node)), + ) + ) + .unique() + .scalars() + .all() + ) + for node_rev in node_revisions + nodes_via_dimension_link: + if node_rev.name not in processed: # pragma: no cover + to_process.append(node_rev.node) + else: + # All other nodes are added to the result set + current_node = await Node.get_by_name( # type: ignore + session, + current_node.name, + options=[ + joinedload(Node.current).options( + *NodeRevision.default_load_options(), + ), + selectinload(Node.children).options( + selectinload(NodeRevision.node), + ), + ], + ) + if current_node: + final_set.add(current_node.current) + for child in current_node.children: + if child.name not in processed: + to_process.append(child.node) + if node_types: + return [node for node in final_set if node.type in node_types] + return list(final_set) + + +async def get_nodes_with_common_dimensions( + session: AsyncSession, + common_dimensions: List[Node], + node_types: Optional[List[NodeType]] = None, +) -> List[NodeRevision]: + """ + Find all nodes that share a list of common dimensions + """ + nodes_that_share_dimensions = set() + first = True + for dimension in common_dimensions: + new_nodes = await get_nodes_with_dimension(session, dimension, node_types) + if first: + nodes_that_share_dimensions = set(new_nodes) + first = False + else: + nodes_that_share_dimensions = nodes_that_share_dimensions.intersection( + set(new_nodes), + ) + if not nodes_that_share_dimensions: + break + return list(nodes_that_share_dimensions) + + +def topological_sort(nodes: List[Node]) -> List[Node]: + """ + Sort a list of nodes into topological order so that the nodes with the most dependencies + are later in the list, and the nodes with the fewest dependencies are earlier. + """ + all_nodes = {node.name: node for node in nodes} + + # Build adjacency list and calculate in-degrees + adjacency_list: Dict[str, List[Node]] = {} + in_degrees: Dict[str, int] = {} + for node in nodes: + adjacency_list[node.name] = [ + parent for parent in node.current.parents if parent.name in all_nodes + ] + in_degrees[node.name] = 0 + for parents in adjacency_list.values(): + for parent in parents: + in_degrees[parent.name] += 1 + + # Initialize queue with nodes having in-degree 0 + queue: List[Node] = [ + all_nodes[name] for name, degree in in_degrees.items() if degree == 0 + ] + + # Perform topological sort using Kahn's algorithm + sorted_nodes: List[Node] = [] + while queue: + current_node = queue.pop(0) + sorted_nodes.append(current_node) + for child in adjacency_list.get(current_node.name, []): + in_degrees[child.name] -= 1 + if in_degrees[child.name] == 0: + queue.append(child) + + # Check for cycles + if len(sorted_nodes) != len(in_degrees): + raise DJGraphCycleException("Graph has at least one cycle") + + return sorted_nodes[::-1] + + +async def get_dimension_dag_indegree(session, node_names: List[str]) -> Dict[str, int]: + """ + For a given node, calculate the indegrees for its dimensions graph by finding the number + of dimension links that reference this node. Non-dimension nodes will always have an + indegree of 0. + """ + nodes = await Node.get_by_names(session, node_names) + dimension_ids = [node.id for node in nodes] + statement = ( + select( + DimensionLink.dimension_id, + func.count(DimensionLink.id), + ) + .where(DimensionLink.dimension_id.in_(dimension_ids)) + .group_by(DimensionLink.dimension_id) + ) + result = await session.execute(statement) + link_counts = {link[0]: link[1] for link in result.unique().all()} + dimension_dag_indegree = {node.name: link_counts.get(node.id, 0) for node in nodes} + return dimension_dag_indegree diff --git a/datajunction-server/datajunction_server/sql/decompose.py b/datajunction-server/datajunction_server/sql/decompose.py new file mode 100644 index 000000000..bdb3c62c2 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/decompose.py @@ -0,0 +1,166 @@ +"""Used for extracting measures form metric definitions.""" + +import hashlib +from functools import lru_cache + +from datajunction_server.models.cube_materialization import ( + Aggregability, + AggregationRule, + Measure, +) +from datajunction_server.sql import functions as dj_functions +from datajunction_server.sql.parsing.backends.antlr4 import ast, parse + + +class MeasureExtractor: + """ + Extracts aggregatable measures from a metric definition and generates SQL + derived from those measures. + """ + + def __init__(self, query_ast: ast.Query): + self.handlers = { + dj_functions.Sum: self._simple_associative_agg, + dj_functions.Count: self._simple_associative_agg, + dj_functions.Max: self._simple_associative_agg, + dj_functions.Min: self._simple_associative_agg, + dj_functions.Avg: self._avg, + } + + # Outputs from decomposition + self._measures: list[Measure] = [] + self._measures_tracker: set[str] = set() + self._query_ast = query_ast + self._extracted = False + + @classmethod + @lru_cache(maxsize=128) + def from_query_string(cls, metric_query: str): + """Create measures extractor from query string""" + query_ast = parse(metric_query) + return MeasureExtractor(query_ast=query_ast) + + @classmethod + def from_query_ast(cls, query_ast: ast.Query): # pragma: no cover + """Create measures extractor from query AST""" + return MeasureExtractor(query_ast=query_ast) # pragma: no cover + + def extract(self) -> tuple[list[Measure], ast.Query]: + """ + Decomposes the metric query into its constituent aggregatable measures and + constructs a SQL query derived from those measures. + """ + if not self._extracted: + # Normalize metric queries with aliases + parent_node_alias = self._query_ast.select.from_.relations[ # type: ignore + 0 + ].primary.alias + if parent_node_alias: + for col in self._query_ast.find_all(ast.Column): + if ( + col.namespace + and col.namespace[0].name == parent_node_alias.name + ): + col.name = ast.Name(col.name.name) + self._query_ast.select.from_.relations[0].primary.set_alias(None) # type: ignore + + for func in self._query_ast.find_all(ast.Function): + dj_function = func.function() + handler = self.handlers.get(dj_function) + if handler and dj_function.is_aggregation: + if func_measures := handler(func): # pragma: no cover + MeasureExtractor.update_ast(func, func_measures) + + for measure in sorted(func_measures, key=lambda m: m.name): + if measure.name not in self._measures_tracker: + self._measures_tracker.add(measure.name) + self._measures.append(measure) + + self._extracted = True + return self._measures, self._query_ast + + def _simple_associative_agg(self, func) -> list[Measure]: + """ + Handles measures decomposition for a single-argument associative aggregation function. + Examples: SUM, MAX, MIN, COUNT + """ + arg = func.args[0] + measure_name = "_".join( + [str(col) for col in arg.find_all(ast.Column)] + [func.name.name.lower()], + ) + expression = f"{func.quantifier} {arg}" if func.quantifier else str(arg) + short_hash = hashlib.md5(expression.encode("utf-8")).hexdigest()[:8] + + return [ + Measure( + name=f"{measure_name}_{short_hash}", + expression=expression, + aggregation=func.name.name.upper(), + rule=AggregationRule( + type=Aggregability.FULL + if func.quantifier != ast.SetQuantifier.Distinct + else Aggregability.LIMITED, + ), + ), + ] + + def _avg(self, func) -> list[Measure]: + """ + Handles measures decomposition for AVG (it requires both the SUM and COUNT + of the selected measure). + """ + arg = func.args[0] + measure_name = "_".join([str(col) for col in arg.find_all(ast.Column)]) + expression = str(arg) + short_hash = hashlib.md5(expression.encode("utf-8")).hexdigest()[:8] + return [ + Measure( + name=f"{measure_name}_{dj_functions.Sum.__name__.lower()}_{short_hash}", + expression=expression, + aggregation=dj_functions.Sum.__name__.upper(), + rule=AggregationRule( + type=Aggregability.FULL + if func.quantifier != ast.SetQuantifier.Distinct + else Aggregability.LIMITED, + ), + ), + Measure( + name=f"{measure_name}_{dj_functions.Count.__name__.lower()}_{short_hash}", + expression=expression, + aggregation=dj_functions.Count.__name__.upper(), + rule=AggregationRule( + type=Aggregability.FULL + if func.quantifier != ast.SetQuantifier.Distinct + else Aggregability.LIMITED, + ), + ), + ] + + @staticmethod + def update_ast(func, measures: list[Measure]): + """ + Updates the query AST based on the measures derived from the function. + """ + if func.function() == dj_functions.Avg: + func.parent.replace( + from_=func, + to=ast.BinaryOp( + op=ast.BinaryOpKind.Divide, + left=ast.Function( + ast.Name("SUM"), + args=[ast.Column(ast.Name(measures[0].name))], + ), + right=ast.Function( + ast.Name("SUM"), + args=[ast.Column(ast.Name(measures[1].name))], + ), + ), + ) + elif ( + func.function() == dj_functions.Count + and func.quantifier != ast.SetQuantifier.Distinct + ): + func.name.name = "SUM" + func.args = [ast.Column(ast.Name(measure.name)) for measure in measures] + else: + func.args = [ast.Column(ast.Name(measure.name)) for measure in measures] diff --git a/datajunction-server/datajunction_server/sql/functions.py b/datajunction-server/datajunction_server/sql/functions.py new file mode 100644 index 000000000..c16593347 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/functions.py @@ -0,0 +1,4624 @@ +# mypy: ignore-errors + +""" +SQL functions for type inference. + +This file holds all the functions that we want to support in the SQL used to define +nodes. The functions are used to infer types. + +Spark function reference +https://github.com/apache/spark/tree/74cddcfda3ac4779de80696cdae2ba64d53fc635/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions + +Java strictmath reference +https://docs.oracle.com/javase/8/docs/api/java/lang/StrictMath.html + +Databricks reference: +https://docs.databricks.com/sql/language-manual/sql-ref-functions-builtin-alpha.html +""" + +import inspect +import re +from itertools import zip_longest +from typing import ( + TYPE_CHECKING, + Callable, + ClassVar, + Dict, + List, + Optional, + Tuple, + Type, + Union, + cast, + get_origin, +) + +import datajunction_server.sql.parsing.types as ct +from datajunction_server.errors import ( + DJError, + DJInvalidInputException, + DJNotImplementedException, + ErrorCode, +) +from datajunction_server.models.engine import Dialect +from datajunction_server.sql.parsing.backends.exceptions import DJParseException +from datajunction_server.utils import get_settings + +if TYPE_CHECKING: + from datajunction_server.sql.parsing.ast import Expression + + +def compare_registers(types, register) -> bool: + """ + Comparing registers + """ + for (type_a, register_a), (type_b, register_b) in zip_longest( + types, + register, + fillvalue=(-1, None), + ): + if type_b == -1 and register_b is None: + if register and register[-1] and register[-1][0] == -1: # args + register_b = register[-1][1] + else: + return False # pragma: no cover + if type_a == -1: + register_a = type(register_a) + if not issubclass(register_a, register_b): # type: ignore + return False + return True + + +class DispatchMeta(type): + """ + Dispatch abstract class for function registry + """ + + def __getattribute__(cls, func_name): + if func_name in type.__getattribute__(cls, "registry").get(cls, {}): + + def dynamic_dispatch(*args: "Expression"): + return cls.dispatch(func_name, *args)(*args) + + return dynamic_dispatch + return type.__getattribute__(cls, func_name) + + +class Dispatch(metaclass=DispatchMeta): + """ + Function registry + """ + + registry: ClassVar[Dict[str, Dict[Tuple[Tuple[int, Type]], Callable]]] = {} + + @classmethod + def register(cls, func): + func_name = func.__name__ + params = inspect.signature(func).parameters + spread_types = [[]] + cls.registry[cls] = cls.registry.get(cls) or {} + cls.registry[cls][func_name] = cls.registry[cls].get(func_name) or {} + for i, (key, value) in enumerate(params.items()): + name = str(value).split(":", maxsplit=1)[0] + if name.startswith("**"): + raise ValueError( + "kwargs are not supported in dispatch.", + ) # pragma: no cover + if name.startswith("*"): + i = -1 + type_ = params[key].annotation + if type_ == inspect.Parameter.empty: + raise ValueError( # pragma: no cover + "All arguments must have a type annotation.", + ) + inner_types = [type_] + if get_origin(type_) == Union: + inner_types = type_.__args__ + for _ in inner_types: + spread_types += spread_types[:] + temp = [] + for type_ in inner_types: + for types in spread_types: + temp.append(types[:]) + temp[-1].append((i, type_)) + spread_types = temp + for types in spread_types: + cls.registry[cls][func_name][tuple(types)] = func # type: ignore + return func + + @classmethod + def dispatch(cls, func_name, *args: "Expression"): + type_registry = cls.registry[cls].get(func_name) # type: ignore + if not type_registry: + raise ValueError( + f"No function registered on {cls.__name__}`{func_name}`.", + ) # pragma: no cover + + type_list = [] + for i, arg in enumerate(args): + type_list.append( + (i, type(arg.type) if hasattr(arg, "type") else type(arg)), + ) + + types = tuple(type_list) + + if types in type_registry: # type: ignore + return type_registry[types] # type: ignore + + for register, func in type_registry.items(): # type: ignore + if compare_registers(types, register): + return func + + raise TypeError( + f"`{cls.__name__}.{func_name}` got an invalid " + "combination of types: " + f"{', '.join(str(t[1].__name__) for t in types)}", + ) + + +class Function(Dispatch): + """ + A DJ function. + """ + + is_aggregation: ClassVar[bool] = False + is_runtime: ClassVar[bool] = False + dialects: List[Dialect] = [Dialect.SPARK] + + @staticmethod + def infer_type(*args) -> ct.ColumnType: + raise NotImplementedError() + + @staticmethod + def compile_lambda(*args): + """ + Compiles the lambda function used by a given Spark function that takes + a lambda function. This allows us to evaluate the lambda's expression and + determine the result's type. + """ + + +class TableFunction(Dispatch): + """ + A DJ table-valued function. + """ + + @staticmethod + def infer_type(*args) -> List[ct.ColumnType]: + raise NotImplementedError() + + +class DjLogicalTimestamp(Function): + """ + A special function that returns the "current" timestamp as a string based on the + specified format. Used for incrementally materializing nodes, where "current" refers + to the timestamp associated with the given partition that's being processed. + """ + + is_runtime = True + + @staticmethod + def substitute(): + settings = get_settings() + return settings.dj_logical_timestamp_format + + +@DjLogicalTimestamp.register # type: ignore +def infer_type() -> ct.StringType: + """ + Defaults to returning a timestamp in the format %Y-%m-%d %H:%M:%S + """ + return ct.StringType() + + +@DjLogicalTimestamp.register # type: ignore +def infer_type(_: ct.StringType) -> ct.StringType: + """ + This function can optionally take a datetime format string like: + DJ_CURRENT_TIMESTAMP('%Y-%m-%d') + """ + return ct.StringType() + + +##################### +# Regular Functions # +##################### + + +class Abs(Function): + """ + Returns the absolute value of the numeric or interval value. + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Abs.register +def infer_type( + arg: ct.NumberType, +) -> ct.NumberType: + type_ = arg.type + return type_ + + +class Acos(Function): + """ + Returns the inverse cosine (a.k.a. arc cosine) of expr + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Acos.register +def infer_type( + arg: ct.NumberType, +) -> ct.FloatType: + return ct.FloatType() + + +class Aggregate(Function): + """ + Applies a binary operator to an initial state and all elements in the array, + and reduces this to a single state. The final state is converted into the + final result by applying a finish function. + """ + + @staticmethod + def compile_lambda(*args): + """ + Compiles the lambda function used by the `aggregate` Spark function so that + the lambda's expression can be evaluated to determine the result's type. + """ + from datajunction_server.sql.parsing import ast + + # aggregate's lambda function can take three or four arguments, depending on whether + # an optional finish function is provided + if len(args) == 4: + expr, start, merge, finish = args + else: + expr, start, merge = args + finish = None + + available_identifiers = { + identifier.name: idx for idx, identifier in enumerate(merge.identifiers) + } + merge_columns = list( + merge.expr.filter( + lambda x: isinstance(x, ast.Column) + and x.alias_or_name.name in available_identifiers, + ), + ) + finish_columns = ( + list( + finish.expr.filter( + lambda x: isinstance(x, ast.Column) + and x.alias_or_name.name in available_identifiers, + ), + ) + if finish + else [] + ) + for col in merge_columns: + if available_identifiers.get(col.alias_or_name.name) == 0: + col.add_type(start.type) + if available_identifiers.get(col.alias_or_name.name) == 1: + col.add_type(expr.type.element.type) + for col in finish_columns: + if ( + available_identifiers.get(col.alias_or_name.name) == 0 + ): # pragma: no cover + col.add_type(start.type) + + +@Aggregate.register # type: ignore +def infer_type( + expr: ct.ListType, + start: ct.ColumnType, + merge: ct.ColumnType, +) -> ct.ColumnType: + return merge.expr.type + + +class AnyValue(Function): + """ + Returns any value of the specified expression + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@AnyValue.register +def infer_type( + expr: ct.ColumnType, +) -> ct.ColumnType: + return expr.type # type: ignore + + +class ApproxCountDistinct(Function): + """ + approx_count_distinct(expr) + """ + + is_aggregation = True + dialects = [Dialect.DRUID] + + +@ApproxCountDistinct.register +def infer_type( + expr: ct.ColumnType, +) -> ct.LongType: + return ct.LongType() + + +class ApproxCountDistinctDsHll(Function): + """ + Counts distinct values of an HLL sketch column or a regular column + """ + + is_aggregation = True + dialects = [Dialect.DRUID] + + +@ApproxCountDistinctDsHll.register +def infer_type( + expr: ct.ColumnType, +) -> ct.LongType: + return ct.LongType() + + +class ApproxCountDistinctDsTheta(Function): + """ + Counts distinct values of a Theta sketch column or a regular column. + """ + + is_aggregation = True + dialects = [Dialect.DRUID] + + +@ApproxCountDistinctDsTheta.register +def infer_type( + expr: ct.ColumnType, +) -> ct.LongType: + return ct.LongType() + + +class ApproxPercentile(Function): + """ + approx_percentile(col, percentage [, accuracy]) - + Returns the approximate percentile of the numeric or ansi interval + column col which is the smallest value in the ordered col values + """ + + is_aggregation = True + + +@ApproxPercentile.register +def infer_type( + col: ct.NumberType, + percentage: ct.ListType, + accuracy: Optional[ct.NumberType], +) -> ct.DoubleType: + return ct.ListType(element_type=col.type) # type: ignore + + +@ApproxPercentile.register +def infer_type( + col: ct.NumberType, + percentage: ct.FloatType, + accuracy: Optional[ct.NumberType], +) -> ct.NumberType: + return col.type # type: ignore + + +class Array(Function): + """ + Returns an array of constants + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Array.register # type: ignore +def infer_type( + *elements: ct.ColumnType, +) -> ct.ListType: + types = {element.type for element in elements if element.type != ct.NullType()} + if len(types) > 1: + raise DJParseException( + f"Multiple types {', '.join(sorted(str(typ) for typ in types))} passed to array.", + ) + element_type = elements[0].type if elements else ct.NullType() + return ct.ListType(element_type=element_type) + + +@Array.register # type: ignore +def infer_type() -> ct.ListType: + return ct.ListType(element_type=ct.NullType()) + + +class ArrayAgg(Function): + """ + Collects and returns a list of non-unique elements. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@ArrayAgg.register # type: ignore +def infer_type( + *elements: ct.ColumnType, +) -> ct.ListType: + types = {element.type for element in elements} + if len(types) > 1: # pragma: no cover + raise DJParseException( + f"Multiple types {', '.join(sorted(str(typ) for typ in types))} passed to array.", + ) + element_type = elements[0].type if elements else ct.NullType() + return ct.ListType(element_type=element_type) + + +class ArrayAppend(Function): + """ + Add the element at the end of the array passed as first argument + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@ArrayAppend.register # type: ignore +def infer_type( + array: ct.ListType, + item: ct.ColumnType, +) -> ct.ListType: + return ct.ListType(element_type=item.type) + + +class ArrayCompact(Function): + """ + array_compact(array) - Removes null values from the array. + """ + + +@ArrayCompact.register +def infer_type( + array: ct.ListType, +) -> ct.ListType: + return array.type + + +class ArrayConcat(Function): + """ + array_concat(arr1, arr2) + Concatenates arr2 to arr1. The resulting array type is determined by the type of arr1. + """ + + dialects = [Dialect.DRUID] + + +@ArrayConcat.register +def infer_type( + arr1: ct.ListType, + arr2: ct.ListType, +) -> ct.ListType: + return arr1.type + + +class ArrayContains(Function): + """ + array_contains(array, value) - Returns true if the array contains the value. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@ArrayContains.register +def infer_type( + array: ct.ListType, + element: ct.ColumnType, +) -> ct.BooleanType: + return ct.BooleanType() + + +class ArrayDistinct(Function): + """ + array_distinct(array) - Removes duplicate values from the array. + """ + + +@ArrayDistinct.register +def infer_type( + array: ct.ListType, +) -> ct.ListType: + return array.type + + +class ArrayExcept(Function): + """ + array_except(array1, array2) - Returns an array of the elements in + array1 but not in array2, without duplicates. + """ + + +@ArrayExcept.register +def infer_type( + array1: ct.ListType, + array2: ct.ListType, +) -> ct.ListType: + return array1.type + + +class ArrayLength(Function): + """ + array_length(expr) - Returns the size of an array. The function returns null for null input. + """ + + dialects = [Dialect.DRUID] + + +@ArrayLength.register +def infer_type( + array: ct.ListType, +) -> ct.LongType: + return ct.LongType() + + +class ArrayIntersect(Function): + """ + array_intersect(array1, array2) - Returns an array of the + elements in the intersection of array1 and array2, without duplicates. + """ + + +@ArrayIntersect.register +def infer_type( + array1: ct.ListType, + array2: ct.ListType, +) -> ct.ListType: + return array1.type + + +class ArrayJoin(Function): + """ + array_join(array, delimiter[, nullReplacement]) - Concatenates + the elements of the given array using the delimiter and an + optional string to replace nulls. + """ + + +@ArrayJoin.register +def infer_type( + array: ct.ListType, + delimiter: ct.StringType, +) -> ct.StringType: + return ct.StringType() + + +@ArrayJoin.register +def infer_type( + array: ct.ListType, + delimiter: ct.StringType, + null_replacement: ct.StringType, +) -> ct.StringType: + return ct.StringType() + + +class ArrayMax(Function): + """ + array_max(array) - Returns the maximum value in the array. NaN is + greater than any non-NaN elements for double/float type. NULL + elements are skipped. + """ + + +@ArrayMax.register +def infer_type( + array: ct.ListType, +) -> ct.NumberType: + return array.type.element.type + + +class ArrayMin(Function): + """ + array_min(array) - Returns the minimum value in the array. NaN is greater than + any non-NaN elements for double/float type. NULL elements are skipped. + """ + + +@ArrayMin.register +def infer_type( + array: ct.ListType, +) -> ct.NumberType: + return array.type.element.type + + +class ArrayOffset(Function): + """ + ARRAY_OFFSET(arr, long) + Returns the array element at the 0-based index supplied + """ + + dialects = [Dialect.DRUID] + + +@ArrayOffset.register +def infer_type( + array: ct.ListType, + index: Union[ct.LongType, ct.IntegerType], +) -> ct.NumberType: + return array.type.element.type # type: ignore + + +class ArrayOrdinal(Function): + """ + ARRAY_ORDINAL(arr, long) + Returns the array element at the 1-based index supplied + """ + + dialects = [Dialect.DRUID] + + +@ArrayOrdinal.register +def infer_type( + array: ct.ListType, + index: Union[ct.LongType, ct.IntegerType], +) -> ct.NumberType: + return array.type.element.type # type: ignore + + +class ArrayPosition(Function): + """ + array_position(array, element) - Returns the (1-based) index of the first + element of the array as long. + """ + + +@ArrayPosition.register +def infer_type( + array: ct.ListType, + element: ct.ColumnType, +) -> ct.LongType: + return ct.LongType() + + +class ArrayRemove(Function): + """ + array_remove(array, element) - Remove all elements that equal to element from array. + """ + + +@ArrayRemove.register +def infer_type( + array: ct.ListType, + element: ct.ColumnType, +) -> ct.ListType: + return array.type + + +class ArrayRepeat(Function): + """ + array_repeat(element, count) - Returns the array containing element count times. + """ + + +@ArrayRepeat.register +def infer_type( + element: ct.ColumnType, + count: ct.IntegerType, +) -> ct.ListType: + return ct.ListType(element_type=element.type) + + +class ArraySize(Function): + """ + array_size(expr) - Returns the size of an array. The function returns null for null input. + """ + + +@ArraySize.register +def infer_type( + array: ct.ListType, +) -> ct.LongType: + return ct.LongType() + + +class ArraySort(Function): + """ + array_sort(expr, func) - Sorts the input array + """ + + +@ArraySort.register +def infer_type( + array: ct.ListType, +) -> ct.ListType: + return array.type + + +@ArraySort.register +def infer_type( + array: ct.ListType, + sort_func: ct.LambdaType, +) -> ct.ListType: # pragma: no cover + return array.type + + +class ArrayUnion(Function): + """ + array_union(array1, array2) - Returns an array of the elements + in the union of array1 and array2, without duplicates. + """ + + +@ArrayUnion.register +def infer_type( + array1: ct.ListType, + array2: ct.ListType, +) -> ct.ListType: + return array1.type + + +class ArraysOverlap(Function): + """ + arrays_overlap(a1, a2) - Returns true if a1 contains at least a + non-null element present also in a2. + """ + + +@ArraysOverlap.register +def infer_type( + array1: ct.ListType, + array2: ct.ListType, +) -> ct.ListType: + return ct.BooleanType() + + +class Avg(Function): + """ + Computes the average of the input column or expression. + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Avg.register +def infer_type( + arg: ct.DecimalType, +) -> ct.DecimalType: + type_ = arg.type + return ct.DecimalType(type_.precision + 4, type_.scale + 4) + + +@Avg.register +def infer_type( + arg: ct.IntervalTypeBase, +) -> ct.IntervalTypeBase: + return type(arg.type)() + + +@Avg.register # type: ignore +def infer_type( + arg: ct.NumberType, +) -> ct.DoubleType: + return ct.DoubleType() + + +@Avg.register # type: ignore +def infer_type( + arg: ct.DateTimeBase, +) -> ct.DateTimeBase: + return type(arg.type)() + + +class Cardinality(Function): + """ + Returns the size of an array or a map. + """ + + +@Cardinality.register # type: ignore +def infer_type( + args: ct.ListType, +) -> ct.IntegerType: + return ct.IntegerType() + + +@Cardinality.register # type: ignore +def infer_type( + args: ct.MapType, +) -> ct.IntegerType: + return ct.IntegerType() + + +class Cbrt(Function): + """ + cbrt(expr) - Computes the cube root of the value expr. + """ + + +@Cbrt.register # type: ignore +def infer_type( + arg: ct.NumberType, +) -> ct.ColumnType: + return ct.FloatType() + + +class Ceil(Function): + """ + Computes the smallest integer greater than or equal to the input value. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +class Ceiling(Function): + """ + ceiling(expr[, scale]) - Returns the smallest number after rounding up that is not smaller + than expr. An optional scale parameter can be specified to control the rounding behavior. + """ + + +@Ceil.register +@Ceiling.register +def infer_type( + args: ct.NumberType, + _target_scale: ct.IntegerType, +) -> ct.DecimalType: + target_scale = _target_scale.value + if isinstance(args.type, ct.DecimalType): + precision = max(args.type.precision - args.type.scale + 1, -target_scale + 1) + scale = min(args.type.scale, max(0, target_scale)) + return ct.DecimalType(precision, scale) + if args.type == ct.TinyIntType(): + precision = max(3, -target_scale + 1) + return ct.DecimalType(precision, 0) + if args.type == ct.SmallIntType(): + precision = max(5, -target_scale + 1) + return ct.DecimalType(precision, 0) + if args.type == ct.IntegerType(): + precision = max(10, -target_scale + 1) + return ct.DecimalType(precision, 0) + if args.type == ct.BigIntType(): + precision = max(20, -target_scale + 1) + return ct.DecimalType(precision, 0) + if args.type == ct.FloatType(): + precision = max(14, -target_scale + 1) + scale = min(7, max(0, target_scale)) + return ct.DecimalType(precision, scale) + if args.type == ct.DoubleType(): + precision = max(30, -target_scale + 1) + scale = min(15, max(0, target_scale)) + return ct.DecimalType(precision, scale) + + raise DJParseException( # pragma: no cover + f"Unhandled numeric type in Ceil `{args.type}`", + ) + + +@Ceil.register +@Ceiling.register +def infer_type( + args: ct.DecimalType, +) -> ct.DecimalType: + return ct.DecimalType(args.type.precision - args.type.scale + 1, 0) + + +@Ceil.register +@Ceiling.register +def infer_type( + args: ct.NumberType, +) -> ct.BigIntType: + return ct.BigIntType() + + +class Char(Function): + """ + char(expr) - Returns the ASCII character having the binary equivalent to expr. + """ + + +@Char.register # type: ignore +def infer_type( + arg: ct.IntegerType, +) -> ct.ColumnType: + return ct.StringType() + + +class CharLength(Function): + """ + char_length(expr) - Returns the length of the value expr. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@CharLength.register # type: ignore +def infer_type(arg: ct.StringType) -> ct.ColumnType: + return ct.IntegerType() + + +class CharacterLength(Function): + """ + character_length(expr) - Returns the length of the value expr. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@CharacterLength.register # type: ignore +def infer_type(arg: ct.StringType) -> ct.ColumnType: + return ct.IntegerType() + + +class Chr(Function): + """ + chr(expr) - Returns the ASCII character having the binary equivalent to expr. + """ + + +@Chr.register # type: ignore +def infer_type(arg: ct.IntegerType) -> ct.ColumnType: + return ct.StringType() + + +class Coalesce(Function): + """ + Computes the average of the input column or expression. + """ + + is_aggregation = False + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Coalesce.register # type: ignore +def infer_type( + *args: ct.ColumnType, +) -> ct.ColumnType: + if not args: # pragma: no cover + raise DJInvalidInputException( + message="Wrong number of arguments to function", + errors=[ + DJError( + code=ErrorCode.INVALID_ARGUMENTS_TO_FUNCTION, + message="You need to pass at least one argument to `COALESCE`.", + ), + ], + ) + for arg in args: + if arg.type != ct.NullType(): + return arg.type + return ct.NullType() + + +class CollectList(Function): + """ + Collects and returns a list of non-unique elements. + """ + + is_aggregation = True + + +@CollectList.register +def infer_type( + arg: ct.ColumnType, +) -> ct.ColumnType: + return ct.ListType(element_type=arg.type) + + +class CollectSet(Function): + """ + Collects and returns a list of unique elements. + """ + + is_aggregation = True + + +@CollectSet.register +def infer_type( + arg: ct.ColumnType, +) -> ct.ColumnType: + return ct.ListType(element_type=arg.type) + + +class Concat(Function): + """ + concat(col1, col2, ..., colN) - Returns the concatenation of col1, col2, ..., colN. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Concat.register # type: ignore +def infer_type( + *strings: ct.StringType, +) -> ct.StringType: + return ct.StringType() + + +@Concat.register # type: ignore +def infer_type( + *arrays: ct.ListType, +) -> ct.ListType: + return arrays[0].type + + +@Concat.register # type: ignore +def infer_type( + *maps: ct.MapType, +) -> ct.MapType: + return maps[0].type + + +class ConcatWs(Function): + """ + concat_ws(separator, [str | array(str)]+) - Returns the concatenation of the + strings separated by separator. + """ + + +@ConcatWs.register # type: ignore +def infer_type( + sep: ct.StringType, + *strings: ct.StringType, +) -> ct.ColumnType: + return ct.StringType() + + +@ConcatWs.register # type: ignore +def infer_type( + sep: ct.StringType, + *strings: ct.ListType, +) -> ct.ColumnType: + return ct.StringType() + + +class Contains(Function): + """ + contains(left, right) - Returns a boolean. The value is True if right is found inside left. + Returns NULL if either input expression is NULL. Otherwise, returns False. Both left or + right must be of STRING or BINARY type. + """ + + +@Contains.register # type: ignore +def infer_type(arg1: ct.StringType, arg2: ct.StringType) -> ct.ColumnType: + return ct.BooleanType() + + +class ContainsString(Function): + """ + contains_string(left, right) - Returns a boolean + """ + + dialects = [Dialect.DRUID] + + +@ContainsString.register # type: ignore +def infer_type(arg1: ct.StringType, arg2: ct.StringType) -> ct.ColumnType: + return ct.BooleanType() + + +@Contains.register # type: ignore +def infer_type( + arg1: ct.BinaryType, + arg2: ct.BinaryType, +) -> ct.ColumnType: # pragma: no cover + return ct.BooleanType() + + +class Conv(Function): + """ + conv(expr, from_base, to_base) - Convert the number expr from from_base to to_base. + """ + + +@Conv.register # type: ignore +def infer_type( + arg1: ct.NumberType, + arg2: ct.IntegerType, + arg3: ct.IntegerType, +) -> ct.ColumnType: + return ct.StringType() + + +@Conv.register # type: ignore +def infer_type( + arg1: ct.StringType, + arg2: ct.IntegerType, + arg3: ct.IntegerType, +) -> ct.ColumnType: + return ct.StringType() + + +class ConvertTimezone(Function): + """ + convert_timezone(from_tz, to_tz, timestamp) - Convert timestamp from from_tz to to_tz. + Spark 3.4+ + """ + + +@ConvertTimezone.register # type: ignore +def infer_type( + arg1: ct.StringType, + arg2: ct.StringType, + arg3: ct.TimestampType, +) -> ct.ColumnType: + return ct.TimestampType() + + +class Corr(Function): + """ + corr(expr1, expr2) - Compute the correlation of expr1 and expr2. + """ + + +@Corr.register # type: ignore +def infer_type( + arg1: ct.NumberType, + arg2: ct.NumberType, +) -> ct.ColumnType: + return ct.FloatType() + + +class Cos(Function): + """ + cos(expr) - Compute the cosine of expr. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Cos.register # type: ignore +def infer_type(arg: ct.NumberType) -> ct.ColumnType: + return ct.FloatType() + + +class Cosh(Function): + """ + cosh(expr) - Compute the hyperbolic cosine of expr. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Cosh.register # type: ignore +def infer_type(arg: ct.NumberType) -> ct.ColumnType: + return ct.FloatType() + + +class Cot(Function): + """ + cot(expr) - Compute the cotangent of expr. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Cot.register # type: ignore +def infer_type(arg: ct.NumberType) -> ct.ColumnType: + return ct.FloatType() + + +class Count(Function): + """ + Counts the number of non-null values in the input column or expression. + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Count.register # type: ignore +def infer_type( + *args: ct.ColumnType, +) -> ct.BigIntType: + return ct.BigIntType() + + +class CountIf(Function): + """ + count_if(expr) - Returns the number of true values in expr. + """ + + +@CountIf.register # type: ignore +def infer_type(arg: ct.BooleanType) -> ct.IntegerType: + return ct.IntegerType() # pragma: no cover + + +class CountMinSketch(Function): + """ + count_min_sketch(col, eps, confidence, seed) - Creates a Count-Min sketch of col. + """ + + +@CountMinSketch.register # type: ignore +def infer_type( + arg1: ct.ColumnType, + arg2: ct.FloatType, + arg3: ct.FloatType, + arg4: ct.IntegerType, +) -> ct.ColumnType: + return ct.BinaryType() + + +class CovarPop(Function): + """ + covar_pop(expr1, expr2) - Returns the population covariance of expr1 and expr2. + """ + + +@CovarPop.register # type: ignore +def infer_type( + arg1: ct.NumberType, + arg2: ct.NumberType, +) -> ct.ColumnType: + return ct.FloatType() + + +class CovarSamp(Function): + """ + covar_samp(expr1, expr2) - Returns the sample covariance of expr1 and expr2. + """ + + +@CovarSamp.register # type: ignore +def infer_type(arg1: ct.NumberType, arg2: ct.NumberType) -> ct.ColumnType: + return ct.FloatType() + + +class Crc32(Function): + """ + crc32(expr) - Computes a cyclic redundancy check value and returns the result as a bigint. + """ + + +@Crc32.register # type: ignore +def infer_type(arg: ct.StringType) -> ct.ColumnType: + return ct.BigIntType() + + +class Csc(Function): + """ + csc(expr) - Computes the cosecant of expr. + """ + + +@Csc.register # type: ignore +def infer_type(arg: ct.NumberType) -> ct.ColumnType: + return ct.FloatType() + + +class CumeDist(Function): + """ + cume_dist() - Computes the cumulative distribution of a value within a group of values. + """ + + +@CumeDist.register # type: ignore +def infer_type() -> ct.ColumnType: + return ct.FloatType() + + +class Curdate(Function): + """ + curdate() - Returns the current date. + """ + + +@Curdate.register # type: ignore +def infer_type() -> ct.ColumnType: + return ct.DateType() + + +class CurrentCatalog(Function): + """ + current_catalog() - Returns the current catalog. + """ + + +@CurrentCatalog.register # type: ignore +def infer_type() -> ct.ColumnType: + return ct.StringType() + + +class CurrentDatabase(Function): + """ + current_database() - Returns the current database. + """ + + +@CurrentDatabase.register # type: ignore +def infer_type() -> ct.ColumnType: + return ct.StringType() + + +class CurrentDate(Function): + """ + Returns the current date. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@CurrentDate.register # type: ignore +def infer_type() -> ct.DateType: + return ct.DateType() + + +class CurrentSchema(Function): + """ + current_schema() - Returns the current schema. + """ + + +@CurrentSchema.register # type: ignore +def infer_type() -> ct.ColumnType: + return ct.StringType() + + +class CurrentTime(Function): + """ + Returns the current time. + """ + + +@CurrentTime.register # type: ignore +def infer_type() -> ct.TimeType: + return ct.TimeType() + + +class CurrentTimestamp(Function): + """ + Returns the current timestamp. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@CurrentTimestamp.register # type: ignore +def infer_type() -> ct.TimestampType: + return ct.TimestampType() + + +class CurrentTimezone(Function): + """ + current_timezone() - Returns the current timezone. + """ + + +@CurrentTimezone.register # type: ignore +def infer_type() -> ct.ColumnType: + return ct.StringType() + + +class CurrentUser(Function): + """ + current_user() - Returns the current user. + """ + + +@CurrentUser.register # type: ignore +def infer_type() -> ct.ColumnType: + return ct.StringType() + + +class Date(Function): + """ + date(expr) - Converts expr to date. + """ + + +@Date.register # type: ignore +def infer_type(arg: Union[ct.StringType, ct.TimestampType]) -> ct.ColumnType: + return ct.DateType() + + +class DateAdd(Function): + """ + date_add(date|timestamp|str, int) - Adds a specified number of days to a date. + """ + + dialects = [Dialect.SPARK] + + +@DateAdd.register # type: ignore +def infer_type( + start_date: ct.DateType, + days: ct.IntegerBase, +) -> ct.DateType: + return ct.DateType() + + +@DateAdd.register # type: ignore +def infer_type( + start_date: ct.TimestampType, + days: ct.IntegerBase, +) -> ct.DateType: + return ct.DateType() + + +@DateAdd.register # type: ignore +def infer_type( + start_date: ct.StringType, + days: ct.IntegerBase, +) -> ct.DateType: + return ct.DateType() + + +class Datediff(Function): + """ + Computes the difference in days between two dates. + """ + + +@Datediff.register # type: ignore +def infer_type( + start_date: ct.DateType, + end_date: ct.DateType, +) -> ct.IntegerType: + return ct.IntegerType() + + +@Datediff.register # type: ignore +def infer_type( + start_date: ct.StringType, + end_date: ct.StringType, +) -> ct.IntegerType: + return ct.IntegerType() + + +@Datediff.register # type: ignore +def infer_type( + start_date: ct.TimestampType, + end_date: ct.TimestampType, +) -> ct.IntegerType: + return ct.IntegerType() + + +@Datediff.register # type: ignore +def infer_type( + start_date: ct.IntegerType, + end_date: ct.IntegerType, +) -> ct.IntegerType: + return ct.IntegerType() # pragma: no cover + + +class DateFromUnixDate(Function): + """ + date_from_unix_date(expr) - Converts the number of days from epoch (1970-01-01) to a date. + """ + + +@DateFromUnixDate.register # type: ignore +def infer_type(arg: ct.IntegerType) -> ct.ColumnType: + return ct.DateType() + + +class DateFormat(Function): + """ + date_format(timestamp, fmt) - Converts timestamp to a value of string + in the format specified by the date format fmt. + """ + + +@DateFormat.register # type: ignore +def infer_type( + timestamp: ct.TimestampType, + fmt: ct.StringType, +) -> ct.StringType: + return ct.StringType() + + +class DatePart(Function): + """ + date_part(field, source) - Extracts a part of the date or time given. + """ + + +@DatePart.register # type: ignore +def infer_type( + arg1: ct.StringType, + arg2: Union[ct.DateType, ct.TimestampType], +) -> ct.ColumnType: + # The output can be integer, float, or string depending on the part extracted. + # Here we assume the output is an integer for simplicity. Adjust as needed. + return ct.IntegerType() + + +class DateSub(Function): + """ + Subtracts a specified number of days from a date. + """ + + +@DateSub.register # type: ignore +def infer_type( + start_date: ct.DateType, + days: ct.IntegerBase, +) -> ct.DateType: + return ct.DateType() + + +@DateSub.register # type: ignore +def infer_type( + start_date: ct.StringType, + days: ct.IntegerBase, +) -> ct.DateType: + return ct.DateType() + + +class Day(Function): + """ + Returns the day of the month for a specified date. + """ + + +@Day.register # type: ignore +def infer_type( + arg: Union[ct.StringType, ct.DateType, ct.TimestampType], +) -> ct.IntegerType: # type: ignore + return ct.IntegerType() + + +class Dayofmonth(Function): + """ + dayofmonth(date) - Extracts the day of the month of a given date. + """ + + +@Dayofmonth.register # type: ignore +def infer_type( + arg: Union[ct.DateType, ct.StringType], +) -> ct.ColumnType: + return ct.IntegerType() + + +class Dayofweek(Function): + """ + dayofweek(date) - Extracts the day of the week of a given date. + """ + + +@Dayofweek.register # type: ignore +def infer_type( + arg: Union[ct.DateType, ct.StringType], +) -> ct.ColumnType: + return ct.IntegerType() + + +class Dayofyear(Function): + """ + dayofyear(date) - Extracts the day of the year of a given date. + """ + + +@Dayofyear.register # type: ignore +def infer_type( + arg: Union[ct.DateType, ct.StringType], +) -> ct.ColumnType: + return ct.IntegerType() + + +class Decimal(Function): + """ + decimal(expr, precision, scale) - Converts expr to a decimal number. + """ + + +@Decimal.register # type: ignore +def infer_type( + arg1: Union[ct.IntegerType, ct.FloatType, ct.StringType], +) -> ct.ColumnType: + return ct.DecimalType(8, 6) + + +class Decode(Function): + """ + decode(bin, charset) - Decodes the first argument using the second argument + character set. + + TODO: decode(expr, search, result [, search, result ] ... [, default]) - Compares + expr to each search value in order. If expr is equal to a search value, decode + returns the corresponding result. If no match is found, then it returns default. + If default is omitted, it returns null. + """ + + +@Decode.register # type: ignore +def infer_type(arg1: ct.BinaryType, arg2: ct.StringType) -> ct.ColumnType: + return ct.StringType() + + +class Degrees(Function): + """ + degrees(expr) - Converts radians to degrees. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Degrees.register # type: ignore +def infer_type(arg: ct.NumberType) -> ct.ColumnType: + return ct.FloatType() + + +class DenseRank(Function): + """ + dense_rank() - Computes the dense rank of a value in a group of values. + """ + + +@DenseRank.register +def infer_type() -> ct.IntegerType: + return ct.IntegerType() + + +@DenseRank.register +def infer_type(_: ct.ColumnType) -> ct.IntegerType: + return ct.IntegerType() + + +class Div(Function): + """ + expr1 div expr2 - Divide expr1 by expr2. It returns NULL if an operand is NULL or + expr2 is 0. The result is casted to long. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Div.register +def infer_type(expr1: ct.NumberType, expr2: ct.NumberType) -> ct.LongType: + return ct.LongType() + + +class Double(Function): + """ + double(expr) - Converts expr to a double precision floating-point number. + """ + + +@Double.register # type: ignore +def infer_type( + arg: Union[ct.IntegerType, ct.FloatType, ct.StringType], +) -> ct.ColumnType: + return ct.DoubleType() + + +class E(Function): + """ + e() - Returns the mathematical constant e. + """ + + +@E.register # type: ignore +def infer_type() -> ct.ColumnType: + return ct.FloatType() + + +class ElementAt(Function): + """ + element_at(array, index) - Returns element of array at given (1-based) index + element_at(map, key) - Returns value for given key. + """ + + +@ElementAt.register +def infer_type( + array: ct.ListType, + _: ct.NumberType, +) -> ct.ColumnType: + return array.type.element.type + + +@ElementAt.register +def infer_type( + map_arg: ct.MapType, + _: ct.NumberType, +) -> ct.ColumnType: + return map_arg.type.value.type + + +class Elt(Function): + """ + elt(n, input1, input2, ...) - Returns the n-th input, e.g., returns input2 when n is 2. + """ + + +@Elt.register # type: ignore +def infer_type(arg1: ct.IntegerType, *args: ct.ColumnType) -> ct.ColumnType: + return args[0].type + + +class Encode(Function): + """ + encode(str, charset) - Encodes str into the provided charset. + """ + + +@Encode.register # type: ignore +def infer_type(arg1: ct.StringType, arg2: ct.StringType) -> ct.ColumnType: + return ct.StringType() + + +class Endswith(Function): + """ + endswith(str, substr) - Returns true if str ends with substr. + """ + + +@Endswith.register # type: ignore +def infer_type(arg1: ct.StringType, arg2: ct.StringType) -> ct.ColumnType: + return ct.BooleanType() + + +class EqualNull(Function): + """ + equal_null(expr1, expr2) - Returns true if expr1 and expr2 are equal or both are null. + """ + + +@EqualNull.register # type: ignore +def infer_type(arg1: ct.ColumnType, arg2: ct.ColumnType) -> ct.ColumnType: + return ct.BooleanType() + + +class Every(Function): + """ + every(expr) - Returns true if all values are true. + """ + + is_aggregation = True + + +@Every.register # type: ignore +def infer_type(arg: ct.BooleanType) -> ct.ColumnType: + return ct.BooleanType() + + +class Exists(Function): + """ + exists(expr, pred) - Tests whether a predicate holds for one or more + elements in the array. + """ + + @staticmethod + def compile_lambda(*args): + """ + Compiles the lambda function used by the `filter` Spark function so that + the lambda's expression can be evaluated to determine the result's type. + """ + from datajunction_server.sql.parsing import ast + + expr, func = args + if len(func.identifiers) != 1: + raise DJParseException( # pragma: no cover + message="The function `exists` takes a lambda function that takes at " + "most one argument.", + ) + lambda_arg_col = [ + col + for col in func.expr.find_all(ast.Column) + if col.alias_or_name.name == func.identifiers[0].name + ][0] + lambda_arg_col.add_type(expr.type.element.type) + + +@Exists.register # type: ignore +def infer_type( + expr: ct.ListType, + pred: ct.BooleanType, +) -> ct.ColumnType: + return ct.BooleanType() + + +class Exp(Function): + """ + Returns e to the power of expr. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Exp.register # type: ignore +def infer_type( + args: ct.ColumnType, +) -> ct.DoubleType: + return ct.DoubleType() + + +class Explode(Function): + """ + explode(expr) - Returns a new row for each element in the given array or map. + """ + + +@Explode.register # type: ignore +def infer_type(arg: ct.ListType) -> ct.ColumnType: + return arg.type.element.type # pragma: no cover + + +@Explode.register # type: ignore +def infer_type(arg: ct.MapType) -> ct.ColumnType: + return arg.type.value.type # pragma: no cover + + +class ExplodeOuter(Function): + """ + explode_outer(expr) - Similar to explode, but returns null if the array/map is null or empty. + """ + + +@ExplodeOuter.register # type: ignore +def infer_type(arg: ct.ListType) -> ct.ColumnType: + return arg.type.element.type + + +@ExplodeOuter.register # type: ignore +def infer_type(arg: ct.MapType) -> ct.ColumnType: + return arg.type.value.type + + +class Expm1(Function): + """ + expm1(expr) - Calculates e^x - 1. + """ + + +@Expm1.register # type: ignore +def infer_type(arg: ct.NumberType) -> ct.ColumnType: + return ct.FloatType() + + +class Extract(Function): + """ + Returns a specified component of a timestamp, such as year, month or day. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + @staticmethod + def infer_type( # type: ignore + field: "Expression", + source: "Expression", + ) -> Union[ct.DecimalType, ct.IntegerType]: + if str(field.name) == "SECOND": # type: ignore + return ct.DecimalType(8, 6) + return ct.IntegerType() + + +class Factorial(Function): + """ + factorial(expr) - Returns the factorial of the number. + """ + + +@Factorial.register # type: ignore +def infer_type(arg: ct.IntegerType) -> ct.ColumnType: + return ct.IntegerType() + + +class Filter(Function): + """ + Filter an array. + """ + + @staticmethod + def compile_lambda(*args): + """ + Compiles the lambda function used by the `filter` Spark function so that + the lambda's expression can be evaluated to determine the result's type. + """ + from datajunction_server.sql.parsing import ast + + expr, func = args + if len(func.identifiers) > 2: + raise DJParseException( + message="The function `filter` takes a lambda function that takes at " + "most two arguments.", + ) + for col in func.expr.find_all(ast.Column): + if ( # pragma: no cover + col.alias_or_name.namespace + and col.alias_or_name.namespace.name + and func.identifiers[0].name == col.alias_or_name.namespace.name + ) or func.identifiers[0].name == col.alias_or_name.name: + col.add_type(expr.type.element.type) + if ( + len(func.identifiers) == 2 + and col.alias_or_name.name == func.identifiers[1].name + ): + col.add_type(ct.LongType()) + + +@Filter.register # type: ignore +def infer_type( + arg: Union[ct.ListType, ct.PrimitiveType], + func: ct.PrimitiveType, +) -> ct.ListType: + return arg.type # type: ignore + + +class FindInSet(Function): + """ + find_in_set(str, str_list) - Returns the index of the first occurrence of str in str_list. + """ + + +@FindInSet.register # type: ignore +def infer_type(arg1: ct.StringType, arg2: ct.StringType) -> ct.ColumnType: + return ct.IntegerType() + + +class First(Function): + """ + Returns the first value of expr for a group of rows. If isIgnoreNull is + true, returns only non-null values. + """ + + is_aggregation = True + + +@First.register +def infer_type( + arg: ct.ColumnType, +) -> ct.ColumnType: + return arg.type + + +@First.register +def infer_type( + arg: ct.ColumnType, + is_ignore_null: ct.BooleanType, +) -> ct.ColumnType: + return arg.type + + +class FirstValue(Function): + """ + Returns the first value of expr for a group of rows. If isIgnoreNull is + true, returns only non-null values. + """ + + is_aggregation = True + + +@FirstValue.register +def infer_type( + arg: ct.ColumnType, +) -> ct.ColumnType: + return arg.type + + +@FirstValue.register +def infer_type( + arg: ct.ColumnType, + is_ignore_null: ct.BooleanType, +) -> ct.ColumnType: + return arg.type + + +class Flatten(Function): + """ + Flatten an array. + """ + + +@Flatten.register # type: ignore +def infer_type( + array: ct.ListType, +) -> ct.ListType: + return array.type.element.type # type: ignore + + +class Float(Function): + """ + float(expr) - Casts the value expr to the target data type float. + """ + + +@Float.register # type: ignore +def infer_type(arg: Union[ct.NumberType, ct.StringType]) -> ct.FloatType: + return ct.FloatType() + + +class Floor(Function): + """ + Returns the largest integer less than or equal to a specified number. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Floor.register # type: ignore +def infer_type( + args: ct.DecimalType, +) -> ct.DecimalType: + return ct.DecimalType(args.type.precision - args.type.scale + 1, 0) + + +@Floor.register # type: ignore +def infer_type( + args: ct.NumberType, +) -> ct.BigIntType: + return ct.BigIntType() + + +@Floor.register # type: ignore +def infer_type( + args: ct.NumberType, + _target_scale: ct.IntegerType, +) -> ct.DecimalType: + target_scale = _target_scale.value + if isinstance(args.type, ct.DecimalType): + precision = max(args.type.precision - args.type.scale + 1, -target_scale + 1) + scale = min(args.type.scale, max(0, target_scale)) + return ct.DecimalType(precision, scale) + if args.type == ct.TinyIntType(): + precision = max(3, -target_scale + 1) + return ct.DecimalType(precision, 0) + if args.type == ct.SmallIntType(): + precision = max(5, -target_scale + 1) + return ct.DecimalType(precision, 0) + if args.type == ct.IntegerType(): + precision = max(10, -target_scale + 1) + return ct.DecimalType(precision, 0) + if args.type == ct.BigIntType(): + precision = max(20, -target_scale + 1) + return ct.DecimalType(precision, 0) + if args.type == ct.FloatType(): + precision = max(14, -target_scale + 1) + scale = min(7, max(0, target_scale)) + return ct.DecimalType(precision, scale) + if args.type == ct.DoubleType(): + precision = max(30, -target_scale + 1) + scale = min(15, max(0, target_scale)) + return ct.DecimalType(precision, scale) + + raise DJParseException( + f"Unhandled numeric type in Floor `{args.type}`", + ) # pragma: no cover + + +class Forall(Function): + """ + forall(expr, predicate) - Returns true if a given predicate holds for all elements of an array. + """ + + @staticmethod + def compile_lambda(*args): + """ + Compiles the lambda function used by the `filter` Spark function so that + the lambda's expression can be evaluated to determine the result's type. + """ + from datajunction_server.sql.parsing import ast + + expr, func = args + if len(func.identifiers) != 1: + raise DJParseException( # pragma: no cover + message="The function `forall` takes a lambda function that takes at " + "most one argument.", + ) + lambda_arg_col = [ + col + for col in func.expr.find_all(ast.Column) + if col.alias_or_name.name == func.identifiers[0].name + ][0] + lambda_arg_col.add_type(expr.type.element.type) + + +@Forall.register # type: ignore +def infer_type(arg1: ct.ListType, arg2: ct.BooleanType) -> ct.ColumnType: + return ct.BooleanType() + + +class FormatNumber(Function): + """ + format_number(x, d) - Formats the number x to a format like '#,###,###.##', + rounded to d decimal places. + """ + + +@FormatNumber.register # type: ignore +def infer_type(arg1: ct.FloatType, arg2: ct.IntegerType) -> ct.StringType: + return ct.StringType() + + +@FormatNumber.register # type: ignore +def infer_type(arg1: ct.FloatType, arg2: ct.StringType) -> ct.StringType: + return ct.StringType() + + +class FormatString(Function): + """ + format_string(format, ...) - Formats the arguments in printf-style. + """ + + +@FormatString.register # type: ignore +def infer_type(arg1: ct.StringType, *args: ct.PrimitiveType) -> ct.StringType: + return ct.StringType() + + +class FromCsv(Function): + """ + from_csv(csvStr, schema, options) - Parses a CSV string and returns a struct. + """ + + +@FromCsv.register # type: ignore +def infer_type( + arg1: ct.StringType, + schema: ct.StringType, + arg3: Optional[ct.MapType] = None, +) -> ct.ColumnType: + # TODO: Handle options? + from datajunction_server.sql.parsing.backends.antlr4 import ( + parse_rule, + ) # pragma: no cover + + return ct.StructType( + *parse_rule(schema.value, "complexColTypeList") + ) # pragma: no cover + + +class FromJson(Function): # pragma: no cover + """ + Converts a JSON string to a struct or map. + """ + + +@FromJson.register # type: ignore +def infer_type( + json: ct.StringType, + schema: ct.StringType, + options: Optional[Function] = None, +) -> ct.StructType: + from datajunction_server.sql.parsing.backends.antlr4 import parse_rule + + schema_type = re.sub(r"^'(.*)'$", r"\1", schema.value) + try: + return parse_rule(schema_type, "dataType") + except DJParseException: + return ct.StructType(*parse_rule(schema_type, "complexColTypeList")) + + +class FromUnixtime(Function): + """ + from_unixtime(unix_time, format) - Converts the number of seconds from the Unix + epoch to a string representing the timestamp. + """ + + +@FromUnixtime.register # type: ignore +def infer_type(arg1: ct.IntegerType, arg2: ct.StringType) -> ct.ColumnType: + return ct.StringType() + + +class FromUtcTimestamp(Function): + """ + from_utc_timestamp(timestamp, timezone) - Renders that time as a timestamp + in the given time zone. + """ + + +@FromUtcTimestamp.register # type: ignore +def infer_type(arg1: ct.TimestampType, arg2: ct.StringType) -> ct.ColumnType: + return ct.TimestampType() + + +@FromUtcTimestamp.register # type: ignore +def infer_type(arg1: ct.StringType, arg2: ct.StringType) -> ct.ColumnType: + return ct.TimestampType() + + +class Get(Function): + """ + get(expr, index) - Retrieves an element from an array at the specified + index or retrieves a value from a map for the given key. + """ + + +@Get.register # type: ignore +def infer_type(arg1: ct.ListType, arg2: ct.IntegerType) -> ct.ColumnType: + return arg1.type.element.type + + +class GetJsonObject(Function): + """ + get_json_object(jsonString, path) - Extracts a JSON object from a JSON + string based on the JSON path specified. + """ + + +@GetJsonObject.register # type: ignore +def infer_type(arg1: ct.StringType, arg2: ct.StringType) -> ct.ColumnType: + return ct.StringType() + + +class GetBit(Function): + """ + getbit(expr, pos) - Returns the value of the bit (0 or 1) at the specified position. + """ + + +@GetBit.register # type: ignore +def infer_type(arg1: ct.IntegerType, arg2: ct.IntegerType) -> ct.ColumnType: + return ct.IntegerType() + + +class Greatest(Function): + """ + greatest(expr, ...) - Returns the greatest value of all parameters, skipping null values. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Greatest.register # type: ignore +def infer_type( + *values: ct.NumberType, +) -> ct.ColumnType: + return values[0].type + + +class Grouping(Function): + """ + grouping(col) - Returns 1 if the specified column is aggregated, and 0 otherwise. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Grouping.register # type: ignore +def infer_type(arg: ct.ColumnType) -> ct.ColumnType: + return ct.IntegerType() + + +class GroupingId(Function): + """ + grouping_id(cols) - Returns a bit vector with a bit for each grouping column. + """ + + +@GroupingId.register # type: ignore +def infer_type(*args: ct.ColumnType) -> ct.ColumnType: + return ct.BigIntType() + + +class Hash(Function): + """ + hash(args) - Returns a hash value of the arguments. + """ + + +@Hash.register # type: ignore +def infer_type(*args: ct.ColumnType) -> ct.ColumnType: + return ct.IntegerType() + + +class Hex(Function): + """ + hex(expr) - Converts a number or a string to a hexadecimal string. + """ + + +@Hex.register # type: ignore +def infer_type(arg: Union[ct.IntegerType, ct.StringType]) -> ct.ColumnType: + return ct.StringType() + + +class HistogramNumeric(Function): + """ + histogram_numeric(col, numBins) - Generates a histogram using a series of buckets + defined by equally spaced width intervals. + """ + + +@HistogramNumeric.register # type: ignore +def infer_type(arg1: ct.ColumnType, arg2: ct.IntegerType) -> ct.ColumnType: + # assuming that there's a StructType for the bin and frequency + from datajunction_server.sql.parsing import ast + + return ct.ListType( + element_type=ct.StructType( + ct.NestedField(ast.Name("x"), ct.FloatType()), + ct.NestedField(ast.Name("y"), ct.FloatType()), + ), + ) + + +class Hour(Function): + """ + hour(timestamp) - Extracts the hour from a timestamp. + """ + + +@Hour.register # type: ignore +def infer_type( + arg: Union[ct.TimestampType, ct.StringType], +) -> ct.ColumnType: + return ct.IntegerType() + + +class Hypot(Function): + """ + hypot(a, b) - Returns sqrt(a^2 + b^2) without intermediate overflow or underflow. + """ + + +@Hypot.register # type: ignore +def infer_type(arg1: ct.NumberType, arg2: ct.NumberType) -> ct.ColumnType: + return ct.FloatType() + + +class If(Function): + """ + If statement + + if(condition, result, else_result): if condition evaluates to true, + then returns result; otherwise returns else_result. + """ + + +@If.register # type: ignore +def infer_type( + cond: ct.BooleanType, + then: ct.ColumnType, + else_: ct.ColumnType, +) -> ct.ColumnType: + if not then.type.is_compatible(else_.type): + raise DJInvalidInputException( + message="The then result and else result must match in type! " + f"Got {then.type} and {else_.type}", + ) + if then.type == ct.NullType(): + return else_.type + return then.type + + +class IfNull(Function): + """ + Returns the second expression if the first is null, else returns the first expression. + """ + + +@IfNull.register +def infer_type(*args: ct.ColumnType) -> ct.ColumnType: + return args[0].type if args[1].type == ct.NullType() else args[1].type + + +class ILike(Function): + """ + ilike(str, pattern) - Performs case-insensitive LIKE match. + """ + + +@ILike.register # type: ignore +def infer_type(arg1: ct.StringType, arg2: ct.StringType) -> ct.ColumnType: + return ct.BooleanType() + + +class InitCap(Function): + """ + initcap(str) - Converts the first letter of each word in the string to uppercase + and the rest to lowercase. + """ + + +@InitCap.register # type: ignore +def infer_type(arg: ct.StringType) -> ct.ColumnType: + return ct.StringType() + + +class Inline(Function): + """ + inline(array_of_struct) - Explodes an array of structs into a table. + """ + + +@Inline.register # type: ignore +def infer_type(arg: ct.ListType) -> ct.ColumnType: + # The output type is the type of the struct's fields + return arg.type.element.type + + +class InlineOuter(Function): + """ + inline_outer(array_of_struct) - Similar to inline, but includes nulls if the size + of the array is less than the size of the outer array. + """ + + +@InlineOuter.register # type: ignore +def infer_type(arg: ct.ListType) -> ct.ColumnType: + # The output type is the type of the struct's fields + return arg.type.element.type + + +class InputFileBlockLength(Function): + """ + input_file_block_length() - Returns the length of the current block being read from HDFS. + """ + + +@InputFileBlockLength.register # type: ignore +def infer_type() -> ct.ColumnType: + return ct.LongType() + + +class InputFileBlockStart(Function): + """ + input_file_block_start() - Returns the start offset of the current block being read from HDFS. + """ + + +@InputFileBlockStart.register # type: ignore +def infer_type() -> ct.ColumnType: + return ct.LongType() + + +class InputFileName(Function): + """ + input_file_name() - Returns the name of the current file being read from HDFS. + """ + + +@InputFileName.register # type: ignore +def infer_type() -> ct.ColumnType: + return ct.StringType() + + +class Instr(Function): + """ + instr(str, substring) - Returns the position of the first occurrence of substring in string. + """ + + +@Instr.register # type: ignore +def infer_type(arg1: ct.StringType, arg2: ct.StringType) -> ct.ColumnType: + return ct.IntegerType() + + +class Int(Function): + """ + int(expr) - Casts the value expr to the target data type int. + """ + + +@Int.register # type: ignore +def infer_type( + arg: ct.ColumnType, +) -> ct.ColumnType: + return ct.IntegerType() + + +class Isnan(Function): + """ + isnan(expr) - Tests if a value is NaN. + """ + + +@Isnan.register # type: ignore +def infer_type(arg: ct.NumberType) -> ct.ColumnType: + return ct.BooleanType() + + +class Isnotnull(Function): + """ + isnotnull(expr) - Tests if a value is not null. + """ + + +@Isnotnull.register # type: ignore +def infer_type(arg: ct.ColumnType) -> ct.ColumnType: + return ct.BooleanType() + + +class Isnull(Function): + """ + isnull(expr) - Tests if a value is null. + """ + + +@Isnull.register # type: ignore +def infer_type(arg: ct.ColumnType) -> ct.ColumnType: + return ct.BooleanType() + + +class JsonArrayLength(Function): + """ + json_array_length(jsonArray) - Returns the length of the JSON array. + """ + + +@JsonArrayLength.register # type: ignore +def infer_type(arg: ct.StringType) -> ct.ColumnType: + return ct.IntegerType() + + +class JsonObjectKeys(Function): + """ + json_object_keys(jsonObject) - Returns all the keys of the JSON object. + """ + + +@JsonObjectKeys.register # type: ignore +def infer_type(arg: ct.StringType) -> ct.ColumnType: + return ct.ListType(element_type=ct.StringType()) + + +class JsonTuple(Function): + """ + json_tuple(json_str, path1, path2, ...) - Extracts multiple values from a JSON object. + """ + + +@JsonTuple.register # type: ignore +def infer_type(json_str: ct.StringType, *paths: ct.StringType) -> ct.ColumnType: + # assuming that there's a TupleType for the extracted values + return ct.ListType(element_type=ct.StringType()) + + +class Kurtosis(Function): + """ + kurtosis(expr) - Returns the kurtosis of the values in a group. + """ + + +@Kurtosis.register # type: ignore +def infer_type(arg: ct.NumberType) -> ct.DoubleType: + return ct.DoubleType() + + +class Lag(Function): + """ + lag(expr[, offset[, default]]) - Returns the value that is `offset` rows + before the current row in a window partition. + """ + + +@Lag.register # type: ignore +def infer_type( + arg: ct.ColumnType, + offset: Optional[ct.IntegerType] = None, + default: Optional[ct.ColumnType] = None, +) -> ct.ColumnType: + # The output type is the same as the input expression's type + return arg.type + + +class Last(Function): + """ + last(expr[, ignoreNulls]) - Returns the last value of `expr` for a group of rows. + """ + + is_aggregation = True + + +@Last.register # type: ignore +def infer_type( + arg: ct.ColumnType, + ignore_nulls: Optional[ct.BooleanType] = None, +) -> ct.ColumnType: + # The output type is the same as the input expression's type + return arg.type + + +class LastDay(Function): + """ + last_day(date) - Returns the last day of the month which the date belongs to. + """ + + +@LastDay.register # type: ignore +def infer_type(arg: Union[ct.DateType, ct.StringType]) -> ct.ColumnType: + return ct.DateType() + + +class LastValue(Function): + """ + last_value(expr[, ignoreNulls]) - Returns the last value in an ordered set of values. + """ + + is_aggregation = True + + +@LastValue.register # type: ignore +def infer_type( + arg: ct.ColumnType, + ignore_nulls: Optional[ct.BooleanType] = None, +) -> ct.ColumnType: + # The output type is the same as the input expression's type + return arg.type + + +class Lcase(Function): + """ + lcase(str) - Converts the string to lowercase. + """ + + +@Lcase.register # type: ignore +def infer_type(arg: ct.StringType) -> ct.ColumnType: + return ct.StringType() + + +class Lead(Function): + """ + lead(expr[, offset[, default]]) - Returns the value that is `offset` + rows after the current row in a window partition. + """ + + +@Lead.register # type: ignore +def infer_type( + arg: ct.ColumnType, + offset: Optional[ct.IntegerType] = None, + default: Optional[ct.ColumnType] = None, +) -> ct.ColumnType: + # The output type is the same as the input expression's type + return arg.type + + +class Least(Function): + """ + least(expr1, expr2, ...) - Returns the smallest value of the list of values. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Least.register # type: ignore +def infer_type(*args: ct.ColumnType) -> ct.ColumnType: + # The output type is the same as the input expressions' type + # Assuming all input expressions have the same type + return args[0].type + + +class Left(Function): + """ + left(str, len) - Returns the leftmost `len` characters from the string. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Left.register # type: ignore +def infer_type( + arg1: ct.StringType, + arg2: ct.IntegerType, +) -> ct.StringType: + return ct.StringType() # pragma: no cover # see test_left_func + + +class Len(Function): + """ + len(str) - Returns the length of the string. + """ + + +@Len.register # type: ignore +def infer_type(arg: ct.StringType) -> ct.IntegerType: + return ct.IntegerType() + + +class Length(Function): + """ + Returns the length of a string. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Length.register # type: ignore +def infer_type( + arg: ct.StringType, +) -> ct.IntegerType: + return ct.IntegerType() + + +class Levenshtein(Function): + """ + Returns the Levenshtein distance between two strings. + """ + + +@Levenshtein.register # type: ignore +def infer_type( + string1: ct.StringType, + string2: ct.StringType, +) -> ct.IntegerType: + return ct.IntegerType() + + +class Like(Function): + """ + like(str, pattern) - Performs pattern matching using SQL's LIKE operator. + """ + + +@Like.register # type: ignore +def infer_type(arg1: ct.StringType, arg2: ct.StringType) -> ct.ColumnType: + return ct.BooleanType() + + +class Ln(Function): + """ + Returns the natural logarithm of a number. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Ln.register # type: ignore +def infer_type( + args: ct.ColumnType, +) -> ct.DoubleType: + return ct.DoubleType() + + +class Localtimestamp(Function): + """ + localtimestamp() - Returns the current timestamp at the system's local time zone. + """ + + +@Localtimestamp.register # type: ignore +def infer_type() -> ct.ColumnType: + return ct.TimestampType() + + +class Locate(Function): + """ + locate(substr, str[, pos]) - Returns the position of the first occurrence of substr in str. + """ + + +@Locate.register # type: ignore +def infer_type( + arg1: ct.StringType, + arg2: ct.StringType, + pos: Optional[ct.IntegerType] = None, +) -> ct.ColumnType: + return ct.IntegerType() + + +class Log(Function): + """ + Returns the logarithm of a number with the specified base. + """ + + +@Log.register # type: ignore +def infer_type( + base: ct.ColumnType, + expr: ct.ColumnType, +) -> ct.DoubleType: + return ct.DoubleType() + + +class Log10(Function): + """ + Returns the base-10 logarithm of a number. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Log10.register # type: ignore +def infer_type( + args: ct.ColumnType, +) -> ct.DoubleType: + return ct.DoubleType() + + +class Log1p(Function): + """ + log1p(expr) - Returns the natural logarithm of the given value plus one. + """ + + +@Log1p.register # type: ignore +def infer_type(arg: ct.NumberType) -> ct.DoubleType: + return ct.DoubleType() + + +class Log2(Function): + """ + Returns the base-2 logarithm of a number. + """ + + +@Log2.register # type: ignore +def infer_type( + args: ct.ColumnType, +) -> ct.DoubleType: + return ct.DoubleType() + + +class Lower(Function): + """ + Converts a string to lowercase. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + @staticmethod + def infer_type(arg: "Expression") -> ct.StringType: # type: ignore + return ct.StringType() + + +class Lpad(Function): + """ + lpad(str, len[, pad]) - Left-pads the string with pad to a length of len. + If str is longer than len, the return value is shortened to len characters. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Lpad.register # type: ignore +def infer_type( + arg1: ct.StringType, + arg2: ct.IntegerType, + pad: Optional[ct.StringType] = None, +) -> ct.StringType: + return ct.StringType() + + +class Ltrim(Function): + """ + ltrim(str[, trimStr]) - Trims the spaces from left end of the string. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Ltrim.register # type: ignore +def infer_type( + arg1: ct.StringType, + trim_str: Optional[ct.StringType] = None, +) -> ct.StringType: + return ct.StringType() + + +class MakeDate(Function): + """ + make_date(year, month, day) - Creates a date from the given year, month, and day. + """ + + +@MakeDate.register # type: ignore +def infer_type( + year: ct.IntegerType, + month: ct.IntegerType, + day: ct.IntegerType, +) -> ct.ColumnType: + return ct.DateType() + + +class MakeDtInterval(Function): + """ + make_dt_interval(days, hours, mins, secs) - Returns a day-time interval. + """ + + +@MakeDtInterval.register # type: ignore +def infer_type( + days: ct.IntegerType, + hours: ct.IntegerType, + mins: ct.IntegerType, + secs: ct.IntegerType, +) -> ct.DayTimeIntervalType: + return ct.DayTimeIntervalType() + + +class MakeInterval(Function): + """ + make_interval(years, months) - Returns a year-month interval. + """ + + +@MakeInterval.register # type: ignore +def infer_type( + years: ct.IntegerType, + months: ct.IntegerType, +) -> ct.YearMonthIntervalType: + return ct.YearMonthIntervalType() + + +class MakeTimestamp(Function): + """ + make_timestamp(year, month, day, hour, min, sec) - Returns a timestamp + made from the arguments. + """ + + +@MakeTimestamp.register # type: ignore +def infer_type( + year: ct.IntegerType, + month: ct.IntegerType, + day: ct.IntegerType, + hour: ct.IntegerType, + min_: ct.IntegerType, + sec: ct.IntegerType, +) -> ct.TimestampType: + return ct.TimestampType() + + +class MakeTimestampLtz(Function): + """ + make_timestamp_ltz(year, month, day, hour, min, sec, timezone) + Returns a timestamp with local time zone. + """ + + +@MakeTimestampLtz.register # type: ignore +def infer_type( + year: ct.IntegerType, + month: ct.IntegerType, + day: ct.IntegerType, + hour: ct.IntegerType, + min_: ct.IntegerType, + sec: ct.IntegerType, + timezone: Optional[ct.StringType] = None, +) -> ct.TimestampType: + return ct.TimestampType() + + +class MakeTimestampNtz(Function): + """ + make_timestamp_ntz(year, month, day, hour, min, sec) + Returns a timestamp without time zone. + """ + + +@MakeTimestampNtz.register # type: ignore +def infer_type( + year: ct.IntegerType, + month: ct.IntegerType, + day: ct.IntegerType, + hour: ct.IntegerType, + min_: ct.IntegerType, + sec: ct.IntegerType, +) -> ct.TimestampType: + return ct.TimestampType() + + +class MakeYmInterval(Function): + """ + make_ym_interval(years, months) - Returns a year-month interval. + """ + + +@MakeYmInterval.register # type: ignore +def infer_type( + years: ct.IntegerType, + months: ct.IntegerType, +) -> ct.YearMonthIntervalType: + return ct.YearMonthIntervalType() + + +class Map(Function): + """ + Returns a map of constants + """ + + +@Map.register # type: ignore +def infer_type( + *args: ct.ColumnType, +) -> ct.MapType: + return ct.MapType(key_type=args[0].type, value_type=args[1].type) + + +class MapConcat(Function): + """ + map_concat(map, ...) - Concatenates all the given maps into one. + """ + + +@MapConcat.register # type: ignore +def infer_type(*args: ct.MapType) -> ct.MapType: + return args[0].type + + +class MapContainsKey(Function): + """ + map_contains_key(map, key) - Returns true if the map contains the given key. + """ + + +@MapContainsKey.register # type: ignore +def infer_type(map_: ct.MapType, key: ct.ColumnType) -> ct.BooleanType: + return ct.BooleanType() + + +class MapEntries(Function): + """ + map_entries(map) - Returns an unordered array of all entries in the given map. + """ + + +@MapEntries.register # type: ignore +def infer_type(map_: ct.MapType) -> ct.ColumnType: + return ct.ListType( + element_type=ct.StructType( + ct.NestedField("key", field_type=map_.type.key.type), + ct.NestedField("value", field_type=map_.type.value.type), + ), + ) + + +class MapFilter(Function): + """ + map_filter(map, function) - Returns a map that only includes the entries that match the + given predicate. + """ + + @staticmethod + def compile_lambda(*args): + """ + Compiles the lambda function used by the `map_filter` Spark function so that + the lambda's expression can be evaluated to determine the result's type. + """ + from datajunction_server.sql.parsing import ast + + expr, func = args + if len(func.identifiers) != 2: + raise DJParseException( # pragma: no cover + message="The function `map_filter` takes a lambda function that takes " + "exactly two arguments.", + ) + identifiers = {iden.name: idx for idx, iden in enumerate(func.identifiers)} + lambda_arg_cols = { + identifiers[col.alias_or_name.name]: col + for col in func.expr.find_all(ast.Column) + if col.alias_or_name.name in identifiers + } + lambda_arg_cols[0].add_type(expr.type.key.type) + lambda_arg_cols[1].add_type(expr.type.value.type) + + +@MapFilter.register # type: ignore +def infer_type(map_: ct.MapType, function: ct.BooleanType) -> ct.MapType: + return map_.type + + +class MapFromArrays(Function): + """ + map_from_arrays(keys, values) - Creates a map from two arrays. + """ + + +@MapFromArrays.register # type: ignore +def infer_type(keys: ct.ListType, values: ct.ListType) -> ct.MapType: + return ct.MapType( + key_type=keys.type.element.type, + value_type=values.type.element.type, + ) + + +class MapFromEntries(Function): + """ + map_from_entries(array) - Creates a map from an array of entries. + """ + + +@MapFromEntries.register # type: ignore +def infer_type(array_of_entries: ct.ListType) -> ct.ColumnType: + entry = cast(ct.StructType, array_of_entries.type.element.type) + key, value = entry.fields + return ct.MapType(key_type=key.type, value_type=value.type) + + +class MapKeys(Function): + """ + map_keys(map) - Returns an unordered array containing the keys of the map. + """ + + +@MapKeys.register # type: ignore +def infer_type( + map_: ct.MapType, +) -> ct.ColumnType: + return ct.ListType(element_type=map_.type.key.type) + + +class MapValues(Function): + """ + map_values(map) - Returns an unordered array containing the values of the map. + """ + + +@MapValues.register # type: ignore +def infer_type(map_: ct.MapType) -> ct.ColumnType: + return ct.ListType(element_type=map_.type.value.type) + + +class MapZipWith(Function): + """ + map_zip_with(map1, map2, function) - Returns a merged map of two given maps by + applying function to the pair of values with the same key. + """ + + @staticmethod + def compile_lambda(*args): + """ + Compiles the lambda function used by the `map_zip_with` Spark function so that + the lambda's expression can be evaluated to determine the result's type. + """ + from datajunction_server.sql.parsing import ast + + map1, map2, func = args + available_identifiers = { + identifier.name: idx for idx, identifier in enumerate(func.identifiers) + } + columns = list( + func.expr.filter( + lambda x: isinstance(x, ast.Column) + and x.alias_or_name.name in available_identifiers, + ), + ) + for col in columns: + if available_identifiers.get(col.alias_or_name.name) == 0: + col.add_type(map1.type.key.type) # pragma: no cover + if available_identifiers.get(col.alias_or_name.name) == 1: + col.add_type(map1.type) + if available_identifiers.get(col.alias_or_name.name) == 2: + col.add_type(map2.type) + + +@MapZipWith.register # type: ignore +def infer_type( + map1: ct.MapType, + map2: ct.MapType, + function: ct.ColumnType, +) -> ct.ColumnType: + return map1.type + + +class Mask(Function): + """ + mask(input[, upperChar, lowerChar, digitChar, otherChar]) - masks the + given string value. The function replaces characters with 'X' or 'x', + and numbers with 'n'. This can be useful for creating copies of tables + with sensitive information removed. + """ + + +@Mask.register # type: ignore +def infer_type( + input_: ct.StringType, + upper: Optional[ct.StringType] = None, + lower: Optional[ct.StringType] = None, + digit: Optional[ct.StringType] = None, + other: Optional[ct.StringType] = None, +) -> ct.StringType: + return ct.StringType() + + +class Max(Function): + """ + Computes the maximum value of the input column or expression. + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Max.register # type: ignore +def infer_type( + arg: ct.StringType, +) -> ct.StringType: + return arg.type # pragma: no cover + + +@Max.register # type: ignore +def infer_type( + arg: ct.NumberType, +) -> ct.NumberType: + return arg.type # pragma: no cover + + +@Max.register # type: ignore +def infer_type( + arg: ct.DateType, +) -> ct.DateType: + return arg.type # pragma: no cover + + +@Max.register # type: ignore +def infer_type( + arg: ct.TimestampType, +) -> ct.TimestampType: + return arg.type # pragma: no cover + + +class MaxBy(Function): + """ + max_by(val, key) - Returns the value of val corresponding to the maximum value of key. + """ + + +@MaxBy.register # type: ignore +def infer_type(val: ct.ColumnType, key: ct.ColumnType) -> ct.ColumnType: + return val.type + + +class Md5(Function): + """ + md5(expr) - Calculates the MD5 hash of the given value. + """ + + +@Md5.register # type: ignore +def infer_type(arg: ct.ColumnType) -> ct.ColumnType: + return ct.StringType() + + +class Mean(Function): + """ + mean(expr) - Returns the average of the values in the group. + """ + + +@Mean.register # type: ignore +def infer_type(arg: ct.ColumnType) -> ct.ColumnType: + return ct.DoubleType() + + +class Median(Function): + """ + median(expr) - Returns the median of the values in the group. + """ + + +@Median.register # type: ignore +def infer_type(arg: ct.NumberType) -> ct.ColumnType: + return ct.DoubleType() + + +# TODO: fix parsing of: +# SELECT median(col) FROM VALUES (INTERVAL '0' MONTH), +# (INTERVAL '10' MONTH) AS tab(col) +# in order to test this +@Median.register # type: ignore +def infer_type(arg: ct.IntervalTypeBase) -> ct.IntervalTypeBase: # pragma: no cover + return arg.type + + +class Min(Function): + """ + Computes the minimum value of the input column or expression. + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Min.register # type: ignore +def infer_type( + arg: ct.StringType, +) -> ct.StringType: + return arg.type # pragma: no cover + + +@Min.register # type: ignore +def infer_type( + arg: ct.NumberType, +) -> ct.NumberType: + return arg.type # pragma: no cover + + +@Min.register # type: ignore +def infer_type( + arg: ct.DateType, +) -> ct.DateType: + return arg.type # pragma: no cover + + +@Min.register # type: ignore +def infer_type( + arg: ct.TimestampType, +) -> ct.TimestampType: + return arg.type # pragma: no cover + + +class MinBy(Function): + """ + min_by(val, key) - Returns the value of val corresponding to the minimum value of key. + """ + + +@MinBy.register # type: ignore +def infer_type(val: ct.ColumnType, key: ct.ColumnType) -> ct.ColumnType: + return val.type + + +class Minute(Function): + """ + minute(timestamp) - Returns the minute component of the string/timestamp + """ + + +@Minute.register # type: ignore +def infer_type(val: Union[ct.StringType, ct.TimestampType]) -> ct.IntegerType: + return ct.IntegerType() + + +class Mod(Function): + """ + mod(expr1, expr2) - Returns the remainder after expr1/expr2. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Mod.register # type: ignore +def infer_type(expr1: ct.NumberType, expr2: ct.NumberType) -> ct.FloatType: + return ct.FloatType() + + +class Mode(Function): + """ + mode(col) - Returns the most frequent value for the values within col. + """ + + +@Mode.register # type: ignore +def infer_type(arg: ct.ColumnType) -> ct.ColumnType: + return arg.type + + +class MonotonicallyIncreasingId(Function): + """ + monotonically_increasing_id() - Returns monotonically increasing 64-bit integers + """ + + +@MonotonicallyIncreasingId.register # type: ignore +def infer_type() -> ct.BigIntType: + return ct.BigIntType() + + +class Month(Function): + """ + Extracts the month of a date or timestamp. + """ + + +@Month.register +def infer_type(arg: Union[ct.StringType, ct.DateTimeBase]) -> ct.BigIntType: + return ct.BigIntType() + + +class MonthsBetween(Function): + """ + months_between(timestamp1, timestamp2[, roundOff]) + """ + + +@MonthsBetween.register +def infer_type( + arg: Union[ct.StringType, ct.TimestampType], + arg2: Union[ct.StringType, ct.TimestampType], + arg3: Optional[ct.BooleanType] = None, +) -> ct.BigIntType: + return ct.FloatType() + + +class NamedStruct(Function): + """ + named_struct(name, val, ...) - Creates a new struct with the given field names and values. + """ + + +@NamedStruct.register # type: ignore +def infer_type(*args: ct.ColumnType) -> ct.ColumnType: + args_iter = iter(args) + nested_fields = [ + ct.NestedField( + name=field_name.value.replace("'", ""), + field_type=field_value.type, + ) + for field_name, field_value in zip(args_iter, args_iter) + ] + return ct.StructType(*nested_fields) + + +class Nanvl(Function): + """ + nanvl(expr1, expr2) - Returns the first argument if it is not NaN, + or the second argument if the first argument is NaN. + """ + + +@Nanvl.register # type: ignore +def infer_type(expr1: ct.NumberType, expr2: ct.NumberType) -> ct.NumberType: + return expr1.type + + +class Negative(Function): + """ + negative(expr) - Returns the negated value of the input expression. + """ + + +@Negative.register # type: ignore +def infer_type(arg: ct.NumberType) -> ct.NumberType: + return arg.type + + +class NextDay(Function): + """ + next_day(start_date, day_of_week) - Returns the first date which is + later than start_date and named as indicated. + """ + + +@NextDay.register # type: ignore +def infer_type( + date: Union[ct.DateType, ct.StringType], + day_of_week: ct.StringType, +) -> ct.ColumnType: + return ct.DateType() + + +class Not(Function): + """ + not(expr) - Returns the logical NOT of the Boolean expression. + """ + + +@Not.register # type: ignore +def infer_type(arg: ct.BooleanType) -> ct.ColumnType: + return ct.BooleanType() # pragma: no cover + + +class Now(Function): + """ + Returns the current timestamp. + """ + + +@Now.register # type: ignore +def infer_type() -> ct.TimestampType: + return ct.TimestampType() + + +class NthValue(Function): + """ + nth_value(input[, offset]) - Returns the value of input at the row + that is the offset-th row from beginning of the window frame + """ + + +@NthValue.register # type: ignore +def infer_type(expr: ct.ColumnType, offset: ct.IntegerType) -> ct.ColumnType: + return expr.type + + +class Ntile(Function): + """ + ntile(n) - Divides the rows for each window partition into n buckets + ranging from 1 to at most n. + """ + + +@Ntile.register # type: ignore +def infer_type(n_buckets: ct.IntegerType) -> ct.ColumnType: + return ct.IntegerType() + + +class Nullif(Function): + """ + nullif(expr1, expr2) - Returns null if expr1 equals expr2, or expr1 otherwise. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Nullif.register # type: ignore +def infer_type(expr1: ct.ColumnType, expr2: ct.ColumnType) -> ct.ColumnType: + return expr1.type + + +class Nvl(Function): + """ + nvl(expr1, expr2) - Returns the first argument if it is not null, or the + second argument if the first argument is null. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Nvl.register # type: ignore +def infer_type(expr1: ct.ColumnType, expr2: ct.ColumnType) -> ct.ColumnType: + return expr1.type + + +class Nvl2(Function): + """ + nvl2(expr1, expr2, expr3) - Returns expr3 if expr1 is null, or expr2 otherwise. + """ + + +@Nvl2.register # type: ignore +def infer_type( + expr1: ct.ColumnType, + expr2: ct.ColumnType, + expr3: ct.ColumnType, +) -> ct.ColumnType: + return expr1.type + + +class OctetLength(Function): + """ + octet_length(expr) - Returns the number of bytes in the input string. + """ + + +@OctetLength.register # type: ignore +def infer_type(expr: ct.StringType) -> ct.ColumnType: + return ct.IntegerType() + + +class Overlay(Function): + """ + overlay(expr1, expr2, start[, length]) - Replaces the substring of expr1 + specified by start (and optionally length) with expr2. + """ + + +@Overlay.register # type: ignore +def infer_type( + input_: ct.StringType, + replace: ct.StringType, + pos: ct.IntegerType, + length: Optional[ct.IntegerType] = None, +) -> ct.ColumnType: + return ct.StringType() + + +class Percentile(Function): + """ + percentile() - Computes the percentage ranking of a value in a group of values. + """ + + is_aggregation = True + + +@Percentile.register +def infer_type( + col: Union[ct.NumberType, ct.IntervalTypeBase], + percentage: ct.NumberType, +) -> ct.FloatType: + return ct.FloatType() # type: ignore + + +@Percentile.register +def infer_type( + col: Union[ct.NumberType, ct.IntervalTypeBase], + percentage: ct.NumberType, + freq: ct.IntegerType, +) -> ct.FloatType: + return ct.FloatType() # type: ignore + + +@Percentile.register +def infer_type( + col: Union[ct.NumberType, ct.IntervalTypeBase], + percentage: ct.ListType, +) -> ct.ListType: + return ct.ListType(element_type=ct.FloatType()) # type: ignore + + +@Percentile.register +def infer_type( + col: Union[ct.NumberType, ct.IntervalTypeBase], + percentage: ct.ListType, + freq: ct.IntegerType, +) -> ct.ListType: + return ct.ListType(element_type=ct.FloatType()) # type: ignore + + +class PercentRank(Function): + """ + percent_rank() - Computes the percentage ranking of a value in a group of values. + """ + + is_aggregation = True + + +@PercentRank.register +def infer_type(arg: ct.NumberType) -> ct.DoubleType: + return ct.DoubleType() + + +class Pow(Function): + """ + Raises a base expression to the power of an exponent expression. + """ + + +@Pow.register # type: ignore +def infer_type( + base: ct.ColumnType, + power: ct.ColumnType, +) -> ct.DoubleType: + return ct.DoubleType() + + +class Power(Function): + """ + Raises a base expression to the power of an exponent expression. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Power.register # type: ignore +def infer_type( + base: ct.ColumnType, + power: ct.ColumnType, +) -> ct.DoubleType: + return ct.DoubleType() + + +class Rand(Function): + """ + rand() - Returns a random value with independent and identically distributed + (i.i.d.) uniformly distributed values in [0, 1). + """ + + +@Rand.register +def infer_type() -> ct.FloatType: + return ct.FloatType() + + +@Rand.register +def infer_type(seed: ct.IntegerType) -> ct.FloatType: + return ct.FloatType() + + +@Rand.register +def infer_type(seed: ct.NullType) -> ct.FloatType: + return ct.FloatType() + + +class Randn(Function): + """ + randn() - Returns a random value with independent and identically + distributed (i.i.d.) values drawn from the standard normal distribution. + """ + + +@Randn.register +def infer_type() -> ct.FloatType: + return ct.FloatType() + + +@Randn.register +def infer_type(seed: ct.IntegerType) -> ct.FloatType: + return ct.FloatType() + + +@Randn.register +def infer_type(seed: ct.NullType) -> ct.FloatType: + return ct.FloatType() + + +class Random(Function): + """ + random() - Returns a random value with independent and identically + distributed (i.i.d.) uniformly distributed values in [0, 1). + """ + + +@Random.register +def infer_type() -> ct.FloatType: + return ct.FloatType() + + +@Random.register +def infer_type(seed: ct.IntegerType) -> ct.FloatType: + return ct.FloatType() + + +@Random.register +def infer_type(seed: ct.NullType) -> ct.FloatType: + return ct.FloatType() + + +class Rank(Function): + """ + rank() - Computes the rank of a value in a group of values. The result is + one plus the number of rows preceding or equal to the current row in the + ordering of the partition. The values will produce gaps in the sequence. + """ + + +@Rank.register +def infer_type() -> ct.IntegerType: + return ct.IntegerType() + + +@Rank.register +def infer_type(_: ct.ColumnType) -> ct.IntegerType: + return ct.IntegerType() + + +class RegexpExtract(Function): + """ + regexp_extract(str, regexp[, idx]) - Extract the first string in the str that + match the regexp expression and corresponding to the regex group index. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@RegexpExtract.register +def infer_type( # type: ignore + str_: ct.StringType, + regexp: ct.StringType, + idx: Optional[ct.IntegerType] = 1, +) -> ct.StringType: + return ct.StringType() + + +class RegexpLike(Function): + """ + regexp_like(str, regexp) - Returns true if str matches regexp, or false otherwise + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@RegexpLike.register +def infer_type( # type: ignore + arg1: ct.StringType, + arg2: ct.StringType, +) -> ct.BooleanType: + return ct.BooleanType() + + +class RegexpReplace(Function): + """ + regexp_replace(str, regexp, rep[, position]) - Replaces all substrings of str that + match regexp with rep. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@RegexpReplace.register +def infer_type( # type: ignore + str_: ct.StringType, + regexp: ct.StringType, + rep: ct.StringType, + position: Optional[ct.IntegerType] = 1, +) -> ct.StringType: + return ct.StringType() + + +class Replace(Function): + """ + replace(str, search[, replace]) - Replaces all occurrences of `search` with `replace`. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Replace.register +def infer_type( # type: ignore + string: ct.StringType, + search: ct.StringType, + replace: Optional[ct.StringType] = "", +) -> ct.StringType: + return ct.StringType() + + +class RowNumber(Function): + """ + row_number() - Assigns a unique, sequential number to each row, starting with + one, according to the ordering of rows within the window partition. + """ + + +@RowNumber.register +def infer_type() -> ct.IntegerType: + return ct.IntegerType() + + +class Round(Function): + """ + Rounds a numeric column or expression to the specified number of decimal places. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Round.register # type: ignore +def infer_type( + child: ct.DecimalType, + scale: ct.IntegerBase, +) -> ct.NumberType: + child_type = child.type + integral_least_num_digits = child_type.precision - child_type.scale + 1 + if scale.value < 0: + new_precision = max( + integral_least_num_digits, + -scale.type.value + 1, + ) # pragma: no cover + return ct.DecimalType(new_precision, 0) # pragma: no cover + new_scale = min(child_type.scale, scale.value) + return ct.DecimalType(integral_least_num_digits + new_scale, new_scale) + + +@Round.register +def infer_type( # type: ignore + child: ct.NumberType, + scale: ct.IntegerBase, +) -> ct.NumberType: + if scale.value == 0: + return ct.IntegerType() + return child.type + + +@Round.register +def infer_type( # type: ignore + child: ct.NumberType, +) -> ct.NumberType: + return ct.IntegerType() + + +class Sequence(Function): + """ + Generates an array of elements from start to stop (inclusive), incrementing by step. + """ + + +@Sequence.register +def infer_type( # type: ignore + start: ct.IntegerBase, + end: ct.IntegerBase, + step: Optional[ct.IntegerBase] = None, +) -> ct.ListType: + return ct.ListType(element_type=start.type) + + +@Sequence.register +def infer_type( # type: ignore + start: ct.TimestampType, + end: ct.TimestampType, + step: ct.IntervalTypeBase, +) -> ct.ListType: + return ct.ListType(element_type=ct.TimestampType()) + + +@Sequence.register +def infer_type( # type: ignore + start: ct.DateType, + end: ct.DateType, + step: ct.IntervalTypeBase, +) -> ct.ListType: + return ct.ListType(element_type=ct.DateType()) + + +class Size(Function): + """ + size(expr) - Returns the size of an array or a map. The function returns + null for null input if spark.sql.legacy.sizeOfNull is set to false or + spark.sql.ansi.enabled is set to true. Otherwise, the function returns -1 + for null input. With the default settings, the function returns -1 for null + input. + """ + + +@Size.register # type: ignore +def infer_type( + arg: ct.ListType, +) -> ct.IntegerType: + return ct.IntegerType() + + +@Size.register # type: ignore +def infer_type( + arg: ct.MapType, +) -> ct.IntegerType: + return ct.IntegerType() + + +class Split(Function): + """ + Splits str around occurrences that match regex and returns an + array with a length of at most limit + """ + + +@Split.register +def infer_type( + string: ct.StringType, + regex: ct.StringType, + limit: Optional[ct.IntegerType] = None, +) -> ct.ColumnType: + return ct.ListType(element_type=ct.StringType()) # type: ignore + + +class Sqrt(Function): + """ + Computes the square root of a numeric column or expression. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Sqrt.register +def infer_type(arg: ct.NumberType) -> ct.DoubleType: + return ct.DoubleType() + + +class Stddev(Function): + """ + Computes the sample standard deviation of a numerical column or expression. + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Stddev.register +def infer_type(arg: ct.NumberType) -> ct.DoubleType: + return ct.DoubleType() + + +class StddevPop(Function): + """ + Computes the population standard deviation of the input column or expression. + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@StddevPop.register +def infer_type(arg: ct.NumberType) -> ct.DoubleType: + return ct.DoubleType() + + +class StddevSamp(Function): + """ + Computes the sample standard deviation of the input column or expression. + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@StddevSamp.register +def infer_type(arg: ct.NumberType) -> ct.DoubleType: + return ct.DoubleType() + + +class Strpos(Function): + """ + strpos(string, substring) -> bigint + Returns the starting position of the first instance of substring in string. Positions + start with 1. If not found, 0 is returned. + strpos(string, substring, instance) -> bigint + Returns the position of the N-th instance of substring in string. When instance is a + negative number the search will start from the end of string. Positions start with 1. + If not found, 0 is returned. + """ + + dialects = [Dialect.TRINO, Dialect.DRUID] + + +@Strpos.register +def infer_type( + string: ct.StringType, + substring: ct.StringType, +) -> ct.IntegerType: + return ct.IntegerType() + + +@Strpos.register +def infer_type( + string: ct.StringType, + substring: ct.StringType, + instance: ct.IntegerType, +) -> ct.IntegerType: + return ct.IntegerType() + + +class Struct(Function): + """ + struct(val1, val2, ...) - Creates a new struct with the given field values. + """ + + +@Struct.register # type: ignore +def infer_type(*args: ct.ColumnType) -> ct.StructType: + return ct.StructType( + *[ + ct.NestedField( + name=arg.alias.name if hasattr(arg, "alias") else f"col{idx}", + field_type=arg.type, + ) + for idx, arg in enumerate(args) + ], + ) + + +class Substring(Function): + """ + Extracts a substring from a string column or expression. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Substring.register +def infer_type( # type: ignore + string: ct.StringType, + pos: ct.IntegerType, +) -> ct.StringType: + return ct.StringType() + + +@Substring.register +def infer_type( # type: ignore + string: ct.StringType, + pos: ct.IntegerType, + length: ct.IntegerType, +) -> ct.StringType: + return ct.StringType() + + +class Substr(Function): + """ + Extracts a substring from a string column or expression. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Substr.register +def infer_type( # type: ignore + string: ct.StringType, + pos: ct.IntegerType, +) -> ct.StringType: + return ct.StringType() + + +@Substr.register +def infer_type( # type: ignore + string: ct.StringType, + pos: ct.IntegerType, + length: ct.IntegerType, +) -> ct.StringType: + return ct.StringType() # pragma: no cover + + +class Sum(Function): + """ + Computes the sum of the input column or expression. + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Sum.register # type: ignore +def infer_type( + arg: ct.IntegerBase, +) -> ct.BigIntType: + return ct.BigIntType() + + +@Sum.register # type: ignore +def infer_type( + arg: ct.DecimalType, +) -> ct.DecimalType: + precision = arg.type.precision + scale = arg.type.scale + return ct.DecimalType(precision + min(10, 31 - precision), scale) + + +@Sum.register # type: ignore +def infer_type( + arg: Union[ct.NumberType, ct.IntervalTypeBase], +) -> ct.DoubleType: + return ct.DoubleType() + + +@Sum.register # type: ignore +def infer_type( + arg: Union[ct.DateType, ct.TimestampType], +) -> ct.DoubleType: + return ct.DoubleType() + + +class ToDate(Function): # pragma: no cover + """ + Converts a date string to a date value. + """ + + +@ToDate.register # type: ignore +def infer_type( + expr: ct.StringType, + fmt: Optional[ct.StringType] = None, +) -> ct.DateType: + return ct.DateType() + + +class ToTimestamp(Function): # pragma: no cover + """ + Parses the timestamp_str expression with the fmt expression to a timestamp. + """ + + +@ToTimestamp.register # type: ignore +def infer_type( + expr: ct.StringType, + fmt: Optional[ct.StringType] = None, +) -> ct.TimestampType: + return ct.TimestampType() + + +class Transform(Function): + """ + transform(expr, func) - Transforms elements in an array + using the function. + """ + + @staticmethod + def compile_lambda(*args): + """ + Compiles the lambda function used by the `transform` Spark function so that + the lambda's expression can be evaluated to determine the result's type. + """ + from datajunction_server.sql.parsing import ast + + expr, func = args + available_identifiers = { + identifier.name: idx for idx, identifier in enumerate(func.identifiers) + } + columns = list( + func.expr.filter( + lambda x: isinstance(x, ast.Column) + and x.alias_or_name.name in available_identifiers, + ), + ) + for col in columns: + # The array element arg + if available_identifiers.get(col.alias_or_name.name) == 0: + col.add_type(expr.type.element.type) + + # The index arg (optional) + if available_identifiers.get(col.alias_or_name.name) == 1: + col.add_type(ct.IntegerType()) + + +@Transform.register # type: ignore +def infer_type( + expr: ct.ListType, + func: ct.PrimitiveType, +) -> ct.ColumnType: + return ct.ListType(element_type=func.expr.type) + + +class TransformKeys(Function): + """ + transform_keys(expr, func) - Transforms keys in a map using the function + """ + + @staticmethod + def compile_lambda(*args): + """ + Compiles the lambda function used by the `transform_keys` Spark function so that + the lambda's expression can be evaluated to determine the result's type. + """ + from datajunction_server.sql.parsing import ast + + expr, func = args + available_identifiers = { + identifier.name: idx for idx, identifier in enumerate(func.identifiers) + } + columns = list( + func.expr.filter( + lambda x: isinstance(x, ast.Column) + and x.alias_or_name.name in available_identifiers, + ), + ) + for col in columns: + # The map key arg + if available_identifiers.get(col.alias_or_name.name) == 0: + col.add_type(expr.type.key.type) + + # The map value arg + if available_identifiers.get(col.alias_or_name.name) == 1: + col.add_type(expr.type.value.type) + + +@TransformKeys.register # type: ignore +def infer_type( + expr: ct.MapType, + func: ct.ColumnType, +) -> ct.MapType: + return ct.MapType(key_type=func.expr.type, value_type=expr.type.value.type) + + +class TransformValues(Function): + """ + transform_values(expr, func) - Transforms values in a map using the function + """ + + @staticmethod + def compile_lambda(*args): + """ + Compiles the lambda function used by the `transform_values` Spark function so that + the lambda's expression can be evaluated to determine the result's type. + """ + from datajunction_server.sql.parsing import ast + + expr, func = args + available_identifiers = { + identifier.name: idx for idx, identifier in enumerate(func.identifiers) + } + columns = list( + func.expr.filter( + lambda x: isinstance(x, ast.Column) + and x.alias_or_name.name in available_identifiers, + ), + ) + for col in columns: + # The map key arg + if available_identifiers.get(col.alias_or_name.name) == 0: + col.add_type(expr.type.key.type) + + # The map value arg + if available_identifiers.get(col.alias_or_name.name) == 1: + col.add_type(expr.type.value.type) + + +@TransformValues.register # type: ignore +def infer_type( + expr: ct.MapType, + func: ct.ColumnType, +) -> ct.MapType: + return ct.MapType(key_type=expr.type.key.type, value_type=func.expr.type) + + +class Trim(Function): + """ + Removes leading and trailing whitespace from a string value. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Trim.register +def infer_type(arg: ct.StringType) -> ct.StringType: + return ct.StringType() + + +class Timestamp(Function): + """ + timestamp(expr) - Casts the value expr to the target data type timestamp. + """ + + # Druid has this function but it means something else: unix_millis() + dialects = [Dialect.SPARK] + + +@Timestamp.register +def infer_type(expr: ct.StringType) -> ct.TimestampType: + return ct.TimestampType() + + +class TimestampMicros(Function): + """ + timestamp_micros(microseconds) - Creates timestamp from the number of microseconds + since UTC epoch. + """ + + dialects = [Dialect.SPARK] + + +@TimestampMicros.register +def infer_type(microseconds: ct.BigIntType) -> ct.TimestampType: + return ct.TimestampType() + + +class TimestampMillis(Function): + """ + timestamp_millis(milliseconds) - Creates timestamp from the number of milliseconds + since UTC epoch. + """ + + dialects = [Dialect.SPARK] + + +@TimestampMillis.register +def infer_type(milliseconds: ct.BigIntType) -> ct.TimestampType: + return ct.TimestampType() + + +class TimestampSeconds(Function): + """ + timestamp_seconds(seconds) - Creates timestamp from the number of seconds + (can be fractional) since UTC epoch. + """ + + dialects = [Dialect.SPARK] + + +@TimestampSeconds.register +def infer_type(seconds: ct.NumberType) -> ct.TimestampType: + return ct.TimestampType() + + +class Unhex(Function): + """ + unhex(str) - Interprets each pair of characters in the input string as a + hexadecimal number and converts it to the byte that number represents. + The output is a binary string. + """ + + +@Unhex.register # type: ignore +def infer_type(arg: ct.StringType) -> ct.ColumnType: + return ct.BinaryType() + + +class UnixDate(Function): + """ + unix_date(date) - Returns the number of days since 1970-01-01. + """ + + dialects = [Dialect.SPARK] + + +@UnixDate.register # type: ignore +def infer_type(arg: ct.DateType) -> ct.IntegerType: + return ct.IntegerType() + + +class UnixMicros(Function): + """ + unix_micros(timestamp) - Returns the number of microseconds since 1970-01-01 00:00:00 UTC. + """ + + dialects = [Dialect.SPARK] + + +@UnixMicros.register # type: ignore +def infer_type(arg: ct.TimestampType) -> ct.BigIntType: + return ct.BigIntType() + + +class UnixMillis(Function): + """ + unix_millis(timestamp) - Returns the number of milliseconds since 1970-01-01 00:00:00 UTC. + Truncates higher levels of precision. + """ + + dialects = [Dialect.SPARK] + + +@UnixMillis.register # type: ignore +def infer_type(arg: ct.TimestampType) -> ct.BigIntType: + return ct.BigIntType() + + +class UnixSeconds(Function): + """ + unix_seconds(timestamp) - Returns the number of seconds since 1970-01-01 00:00:00 UTC. + Truncates higher levels of precision. + """ + + dialects = [Dialect.SPARK] + + +@UnixSeconds.register # type: ignore +def infer_type(arg: ct.TimestampType) -> ct.BigIntType: + return ct.BigIntType() + + +class UnixTimestamp(Function): + """ + unix_timestamp([time_exp[, fmt]]) - Returns the UNIX timestamp of current or specified time. + + Arguments: + time_exp - A date/timestamp or string. If not provided, this defaults to current time. + fmt - Date/time format pattern to follow. Ignored if time_exp is not a string. + Default value is "yyyy-MM-dd HH:mm:ss". See Datetime Patterns for valid date and time + format patterns. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@UnixTimestamp.register # type: ignore +def infer_type( + time_exp: Optional[ct.StringType] = None, + fmt: Optional[ct.StringType] = None, +) -> ct.BigIntType: + return ct.BigIntType() + + +class Upper(Function): + """ + Converts a string value to uppercase. + """ + + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Upper.register +def infer_type(arg: ct.StringType) -> ct.StringType: + return ct.StringType() + + +class Variance(Function): + """ + Computes the sample variance of the input column or expression. + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@Variance.register +def infer_type(arg: ct.NumberType) -> ct.DoubleType: + return ct.DoubleType() + + +class VarPop(Function): + """ + Computes the population variance of the input column or expression. + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@VarPop.register +def infer_type(arg: ct.NumberType) -> ct.DoubleType: + return ct.DoubleType() + + +class VarSamp(Function): + """ + Computes the sample variance of the input column or expression. + """ + + is_aggregation = True + dialects = [Dialect.SPARK, Dialect.DRUID] + + +@VarSamp.register +def infer_type(arg: ct.NumberType) -> ct.DoubleType: + return ct.DoubleType() + + +class Week(Function): + """ + Returns the week number of the year of the input date value. + Note: Trino-only + """ + + +@Week.register +def infer_type(arg: Union[ct.StringType, ct.DateTimeBase]) -> ct.BigIntType: + return ct.BigIntType() + + +class Year(Function): + """ + Returns the year of the input date value. + """ + + +@Year.register +def infer_type( + arg: Union[ct.StringType, ct.DateTimeBase, ct.IntegerType], +) -> ct.BigIntType: + return ct.BigIntType() + + +################################################################################## +# Table Functions # +# https://spark.apache.org/docs/3.3.2/sql-ref-syntax-qry-select-tvf.html#content # +################################################################################## + + +class Explode(TableFunction): + """ + The Explode function is used to explode the specified array, + nested array, or map column into multiple rows. + The explode function will generate a new row for each + element in the specified column. + """ + + +@Explode.register +def infer_type( + arg: ct.ListType, +) -> List[ct.NestedField]: + return [arg.element] + + +@Explode.register +def infer_type( + arg: ct.MapType, +) -> List[ct.NestedField]: + return [arg.key, arg.value] + + +class Unnest(TableFunction): + """ + The unnest function is used to explode the specified array, + nested array, or map column into multiple rows. + It will generate a new row for each element in the specified column. + """ + + dialects = [Dialect.TRINO, Dialect.DRUID] + + +@Unnest.register +def infer_type( + arg: ct.ListType, +) -> List[ct.NestedField]: + return [arg.element] # pragma: no cover + + +@Unnest.register +def infer_type( + arg: ct.MapType, +) -> List[ct.NestedField]: + return [arg.key, arg.value] + + +class FunctionRegistryDict(dict): + """ + Custom dictionary mapping for functions + """ + + def __getitem__(self, key): + """ + Returns a custom error about functions that haven't been implemented yet. + """ + try: + return super().__getitem__(key) + except KeyError as exc: + raise DJNotImplementedException( + f"The function `{key}` hasn't been implemented in " + "DJ yet. You can file an issue at https://github." + "com/DataJunction/dj/issues/new?title=Function+" + f"missing:+{key} to request it to be added, or use " + "the documentation at https://github.com/DataJunct" + "ion/dj/blob/main/docs/functions.rst to implement it.", + ) from exc + + +function_registry = FunctionRegistryDict() +for cls in Function.__subclasses__(): + snake_cased = re.sub(r"(? Iterator: + """ + Flattens `maybe_iterables` by descending into items that are Iterable + """ + + if not isinstance(maybe_iterables, (list, tuple, set, Iterator)): + return iter([maybe_iterables]) + return chain.from_iterable( + (flatten(maybe_iterable) for maybe_iterable in maybe_iterables) + ) + + +@dataclass +class CompileContext: + session: AsyncSession + exception: DJException + + +# typevar used for node methods that return self +# so the typesystem can correlate the self type with the return type +TNode = TypeVar("TNode", bound="Node") + + +class Node(ABC): + """Base class for all DJ AST nodes. + + DJ nodes are python dataclasses with the following patterns: + - Attributes are either + - PRIMITIVES (int, float, str, bool, None) + - iterable from (list, tuple, set) + - Enum + - descendant of `Node` + - Attributes starting with '_' are "obfuscated" and are not included in `children` + + """ + + parent: Optional["Node"] = None + parent_key: Optional[str] = None + + _is_compiled: bool = False + + def __post_init__(self): + self.add_self_as_parent() + + @property + def depth(self) -> int: + if self.parent is None: + return 0 + return self.parent.depth + 1 + + def clear_parent(self: TNode) -> TNode: + """ + Remove parent from the node + """ + self.parent = None + return self + + def set_parent(self: TNode, parent: "Node", parent_key: str) -> TNode: + """ + Add parent to the node + """ + self.parent = parent + self.parent_key = parent_key + return self + + def add_self_as_parent(self: TNode) -> TNode: + """ + Adds self as a parent to all children + """ + for name, child in self.fields( + flat=True, + nodes_only=True, + obfuscated=False, + nones=False, + named=True, + ): + child.set_parent(self, name) + return self + + def __setattr__(self, key: str, value: Any): + """ + Facilitates setting children using `.` syntax ensuring parent is attributed + """ + if key == "parent": + object.__setattr__(self, key, value) + return + + object.__setattr__(self, key, value) + for child in flatten(value): + if isinstance(child, Node) and not key.startswith("_"): + child.set_parent(self, key) + + def swap(self: TNode, other: "Node") -> TNode: + """ + Swap the Node for another + """ + if not (self.parent and self.parent_key): + return self + parent_attr = getattr(self.parent, self.parent_key) + if parent_attr is self: + setattr(self.parent, self.parent_key, other) + return self.clear_parent() + + new = [] + for iterable_type in (list, tuple, set): + if isinstance(parent_attr, iterable_type): + for element in parent_attr: + if self is element: + new.append(other) + else: + new.append(element) + new = iterable_type(new) + break + + setattr(self.parent, self.parent_key, new) + return self.clear_parent() + + def copy(self: TNode) -> TNode: + """ + Create a deep copy of the `self` + """ + return deepcopy(self) + + def get_nearest_parent_of_type( + self: "Node", + node_type: Type[TNode], + ) -> Optional[TNode]: + """ + Traverse up the tree until you find a node of `node_type` or hit the root + """ + if isinstance(self.parent, node_type): + return self.parent + if self.parent is None: + return None + return self.parent.get_nearest_parent_of_type(node_type) + + def get_furthest_parent( + self: "Node", + ) -> Optional[TNode]: + """ + Traverse up the tree until you find a node of `node_type` or hit the root + """ + if self.parent is None: + return None + curr_parent = self.parent + while True: + if curr_parent.parent is None: + return curr_parent + curr_parent = curr_parent.parent + + def flatten(self) -> Iterator["Node"]: + """ + Flatten the sub-ast of the node as an iterator + """ + return self.filter(lambda _: True) + + def fields( + self, + flat: bool = True, + nodes_only: bool = True, + obfuscated: bool = False, + nones: bool = False, + named: bool = False, + ) -> Iterator: + """ + Returns an iterator over fields of a node with particular filters + + Args: + flat: return a flattened iterator (if children are iterable) + nodes_only: do not yield children that are not Nodes (trumped by `obfuscated`) + obfuscated: yield fields that have leading underscores + (typically accessed via a property) + nones: yield values that are None + (optional fields without a value); trumped by `nodes_only` + named: yield pairs `(field name: str, field value)` + Returns: + Iterator: returns all children of a node given filters + and optional flattening (by default Iterator[Node]) + """ + + def make_child_generator(): + """ + Makes a generator enclosing self to return + not obfuscated fields (fields without starting `_`) + """ + for self_field in fields(self): + if ( + not self_field.name.startswith("_") if not obfuscated else True + ) and (self_field.name in self.__dict__): + value = self.__dict__[self_field.name] + values = [value] + if flat: + values = flatten(value) + for value in values: + if named: + yield (self_field.name, value) + else: + yield value + + # `iter`s used to satisfy mypy (`child_generator` type changes between generator, filter) + child_generator = iter(make_child_generator()) + + if nodes_only: + child_generator = iter( + filter( + lambda child: isinstance(child, Node) + if not named + else isinstance(child[1], Node), + child_generator, + ), + ) + + if not nones: + child_generator = iter( + filter( + lambda child: (child is not None) + if not named + else (child[1] is not None), + child_generator, + ), + ) + + return child_generator + + @property + def children(self) -> Iterator["Node"]: + """ + Returns an iterator of all nodes that are one + step from the current node down including through iterables + """ + return self.fields( + flat=True, + nodes_only=True, + obfuscated=False, + nones=False, + named=False, + ) + + def replace( + self, + from_: "Node", + to: "Node", + compare: Optional[Callable[[Any, Any], bool]] = None, + times: int = -1, + copy: bool = True, + ): + """ + Replace a node `from_` with a node `to` in the subtree + """ + replacements = 0 + compare_ = (lambda a, b: a is b) if compare is None else compare + for node in self.flatten(): + if compare_(node, from_): + other = to.copy() if copy else to + if isinstance(from_, Table) and from_.parent == to: + continue + if isinstance(from_, Table): + for ref in from_.ref_columns: + ref.add_table(other) + node.swap(other) + replacements += 1 + if replacements == times: + return + + def filter(self, func: Callable[["Node"], bool]) -> Iterator["Node"]: + """ + Find all nodes that `func` returns `True` for + """ + if func(self): + yield self + + for node in chain(*[child.filter(func) for child in self.children]): + yield node + + def contains(self, other: "Node") -> bool: + """ + Checks if the subtree of `self` contains the node + """ + return any(self.filter(lambda node: node is other)) + + def is_ancestor_of(self, other: Optional["Node"]) -> bool: + """ + Checks if `self` is an ancestor of the node + """ + return bool(other) and other.contains(self) + + def find_all(self, node_type: Type[TNode]) -> Iterator[TNode]: + """ + Find all nodes of a particular type in the node's sub-ast + """ + return self.filter(lambda n: isinstance(n, node_type)) # type: ignore + + def apply(self, func: Callable[["Node"], None]): + """ + Traverse ast and apply func to each Node + """ + func(self) + for child in self.children: + child.apply(func) + + def compare( + self, + other: "Node", + ) -> bool: + """ + Compare two ASTs for deep equality + """ + if type(self) != type(other): + return False + if id(self) == id(other): + return True + return hash(self) == hash(other) + + def diff(self, other: "Node") -> List[Tuple["Node", "Node"]]: + """ + Compare two ASTs for differences and return the pairs of differences + """ + + def _diff(self, other: "Node"): + if self != other: + diffs.append((self, other)) + else: + for child, other_child in zip_longest(self.children, other.children): + _diff(child, other_child) + + diffs: List[Tuple["Node", "Node"]] = [] + _diff(self, other) + return diffs + + def similarity_score(self, other: "Node") -> float: + """ + Determine how similar two nodes are with a float score + """ + self_nodes = list(self.flatten()) + other_nodes = list(other.flatten()) + intersection = [ + self_node for self_node in self_nodes if self_node in other_nodes + ] + union = ( + [self_node for self_node in self_nodes if self_node not in intersection] + + [ + other_node + for other_node in other_nodes + if other_node not in intersection + ] + + intersection + ) + return len(intersection) / len(union) + + def __eq__(self, other) -> bool: + """ + Compares two nodes for "top level" equality. + + Checks for type equality and primitive field types for full equality. + Compares all others for type equality only. No recursing. + Note: Does not check (sub)AST. See `Node.compare` for comparing (sub)ASTs. + """ + return type(self) == type(other) and all( + s == o if type(s) in PRIMITIVES else type(s) == type(o) + for s, o in zip( + (self.fields(False, False, False, True)), + (other.fields(False, False, False, True)), + ) + ) + + def __hash__(self) -> int: + """ + Hash a node + """ + return hash( + tuple( + chain( + (type(self),), + self.fields( + flat=True, + nodes_only=False, + obfuscated=False, + nones=True, + named=False, + ), + ), + ), + ) + + @abstractmethod + def __str__(self) -> str: + """ + Get the string of a node + """ + + async def compile(self, ctx: CompileContext): + """ + Compile a DJ Node. By default, we call compile on all immediate children of this node. + """ + if self._is_compiled: + return + for child in self.children: + if not child.is_compiled(): + await child.compile(ctx) + child._is_compiled = True + self._is_compiled = True + + def is_compiled(self) -> bool: + """ + Checks whether a DJ AST Node is compiled + """ + return self._is_compiled + + +class DJEnum(Enum): + """ + A DJ AST enum + """ + + def __repr__(self) -> str: + return str(self) + + +@dataclass(eq=False) +class Aliasable(Node): + """ + A mixin for Nodes that are aliasable + """ + + alias: Optional["Name"] = None + as_: Optional[bool] = None + semantic_entity: Optional[str] = None + semantic_type: Optional[SemanticType] = None + + def set_alias(self: TNode, alias: Optional["Name"]) -> TNode: + self.alias = alias + return self + + def set_as(self: TNode, as_: bool) -> TNode: + self.as_ = as_ + return self + + def set_semantic_entity(self: TNode, semantic_entity: str) -> TNode: + self.semantic_entity = semantic_entity + return self + + def set_semantic_type(self: TNode, semantic_type: SemanticType) -> TNode: + self.semantic_type = semantic_type + return self + + @property + def columns(self): + """ + Returns a list of self if aliased or named else an empty list + """ + return [self] + + @property + def alias_or_name(self) -> "Name": + if self.alias is not None: + return self.alias + elif isinstance(self, Named): + return self.name + else: + raise DJParseException("Node has no alias or name.") + + +AliasedType = TypeVar("AliasedType", bound=Node) + + +@dataclass(eq=False) +class Alias(Aliasable, Generic[AliasedType]): + """ + Wraps node types with an alias + """ + + child: AliasedType = field(default_factory=Node) + + def __str__(self) -> str: + as_ = " AS " if self.as_ else " " + return f"{self.child}{as_}{self.alias}" + + def is_aggregation(self) -> bool: + return isinstance(self.child, Expression) and self.child.is_aggregation() + + @property + def type(self) -> ColumnType: + return self.child.type + + @property + def columns(self): + """ + Returns a list of self if aliased or named else an empty list + """ + return [self] + + +TExpression = TypeVar("TExpression", bound="Expression") + + +@dataclass(eq=False) +class Expression(Node): + """ + An expression type simply for type checking + """ + + parenthesized: Optional[bool] = field(init=False, default=None) + + @property + def type(self) -> Union[ColumnType, List[ColumnType]]: + """ + Return the type of the expression + """ + + @property + def columns(self): + """ + Returns a list of self if aliased or named else an empty list + """ + return [] + + def is_aggregation(self) -> bool: + """ + Determines whether an Expression is an aggregation or not + """ + for child in self.children: + if hasattr(child, "is_aggregation") and child.is_aggregation(): + return True + return False + + def set_alias(self: TExpression, alias: "Name") -> Alias[TExpression]: + return Alias(child=self).set_alias(alias) + + def without_aliases(self) -> TExpression: + exp = self + while hasattr(exp, "alias") or isinstance(exp, Alias) or hasattr(exp, "child"): + if hasattr(exp, "child"): + exp = exp.child + elif hasattr(exp, "expression"): + exp = exp.expression + return exp + + +@dataclass(eq=False) +class Name(Node): + """ + The string name specified in sql with quote style + """ + + name: str + quote_style: str = "" + namespace: Optional["Name"] = None + + def __post_init__(self): + if isinstance(self.name, Name): + self.quote_style = self.quote_style or self.name.quote_style + self.namespace = self.namespace or self.name.namespace + self.name = self.name.name + + def __str__(self) -> str: + return self.identifier(True) + + @property + def names(self) -> List["Name"]: + namespace = [self] + name = self + while name.namespace: + namespace.append(name.namespace) + name = name.namespace + return namespace[::-1] + + def identifier(self, quotes: bool = True) -> str: + """ + Yield a string of all the names making up + the name with or without quotes + """ + quote_style = "" if not quotes else self.quote_style + namespace = str(self.namespace) + "." if self.namespace else "" + return f"{namespace}{quote_style}{self.name}{quote_style}" + + +TNamed = TypeVar("TNamed", bound="Named") + + +@dataclass(eq=False) # type: ignore +class Named(Node): + """ + An Expression that has a name + """ + + name: Name + + @staticmethod + def namespaces_intersect( + namespace_a: List[Name], + namespace_b: List[Name], + quotes: bool = False, + ): + return all( + na.name == nb.name if not quotes else str(na) == str(nb) + for na, nb in zip(reversed(namespace_a), reversed(namespace_b)) + ) + + @property + def names(self) -> List[Name]: + return self.name.names + + @property + def namespace(self) -> List[Name]: + return self.names[:-1] + + @property + def columns(self): + """ + Returns a list of self if aliased or named else an empty list + """ + return [self] + + def identifier(self, quotes: bool = True) -> str: + if quotes: + return str(self.name) + + return ".".join( + ( + *(name.name for name in self.namespace), + self.name.name, + ), + ) + + @property + def alias_or_name(self) -> "Name": + return self.name + + +@dataclass(eq=False) +class DefaultName(Name): + name: str = "" + + def __bool__(self) -> bool: + return False + + +@dataclass(eq=False) +class UnNamed(Named): + name: Name = field(default_factory=DefaultName) + + +@dataclass(eq=False) +class Column(Aliasable, Named, Expression): + """ + Column used in statements + """ + + _table: Optional[Union[Aliasable, "TableExpression"]] = field( + repr=False, + default=None, + ) + _is_struct_ref: bool = False + _type: Optional["ColumnType"] = field(repr=False, default=None) + _expression: Optional[Expression] = field(repr=False, default=None) + _is_compiled: bool = False + role: Optional[str] = None + dimension_ref: Optional[str] = None + + @property + def type(self): + if self._type: + return self._type + # Column was derived from some other expression we can get the type of + if self.expression: + self.add_type(self.expression.type) + return self.expression.type + + parent_expr = f"in {self.parent}" if self.parent else "that has no parent" + raise DJParseException(f"Cannot resolve type of column {self} {parent_expr}") + + def add_type(self, type_: ColumnType) -> "Column": + """ + Add a referenced type + """ + self._type = type_ + return self + + def copy(self): + return Column( + name=self.name, + alias=self.alias, + _table=self.table, + _type=self.type, + _is_compiled=self.is_compiled(), + ) + + @property + def expression(self) -> Optional[Expression]: + """ + Return the Expression this node points to in a subquery + """ + return self._expression + + def namespace_table(self): + """ + Turns the column's namespace into a table. + """ + if self.name.namespace: + self._table = Table(name=self.name.namespace) + self.name.namespace = None + self._table.parent = self + self._table.parent_key = "_table" + + def add_expression(self, expression: "Expression") -> "Column": + """ + Add a referenced expression where the column came from + """ + self._expression = expression + return self + + def set_struct_ref(self): + """ + Marks this column as a struct dereference. This implies that we treat the name + and namespace values on this object as struct column and struct subscript values. + """ + self._is_struct_ref = True + + def add_table(self, table: "TableExpression"): + self._table = table + + @property + def table(self) -> Optional["TableExpression"]: + """ + Return the table the column was referenced from + """ + return self._table + + @property + def children(self) -> Iterator[Node]: + if self.table and self.table.parent is self: + return chain(super().children, (self.table,)) + return super().children + + @property + def is_api_column(self) -> bool: + """ + Is the column added from the api? + """ + return self._api_column + + def set_api_column(self, api_column: bool = False) -> "Column": + """ + Set the api column flag + """ + self._api_column = api_column + return self + + def use_alias_as_name(self) -> "Column": + """Use the column's alias as its name""" + self.name = self.alias + self.alias = None + return self + + def is_compiled(self): + return self._is_compiled or (self.table and self._type) + + def column_names(self) -> Tuple[Optional[str], str, Optional[str]]: + """ + Returns the column namespace (if any), column name, and subscript name (if any) + """ + subscript_name = None + column_name = self.name.name + column_namespace = None + if len(self.namespace) == 2: # struct + column_namespace, column_name = self.namespace + column_name = column_name.name + subscript_name = self.name.name + elif len(self.namespace) == 1: # non-struct + column_namespace = self.namespace[0].name + return column_namespace, column_name, subscript_name + + @classmethod + def from_existing(cls, col: Aliasable | Expression, table: "TableExpression"): + """ + Build a selectable column from an existing one + """ + return Column( + col.alias_or_name, + _type=col.type, + semantic_entity=col.semantic_entity, + semantic_type=col.semantic_type, + _table=table, + ) + + async def find_table_sources( + self, + ctx: CompileContext, + ) -> List["TableExpression"]: + # flake8: noqa + """ + Find all tables that this column could have originated from. + """ + query = cast( + Query, + self.get_nearest_parent_of_type(Query), + ) + direct_tables = list( + filter( + lambda tbl: tbl.in_from_or_lateral() + and tbl.get_nearest_parent_of_type(Query) is query, + query.find_all(TableExpression), + ), + ) + if hasattr(self, "child"): + self.add_type(self.child.type) + for table in direct_tables: + if not table.is_compiled(): + await table.compile(ctx) + + namespace = ( + self.name.namespace.identifier(False) if self.name.namespace else "" + ) # a.x -> a + + # Determine if the column is referencing a struct + column_namespace, column_name, subscript_name = self.column_names() + is_struct = column_namespace and column_name and subscript_name + + found = [] + + # Go through TableExpressions directly on the AST first and collect all + # possible origins for this column. There may be more than one if the column + # is not namespaced. + for table in direct_tables: + if ( + # This column may be namespaced, in which case we'll search for an origin table + # that has the namespace as an alias or name. If this column has no namespace, + # it should be sourced from the immediate table + not namespace or namespace == table.alias_or_name.identifier(False) + ): + result = await table.add_ref_column(self, ctx) + if result: + found.append(table) + + # This column may be a struct, meaning it'll have an optional namespace, + # a column name, and a subscript + if not found: + for table in direct_tables: + namespace = namespace.split(SEPARATOR)[0] + if table.column_mapping.get(namespace) or ( + table.column_mapping.get(column_name) + and is_struct + and ( + not namespace + or table.alias_or_name.namespace + and table.alias_or_name.namespace.identifier() == namespace + ) + ): + if await table.add_ref_column(self, ctx): + found.append(table) + + if found: + return found + + if not query.in_from_or_lateral(): + correlation_tables = list( + filter( + lambda tbl: tbl.in_from_or_lateral() + and query.is_ancestor_of(tbl.get_nearest_parent_of_type(Query)), + query.find_all(TableExpression), + ), + ) + for table in correlation_tables: + if not namespace or table.alias_or_name.identifier(False) == namespace: + if await table.add_ref_column(self, ctx): + found.append(table) + + if found: + return found + + # Check for ctes + alpha_query = self.get_furthest_parent() + direct_table_names = { + direct_table.alias_or_name.identifier() for direct_table in direct_tables + } + if isinstance(alpha_query, Query) and alpha_query.ctes: + for cte in alpha_query.ctes: + cte_name = cte.alias_or_name.identifier(False) + if cte_name == namespace or ( + not namespace and cte_name in direct_table_names + ): + if await cte.add_ref_column(self, ctx): + found.append(cte) + + # If nothing was found in the initial AST, traverse through dimensions graph + # to find another table in DJ that could be its origin + to_process = collections.deque([(table, []) for table in direct_tables]) + processed = set() + while to_process: + current_table, path = to_process.pop() + if current_table in processed: + continue + processed.add(current_table) + if ( + not namespace + or current_table.alias_or_name.identifier(False) == namespace + ): + if await current_table.add_ref_column(self, ctx): + found.append(current_table) + return found + + # If the table has a DJ node, check to see if the DJ node has dimensions, + # which would link us to new nodes to search for this column in + if isinstance(current_table, Table) and current_table.dj_node: + if not isinstance(current_table.dj_node, NodeRevision): + current_table.set_dj_node(current_table.dj_node.current) + for dj_col in current_table.dj_node.columns: + if dj_col.dimension: + col_dimension = await DJNodeRef.get_by_name( + ctx.session, + dj_col.dimension.name, + options=[ + joinedload(DJNodeRef.current).options( + selectinload(DJNode.columns), + ), + ], + ) + if col_dimension: + new_table = Table( + name=to_namespaced_name(col_dimension.name), + _dj_node=col_dimension.current, + path=path, + ) + new_table._columns = [ + Column( + name=Name(col.name), + _type=col.type, + _table=new_table, + ) + for col in col_dimension.current.columns + ] + to_process.append((new_table, path)) + await ctx.session.refresh(current_table.dj_node, ["dimension_links"]) + for link in current_table.dj_node.dimension_links: + all_roles = [] + if self.role: + all_roles = self.role.split(" -> ") + if (not link.role and not all_roles) or (link.role in all_roles): + await ctx.session.refresh(link, ["dimension"]) + if link.dimension: + await ctx.session.refresh(link.dimension, ["current"]) + new_table = Table( + name=to_namespaced_name(link.dimension.name), + _dj_node=link.dimension.current, + dimension_link=link, + path=path + [link], + ) + await ctx.session.refresh( + link.dimension.current, + ["columns"], + ) + new_table._columns = [ + Column( + name=Name(col.name), + _type=col.type, + _table=new_table, + ) + for col in link.dimension.current.columns + ] + to_process.append((new_table, path + [link])) + return found + + async def compile(self, ctx: CompileContext): + """ + Compile a column. + Determines the table from which a column is from. + """ + + if self.is_compiled(): + return + + # check if the column was already given a table + if self.table and isinstance(self.table.parent, Column): + await self.table.add_ref_column(self, ctx) + else: + found_sources = await self.find_table_sources(ctx) + if len(found_sources) < 1: + ctx.exception.errors.append( + DJError( + code=ErrorCode.INVALID_COLUMN, + message=f"Column `{self}` does not exist on any valid table.", + ), + ) + return + + if len(found_sources) > 1: + ctx.exception.errors.append( + DJError( + code=ErrorCode.INVALID_COLUMN, + message=f"Column `{self.name.name}` found in multiple tables. Consider using fully qualified name.", + ), + ) + return + + source_table = cast(TableExpression, found_sources[0]) + added = await source_table.add_ref_column(self, ctx) + if not added: + ctx.exception.errors.append( + DJError( + code=ErrorCode.INVALID_COLUMN, + message=f"Column `{self}` does not exist on any valid table.", + ), + ) + return + self._is_compiled = True + + @property + def struct_column_name(self) -> str: + """If this is a struct reference, the struct type's column name""" + column_namespace, column_name, subscript_name = self.column_names() + if len(self.namespace) == 1: # non-struct + return column_namespace + return column_name + + @property + def struct_subscript(self) -> str: + """If this is a struct reference, the struct type's field name""" + return self.name.name + + def __str__(self) -> str: + as_ = " AS " if self.as_ else " " + alias = "" if not self.alias else f"{as_}{self.alias}" + if self.table is not None and not isinstance(self.table, FunctionTable): + name = ( + self.struct_column_name + "." + self.struct_subscript + if self._is_struct_ref + else self.name.name + ) + ret = f"{self.name.quote_style}{name}{self.name.quote_style}" + if table_name := self.table.alias_or_name: + ret = ( + ( + table_name + if isinstance(table_name, str) + else table_name.identifier() + ) + + "." + + ret + ) + else: + ret = str(self.name) + if self.parenthesized: + ret = f"({ret})" + return ret + alias + + @property + def is_struct_ref(self): + return self._is_struct_ref + + +@dataclass(eq=False) +class Wildcard(Named, Expression): + """ + Wildcard or '*' expression + """ + + name: Name = field(init=False, repr=False, default=Name("*")) + _table: Optional["Table"] = field(repr=False, default=None) + + @property + def table(self) -> Optional["Table"]: + """ + Return the table the column was referenced from if there's one + """ + return self._table + + def add_table(self, table: "Table") -> "Wildcard": + """ + Add a referenced table + """ + if self._table is None: + self._table = table + return self + + def __str__(self) -> str: + return "*" + + @property + def type(self) -> ColumnType: + return WildcardType() + + +@dataclass(eq=False) +class TableExpression(Aliasable, Expression): + """ + A type for table expressions + """ + + column_list: List[Column] = field(default_factory=list) + _columns: List[Expression] = field( + default_factory=list, + ) # all those expressions that can be had from the table; usually derived from dj node metadata for Table + # ref (referenced) columns are columns used elsewhere from this table + _ref_columns: List[Column] = field(init=False, repr=False, default_factory=list) + + @property + def columns(self) -> List[Expression]: + """ + Return the columns named in this table + """ + col_list_names = {col.name.name for col in self.column_list} + return [ + col + for col in self._columns + if isinstance(col, (Aliasable, Named)) + and (col.alias_or_name.name in col_list_names if col_list_names else True) + ] + + @property + def column_mapping(self) -> Dict[str, Column]: + """ + Return the columns named in this table + """ + return { + col.alias_or_name.name: col + for col in self._columns + if isinstance(col, (Aliasable, Named)) + } + + @property + def ref_columns(self) -> Set[Column]: + """ + Return the columns referenced from this table + """ + return self._ref_columns + + async def add_ref_column( + self, + column: Column, + ctx: Optional[CompileContext] = None, + ) -> bool: + """ + Add column referenced from this table. Returns True if the table has the column + and False otherwise. + + This function handles the following cases: + + Regular columns. For example: + (1) non-aliased columns + `SELECT country_id AS country1 FROM countries` should match the `country_id` + column in the table `countries` + (2) aliased columns + `SELECT C.country_id AS country1 FROM countries C` should match the `country_id` + column in the table `countries` with the column namespace/table alias `C` + + Struct columns. For example: + (1) non-aliased struct columns + `countries` has column `identifiers` with type: + STRUCT + `SELECT identifiers.country_name AS name FROM countries` should match the + `identifier` -> `country_name` column in the table `countries` + (2) aliased struct columns + `countries` has column `identifiers` with type: + STRUCT + `SELECT C.identifiers.country_name AS name FROM countries C` should match the + `identifier` -> `country_name` column in the table `countries` with the column namespace/ + table alias `C` + """ + if not self._columns: + if ctx is None: + raise DJErrorException( + "Uncompiled Table expression requested to " + "add Column ref without a compilation context.", + ) + await self.compile(ctx) + return self.add_column_reference(column) + + def add_column_reference( + self, + column: Column, + ) -> bool: + """ + Add column referenced from this table. Returns True if the table has the column + and False otherwise. + """ + if not isinstance(column, Alias): + ref_col_name = column.name.name + if matching_column := self.column_mapping.get(ref_col_name): + self._ref_columns.append(column) + column.add_table(self) + column.add_expression(matching_column) + column.add_type(matching_column.type) + + # For table-valued functions, add the list of columns that gets + # returned as reference columns and compile them + if isinstance(self, FunctionTable): + if ( + not self.alias + and not column.name.namespace + or ( + self.alias + and column.name.namespace + and self.alias == column.name.namespace + ) + ): + for col in self.column_list: + if column.name.name == col.alias_or_name.name: + self._ref_columns.append(column) + column.add_table(self) + column.add_expression(col) + column.add_type(col.type) + return True + + for col in self.columns: + if isinstance(col, (Aliasable, Named)): + if ( + not column.alias + and not hasattr(column, "child") + and column.name.name == col.alias_or_name.name + ): + self._ref_columns.append(column) + column.add_table(self) + column.add_expression(col) + column.add_type(col.type) + return True + + # For struct types we can additionally check if there's a column that matches + # the search column's namespace and if there's a nested field that matches the + # search column's name + if ( + hasattr(col, "_type") + and col._type + and isinstance(col.type, StructType) + ) or (not hasattr(col, "_type") and isinstance(col.type, StructType)): + # struct column name + column_namespace = ".".join( + [name.name for name in column.namespace], + ) + subscript_name = column.name.name + column_name = column.name.name + if len(column.namespace) == 2: + column_namespace, column_name, _ = column.column_names() + + if col.alias_or_name.identifier(False) in ( + column_namespace, + column_name, + ): + for type_field in col.type.fields: + if type_field.name.name == subscript_name: + self._ref_columns.append(column) + column.set_struct_ref() + column.add_table(self) + column.add_expression(col) + column.add_type(type_field.type) + return True + return False + + def is_compiled(self) -> bool: + return bool(self._columns) or self._is_compiled + + def in_from_or_lateral(self) -> bool: + """ + Determines if the table expression is referenced in a From clause + """ + + if from_ := self.get_nearest_parent_of_type(From): + return from_.get_nearest_parent_of_type( + Select, + ) is self.get_nearest_parent_of_type(Select) + if lateral := self.get_nearest_parent_of_type(LateralView): + return lateral.get_nearest_parent_of_type( + Select, + ) is self.get_nearest_parent_of_type(Select) + return False + + +@dataclass(eq=False) +class Table(TableExpression, Named): + """ + A type for tables + """ + + _dj_node: Optional[DJNode] = field(repr=False, default=None) + dimension_link: Optional[DimensionLink] = field(repr=False, default=None) + path: Optional[List["Table"]] = field(repr=False, default=None) + + @property + def dj_node(self) -> Optional[DJNode]: + """ + Return the dj_node referenced by this table + """ + return self._dj_node + + def set_dj_node(self, dj_node: DJNode) -> "Table": + """ + Set dj_node referenced by this table + """ + self._dj_node = dj_node + return self + + def __str__(self) -> str: + table_str = str(self.name) + if self.alias: + as_ = " AS " if self.as_ else " " + table_str += f"{as_}{self.alias}" + return table_str + + def is_compiled(self) -> bool: + return super().is_compiled() and (self.dj_node is not None) + + def set_alias(self: TNode, alias: "Name") -> TNode: + self.alias = alias + for col in self._columns: + if col.table: + col.table.alias = self.alias + return self + + async def compile(self, ctx: CompileContext): + # things we can validate here: + # - if the node is a dimension in a groupby, is it joinable? + self._is_compiled = True + try: + if not self.dj_node: + dj_node = await get_dj_node( + ctx.session, + self.identifier(quotes=False), + {DJNodeType.SOURCE, DJNodeType.TRANSFORM, DJNodeType.DIMENSION}, + ) + self.set_dj_node(dj_node) + self._columns = [ + Column(Name(col.name), _type=col.type, _table=self) + for col in self.dj_node.columns + ] + except DJErrorException as exc: + ctx.exception.errors.append(exc.dj_error) + + +class Operation(Expression): + """ + A type to overarch types that operate on other expressions + """ + + +class UnaryOpKind(DJEnum): + """ + The accepted unary operations + """ + + Exists = "EXISTS" + Not = "NOT" + + def __str__(self): + return self.value + + +@dataclass(eq=False) +class UnaryOp(Operation): + """ + An operation that operates on a single expression + """ + + op: UnaryOpKind + expr: Expression + + def __str__(self) -> str: + ret = f"{self.op} {self.expr}" + if self.parenthesized: + return f"({ret})" + return ret + + @property + def type(self) -> ColumnType: + type_ = self.expr.type + + def raise_unop_exception(): + raise DJParseException( + f"Incompatible type in unary operation {self}. Got {type} in {self}.", + ) + + if self.op == UnaryOpKind.Not: + if isinstance(type_, BooleanType): + return type_ + raise_unop_exception() + if self.op == UnaryOpKind.Exists: + if isinstance(type_, BooleanType): + return type_ + raise_unop_exception() + + raise DJParseException(f"Unary operation {self.op} not supported!") + + +class BinaryOpKind(DJEnum): + """ + The DJ AST accepted binary operations + """ + + And = "AND" + LogicalAnd = "&&" + Or = "OR" + LogicalOr = "||" + Is = "IS" + Eq = "=" + NotEq = "<>" + NotEquals = "!=" + Gt = ">" + Lt = "<" + GtEq = ">=" + LtEq = "<=" + BitwiseOr = "|" + BitwiseAnd = "&" + BitwiseXor = "^" + Multiply = "*" + Divide = "/" + Plus = "+" + Minus = "-" + Modulo = "%" + NullSafeEq = "<=>" + + +@dataclass(eq=False) +class BinaryOp(Operation): + """ + Represents an operation that operates on two expressions + """ + + op: BinaryOpKind + left: Expression + right: Expression + use_alias_as_name: Optional[bool] = False + + @classmethod + def And( + cls, + left: Expression, + right: Optional[Expression] = None, + *rest: Expression, + ) -> Union["BinaryOp", Expression]: + """ + Create a BinaryOp of kind BinaryOpKind.Eq rolling up all expressions + """ + if right is None: # pragma: no cover + return left + return reduce( + lambda left, right: BinaryOp( + BinaryOpKind.And, + left, + right, + ), + (left, right, *rest), + ) + + @classmethod + def Eq( + cls, + left: Expression, + right: Optional[Expression], + use_alias_as_name: Optional[bool] = False, + ) -> Union["BinaryOp", Expression]: + """ + Create a BinaryOp of kind BinaryOpKind.Eq + """ + if right is None: # pragma: no cover + return left + return BinaryOp( + BinaryOpKind.Eq, + left, + right, + use_alias_as_name=use_alias_as_name, + ) + + def __str__(self) -> str: + left, right = self.left, self.right + if self.use_alias_as_name: + if isinstance(self.right, Column) and self.right.alias: + right = self.right.copy().use_alias_as_name() + if isinstance(self.left, Column) and self.left.alias: + left = self.left.copy().use_alias_as_name() + ret = f"{left} {self.op.value} {right}" + + if self.parenthesized: + return f"({ret})" + return ret + + @property + def type(self) -> ColumnType: + kind = self.op + left_type = self.left.type + right_type = self.right.type + + def raise_binop_exception(): + raise DJParseException( + "Incompatible types in binary operation " + f"{self}. Got left {left_type}, right {right_type}.", + ) + + numeric_types = { + type_: idx + for idx, type_ in enumerate( + [ + str(DoubleType()), + str(FloatType()), + str(BigIntType()), + str(IntegerType()), + ], + ) + } + + def resolve_numeric_types_binary_operations( + left: ColumnType, + right: ColumnType, + ): + if not left.is_compatible(right): + raise_binop_exception() + if str(left) in numeric_types and str(right) in numeric_types: + if str(left) == str(right): + return left + if numeric_types[str(left)] > numeric_types[str(right)]: + return right + return left + return left + + BINOP_TYPE_COMBO_LOOKUP: Dict[ + BinaryOpKind, + Callable[[ColumnType, ColumnType], ColumnType], + ] = { + BinaryOpKind.And: lambda left, right: BooleanType(), + BinaryOpKind.Or: lambda left, right: BooleanType(), + BinaryOpKind.Is: lambda left, right: BooleanType(), + BinaryOpKind.Eq: lambda left, right: BooleanType(), + BinaryOpKind.NotEq: lambda left, right: BooleanType(), + BinaryOpKind.NotEquals: lambda left, right: BooleanType(), + BinaryOpKind.Gt: lambda left, right: BooleanType(), + BinaryOpKind.Lt: lambda left, right: BooleanType(), + BinaryOpKind.GtEq: lambda left, right: BooleanType(), + BinaryOpKind.LtEq: lambda left, right: BooleanType(), + BinaryOpKind.BitwiseOr: lambda left, right: IntegerType() + if str(left) == str(IntegerType()) and str(right) == str(IntegerType()) + else raise_binop_exception(), + BinaryOpKind.BitwiseAnd: lambda left, right: IntegerType() + if str(left) == str(IntegerType()) and str(right) == str(IntegerType()) + else raise_binop_exception(), + BinaryOpKind.BitwiseXor: lambda left, right: IntegerType() + if str(left) == str(IntegerType()) and str(right) == str(IntegerType()) + else raise_binop_exception(), + BinaryOpKind.Multiply: resolve_numeric_types_binary_operations, + BinaryOpKind.Divide: resolve_numeric_types_binary_operations, + BinaryOpKind.Plus: resolve_numeric_types_binary_operations, + BinaryOpKind.Minus: resolve_numeric_types_binary_operations, + BinaryOpKind.Modulo: lambda left, right: IntegerType() + if str(left) == str(IntegerType()) and str(right) == str(IntegerType()) + else raise_binop_exception(), + } + return BINOP_TYPE_COMBO_LOOKUP[kind](left_type, right_type) + + async def compile(self, ctx: CompileContext): + """ + Compile a DJ Node. By default, we call compile on all immediate children of this node. + """ + if self._is_compiled: + return + for child in self.children: + if not child.is_compiled(): + await child.compile(ctx) + child._is_compiled = True + self._is_compiled = True + + +@dataclass(eq=False) +class FrameBound(Expression): + """ + Represents frame bound in a window function + """ + + start: str + stop: str + + def __str__(self) -> str: + return f"{self.start} {self.stop}" + + +@dataclass(eq=False) +class Frame(Expression): + """ + Represents frame in window function + """ + + frame_type: str + start: FrameBound + end: Optional[FrameBound] = None + + def __str__(self) -> str: + end = f" AND {self.end}" if self.end else "" + between = " BETWEEN" if self.end else "" + return f"{self.frame_type}{between} {self.start}{end}" + + +@dataclass(eq=False) +class Over(Expression): + """ + Represents a function used in a statement + """ + + partition_by: List[Expression] = field(default_factory=list) + order_by: List["SortItem"] = field(default_factory=list) + window_frame: Optional[Frame] = None + + def __str__(self) -> str: + partition_by = ( # pragma: no cover + " PARTITION BY " + ", ".join(str(exp) for exp in self.partition_by) + if self.partition_by + else "" + ) + order_by = ( + " ORDER BY " + ", ".join(str(exp) for exp in self.order_by) + if self.order_by + else "" + ) + consolidated_by = "\n".join( + po_by for po_by in (partition_by, order_by) if po_by + ) + window_frame = f" {self.window_frame}" if self.window_frame else "" + return f"OVER ({consolidated_by}{window_frame})" + + +class SetQuantifier(DJEnum): + """ + The accepted set quantifiers + """ + + All = "ALL" + Distinct = "DISTINCT" + + def __str__(self): + return self.value + + +@dataclass(eq=False) +class Function(Named, Operation): + """ + Represents a function used in a statement + """ + + args: List[Expression] = field(default_factory=list) + quantifier: SetQuantifier | None = None + over: Optional[Over] = None + args_compiled: bool = False + + def __new__( + cls, + name: Name, + args: List[Expression], + quantifier: str = "", + over: Optional[Over] = None, + ): + # Check if function is a table-valued function + if ( + hasattr(name, "name") + and not quantifier + and over is None + and name.name.upper() in table_function_registry + ): + return FunctionTable(name, args=args) + + # If not, create a new Function object + return super().__new__(cls) + + def __getnewargs__(self): + return self.name, self.args + + def __deepcopy__(self, memodict): + return self + + def __str__(self) -> str: + if self.name.name.upper() in function_registry and self.is_runtime(): + return self.function().substitute() + + over = f" {self.over} " if self.over else "" + quantifier = f" {self.quantifier} " if self.quantifier else "" + ret = ( + f"{self.name}({quantifier}{', '.join(str(arg) for arg in self.args)}){over}" + ) + if self.parenthesized: + ret = f"({ret})" + return ret + + def function(self): + return function_registry[self.name.name.upper()] + + def is_aggregation(self) -> bool: + if self.function().is_aggregation: + return True + return super().is_aggregation() + + def is_runtime(self) -> bool: + return self.function().is_runtime + + @property + def type(self) -> ColumnType: + return self.function().infer_type(*self.args) + + async def compile(self, ctx: CompileContext): + """ + Compile a function + """ + self._is_compiled = True + for arg in self.args: + if not arg.is_compiled(): + await arg.compile(ctx) + arg._is_compiled = True + + # FIXME: We currently catch this exception because we are unable + # to infer types for nested lambda functions. For the time being, an easy workaround is to + # add a CAST(...) wrapper around the nested lambda function so that the type hard-coded by + # the argument to CAST + try: + self.function().compile_lambda(*self.args) + except DJParseException as parse_exc: + if "Cannot resolve type of column" in parse_exc.message: + logger.warning(parse_exc) + else: + raise parse_exc + + for child in self.children: + if not child.is_compiled(): + await child.compile(ctx) + child._is_compiled = True + + +class Value(Expression): + """ + Base class for all values number, string, boolean + """ + + def is_aggregation(self) -> bool: + return False + + +@dataclass(eq=False) +class Null(Value): + """ + Null value + """ + + def __str__(self) -> str: + return "NULL" + + @property + def type(self) -> ColumnType: + return NullType() + + +@dataclass(eq=False) +class Number(Value): + """ + Number value + """ + + value: Union[float, int, decimal.Decimal] + _type: Optional[IntegerBase] = None + + def __post_init__(self): + super().__post_init__() + + if ( + not isinstance(self.value, float) + and not isinstance(self.value, int) + and not isinstance(self.value, decimal.Decimal) + ): + cast_exceptions = [] + numeric_types = [int, float, decimal.Decimal] + for cast_type in numeric_types: + try: + self.value = cast_type(self.value) + break + except (ValueError, OverflowError) as exception: + cast_exceptions.append(exception) + if len(cast_exceptions) >= len(numeric_types): + raise DJParseException(message="Not a valid number!") + + def __str__(self) -> str: + return str(self.value) + + @property + def type(self) -> ColumnType: + """ + Determine the type of the numeric expression. + """ + # We won't assume that anyone wants SHORT by default + if isinstance(self.value, int): + check_types = (self._type,) if self._type else (IntegerType(), BigIntType()) + for integer_type in check_types: + if integer_type.check_bounds(self.value): + return integer_type + + raise DJParseException( + f"No Integer type of {check_types} can hold the value {self.value}.", + ) + # + # # Arbitrary-precision floating point + # if isinstance(self.value, decimal.Decimal): + # return DecimalType.parse(self.value) + + # Double-precision floating point + if not (1.18e-38 <= abs(self.value) <= 3.4e38): + return DoubleType() + + # Single-precision floating point + return FloatType() + + +@dataclass(eq=False) +class String(Value): + """ + String value + """ + + value: str + + def __str__(self) -> str: + return self.value + + @property + def type(self) -> ColumnType: + return StringType() + + +@dataclass(eq=False) +class Boolean(Value): + """ + Boolean True/False value + """ + + value: bool + + def __str__(self) -> str: + return str(self.value) + + @property + def type(self) -> ColumnType: + return BooleanType() + + +@dataclass(eq=False) +class IntervalUnit(Value): + """ + Interval unit value + """ + + unit: str + value: Optional[Number] = None + + def __str__(self) -> str: + return f"{self.value or ''} {self.unit}" + + +@dataclass(eq=False) +class Interval(Value): + """ + Interval value + """ + + from_: List[IntervalUnit] + to: Optional[IntervalUnit] = None + + def __str__(self) -> str: + to = f"TO {self.to}" if self.to else "" + return f"INTERVAL {' '.join(str(interval) for interval in self.from_)} {to}" + + @property + def type(self) -> ColumnType: + """ + Determine the type of the interval expression. + """ + units = ["YEAR", "MONTH", "DAY", "HOUR", "MINUTE", "SECOND"] + years_months_units = {"YEAR", "MONTH"} + days_seconds_units = {"DAY", "HOUR", "MINUTE", "SECOND"} + from_ = [DateTimeBase.Unit(f.unit) for f in self.from_] + if all(unit.unit in years_months_units for unit in self.from_): + if ( + self.to is None + or self.to.unit is None + or self.to.unit in years_months_units + ): + # If all the units in the from_ list are YEAR or MONTH, the interval + # is a YearMonthInterval + return YearMonthIntervalType( + sorted(from_, key=lambda u: units.index(u))[0], + self.to, + ) + elif all(unit.unit in days_seconds_units for unit in self.from_): + if ( + self.to is None + or self.to.unit is None + or self.to.unit in days_seconds_units + ): + # If the to_ attribute is None or its unit is DAY, HOUR, MINUTE, or + # SECOND, the interval is a DayTimeInterval + return DayTimeIntervalType( + sorted(from_, key=lambda u: units.index(u))[0], + self.to, + ) + raise DJParseException(f"Invalid interval type specified in {self}.") + + +@dataclass(eq=False) +class Struct(Value): + """ + Struct value + """ + + values: List[Aliasable] + + def __str__(self): + inner = ", ".join(str(value) for value in self.values) + return f"STRUCT({inner})" + + +@dataclass(eq=False) +class Predicate(Operation): + """ + Represents a predicate + """ + + negated: bool = False + + @property + def type(self) -> ColumnType: + return BooleanType() + + +@dataclass(eq=False) +class Between(Predicate): + """ + A between statement + """ + + expr: Expression = field(default_factory=Expression) + low: Expression = field(default_factory=Expression) + high: Expression = field(default_factory=Expression) + + def __str__(self) -> str: + not_ = "NOT " if self.negated else "" + between = f"{not_}{self.expr} BETWEEN {self.low} AND {self.high}" + if self.parenthesized: + between = f"({between})" + return between + + @property + def type(self) -> ColumnType: + expr_type = self.expr.type + low_type = self.low.type + high_type = self.high.type + if expr_type == low_type == high_type: + return BooleanType() + raise DJParseException( + f"BETWEEN expects all elements to have the same type got " + f"{expr_type} BETWEEN {low_type} AND {high_type} in {self}.", + ) + + +@dataclass(eq=False) +class In(Predicate): + """ + An in expression + """ + + expr: Expression = field(default_factory=Expression) + source: Union[List[Expression], "Select"] = field(default_factory=Expression) + + def __str__(self) -> str: + not_ = "NOT " if self.negated else "" + source = ( + str(self.source) + if isinstance(self.source, Select) + else "(" + ", ".join(str(exp) for exp in self.source) + ")" + ) + return f"{self.expr} {not_}IN {source}" + + +@dataclass(eq=False) +class Rlike(Predicate): + """ + A regular expression match statement + """ + + expr: Expression = field(default_factory=Expression) + pattern: Expression = field(default_factory=Expression) + + def __str__(self) -> str: + not_ = "NOT " if self.negated else "" + return f"{not_}{self.expr} RLIKE {self.pattern}" + + +@dataclass(eq=False) +class Like(Predicate): + """ + A string pattern matching statement + """ + + expr: Expression = field(default_factory=Expression) + quantifier: str = "" + patterns: List[Expression] = field(default_factory=list) + escape_char: Optional[str] = None + case_sensitive: Optional[bool] = True + + def __str__(self) -> str: + not_ = "NOT " if self.negated else "" + if self.quantifier: # quantifier means a pattern with multiple elements + pattern = f"({', '.join(str(p) for p in self.patterns)})" + else: + pattern = self.patterns + escape_char = f" ESCAPE '{self.escape_char}'" if self.escape_char else "" + quantifier = f"{self.quantifier} " if self.quantifier else "" + like_type = "LIKE" if self.case_sensitive else "ILIKE" + return f"{not_}{self.expr} {like_type} {quantifier}{pattern}{escape_char}" + + @property + def type(self) -> ColumnType: + expr_type = self.expr.type + if expr_type == StringType(): + return BooleanType() + raise DJParseException( + f"Incompatible type for {self}: {expr_type}. Expected STR", + ) + + +@dataclass(eq=False) +class IsNull(Predicate): + """ + A null check statement + """ + + expr: Expression = field(default_factory=Expression) + + def __str__(self) -> str: + not_ = "NOT " if self.negated else "" + return f"{self.expr} IS {not_}NULL" + + @property + def type(self) -> ColumnType: + return BooleanType() + + +@dataclass(eq=False) +class IsBoolean(Predicate): + """ + A boolean check statement + """ + + expr: Expression = field(default_factory=Expression) + value: str = "UNKNOWN" + + def __str__(self) -> str: + not_ = "NOT " if self.negated else "" + return f"{self.expr} IS {not_}{self.value}" + + @property + def type(self) -> ColumnType: + return BooleanType() + + +@dataclass(eq=False) +class IsDistinctFrom(Predicate): + """ + A distinct from check statement + """ + + expr: Expression = field(default_factory=Expression) + right: Expression = field(default_factory=Expression) + + def __str__(self) -> str: + not_ = "NOT " if self.negated else "" + return f"{self.expr} IS {not_}DISTINCT FROM {self.right}" + + @property + def type(self) -> ColumnType: + return BooleanType() + + +@dataclass(eq=False) +class Case(Expression): + """ + A case statement of branches + """ + + expr: Optional[Expression] = None + conditions: List[Expression] = field(default_factory=list) + else_result: Optional[Expression] = None + operand: Optional[Expression] = None + results: List[Expression] = field(default_factory=list) + + def __str__(self) -> str: + branches = "\n\tWHEN ".join( + f"{(cond)} THEN {(result)}" + for cond, result in zip(self.conditions, self.results) + ) + else_ = f"ELSE {(self.else_result)}" if self.else_result else "" + expr = "" if self.expr is None else f" {self.expr} " + ret = f"""CASE {expr} + WHEN {branches} + {else_} + END""" + if self.parenthesized: + return f"({ret})" + return ret + + def is_aggregation(self) -> bool: + return all(result.is_aggregation() for result in self.results) and ( + self.else_result.is_aggregation() if self.else_result else True + ) + + @property + def type(self) -> ColumnType: + result_types = [ + res.type + for res in self.results + ([self.else_result] if self.else_result else []) + if res.type + ] + if not all(result_types[0].is_compatible(res) for res in result_types): + raise DJParseException( + f"Not all the same type in CASE! Found: {', '.join([str(type_) for type_ in result_types])}", + ) + return result_types[0] + + +@dataclass(eq=False) +class Subscript(Expression): + """ + Represents a subscript expression + """ + + expr: Expression + index: Expression + + def __str__(self) -> str: + return f"{self.expr}[{self.index}]" + + @property + def type(self) -> ColumnType: + if isinstance(self.expr.type, MapType): + type_ = cast(MapType, self.expr.type) + return type_.value.type + if isinstance(self.expr.type, StructType): + nested_field = self.expr.type.fields_mapping.get( + self.index.value.replace("'", ""), + ) + return nested_field.type + return cast(ListType, self.expr.type).element.type + + +@dataclass(eq=False) +class Lambda(Expression): + """ + Represents a lambda expression + """ + + identifiers: List[Named] + expr: Expression + + def __str__(self) -> str: + if len(self.identifiers) == 1: + id_str = self.identifiers[0] + else: + id_str = "(" + ", ".join(str(iden) for iden in self.identifiers) + ")" + return f"{id_str} -> {self.expr}" + + @property + def type(self) -> Union[ColumnType, List[ColumnType]]: + """ + The return type of the lambda function + """ + return self.expr.type + + +@dataclass(eq=False) +class JoinCriteria(Node): + """ + Represents the criteria for a join relation in a FROM clause + """ + + on: Optional[Expression] = None + using: Optional[List[Named]] = None + + def __str__(self) -> str: + if self.on: + return f"ON {self.on}" + else: + id_list = ", ".join(str(iden) for iden in self.using) + return f"USING ({id_list})" + + +@dataclass(eq=False) +class Join(Node): + """ + Represents a join relation in a FROM clause + """ + + join_type: str + right: Expression + criteria: Optional[JoinCriteria] = None + lateral: bool = False + natural: bool = False + + def __str__(self) -> str: + parts = [] + if self.natural: + parts.append("NATURAL") + if self.join_type: + parts.append(f"{str(self.join_type).upper().strip()}") + parts.append("JOIN") + if self.lateral: + parts.append("LATERAL") + parts.append(str(self.right)) + if self.criteria: + parts.append(f"{self.criteria}") + return " ".join(parts) + + +@dataclass(eq=False) +class InlineTable(TableExpression, Named): + """ + An inline table + """ + + values: List[Expression] = field(default_factory=list) + explicit_columns: bool = False + + def __str__(self) -> str: + values = "VALUES " + ",\n\t".join( + [f"({', '.join([str(col) for col in row])})" for row in self.values], + ) + inline_alias = self.alias_or_name.name if self.alias_or_name else "" + alias = inline_alias + ( + f"({', '.join([col.alias_or_name.name for col in self.columns])})" + if self.explicit_columns + else "" + ) + return f"{values} AS {alias}" if alias else values + + +@dataclass(eq=False) +class FunctionTableExpression(TableExpression, Named, Operation): + """ + An uninitializable Type for FunctionTable for use as a + default where a FunctionTable is required but succeeds optional fields + """ + + args: List[Expression] = field(default_factory=list) + + +class FunctionTable(FunctionTableExpression): + """ + Represents a table-valued function used in a statement + """ + + def __str__(self) -> str: + alias = f" {self.alias}" if self.alias else "" + as_ = " AS " if self.as_ else "" + cols = ( + f" {', '.join(col.name.name for col in self.column_list)}" + if self.column_list + else "" + ) + + column_parens = False + if self.name.name.upper() == "UNNEST" or ( + self.name.name.upper() == "EXPLODE" + and not isinstance(self.parent, LateralView) + ): + column_parens = True + + column_list_str = f"({cols})" if column_parens else f"{cols}" + args_str = f"({', '.join(str(col) for col in self.args)})" if self.args else "" + return f"{self.name}{args_str}{alias}{as_}{column_list_str}" + + def set_alias(self: TNode, alias: Name) -> TNode: + self.alias = alias + return self + + async def _type(self, ctx: Optional[CompileContext] = None) -> List[NestedField]: + name = self.name.name.upper() + dj_func = table_function_registry[name] + arg_types = [] + for arg in self.args: + if ctx: + await arg.compile(ctx) + arg_types.append(arg.type) + return dj_func.infer_type(*arg_types) + + async def compile(self, ctx): + if self.is_compiled(): + return + self._is_compiled = True + types = await self._type(ctx) + for type, col in zip_longest(types, self.column_list): + if self.column_list: + if (type is None) or (col is None): + ctx.exception.errors.append( + DJError( + code=ErrorCode.INVALID_SQL_QUERY, + message=( + "Found different number of columns than types" + f" in {self}." + ), + context=str(self), + ), + ) + break + else: + col = Column(type.name) + + col.add_type(type.type) + self._columns.append(col) + + +@dataclass(eq=False) +class LateralView(Node): + """ + Represents a lateral view expression + """ + + outer: bool = False + func: FunctionTableExpression = field(default_factory=FunctionTableExpression) + + def __str__(self) -> str: + parts = ["LATERAL VIEW"] + if self.outer: + parts.append(" OUTER") + parts.append(f" {self.func}") + return "".join(parts) + + +@dataclass(eq=False) +class Relation(Node): + """ + Represents a relation + """ + + primary: Expression + extensions: List[Join] = field(default_factory=list) + + def __str__(self) -> str: + if self.extensions: + extensions = " " + "\n".join([str(ext) for ext in self.extensions]) + else: + extensions = "" + return f"{self.primary}{extensions}" + + +@dataclass(eq=False) +class From(Node): + """ + Represents the FROM clause of a SELECT statement + """ + + relations: List[Relation] = field(default_factory=list) + + def __str__(self) -> str: + parts = ["FROM "] + parts += ",\n".join([str(r) for r in self.relations]) + + return "".join(parts) + + @classmethod + def Table(cls, table_name: str): + """ + Create a FROM clause sourcing from this table name + """ + return From( + relations=[ + Relation( + primary=Table( + Name(table_name), + ), + ), + ], + ) + + +@dataclass(eq=False) +class SetOp(Node): + """ + A set operation + """ + + kind: str = "" # Union, intersect, ... + right: Optional["SelectExpression"] = None + + def __str__(self) -> str: + return f"\n{self.kind}\n{self.right}" + + +@dataclass(eq=False) +class Cast(Expression): + """ + A cast to a specified type + """ + + data_type: ColumnType + expression: Expression + + def __str__(self) -> str: + return f"CAST({self.expression} AS {str(self.data_type).upper()})" + + @property + def type(self) -> ColumnType: + """ + Return the type of the expression + """ + return self.data_type + + async def compile(self, ctx: CompileContext): + """ + In most cases we can short-circuit the CAST expression's compilation, since the output + type is determined directly by `data_type`, so evaluating the expression is unnecessary. + """ + for child in self.find_all(Column): + await child.compile(ctx) + + if self.data_type: + return + + +@dataclass(eq=False) +class SortItem(Node): + """ + Defines a sort item of an expression + """ + + expr: Expression + asc: str + nulls: str + + def __str__(self) -> str: + return f"{self.expr} {self.asc} {self.nulls}".strip() + + +@dataclass(eq=False) +class Organization(Node): + """ + Sets up organization for the query + """ + + order: List[SortItem] = field(default_factory=list) + sort: List[SortItem] = field(default_factory=list) + + def __str__(self) -> str: + ret = "" + ret += f"ORDER BY {', '.join(str(i) for i in self.order)}" if self.order else "" + if ret: + ret += "\n" + ret += f"SORT BY {', '.join(str(i) for i in self.sort)}" if self.sort else "" + return ret + + +@dataclass(eq=False) +class Hint(Node): + """ + An Spark SQL hint statement + """ + + name: Name + parameters: List[Column] = field(default_factory=list) + + def __str__(self) -> str: + params = ( + f"({', '.join(str(param) for param in self.parameters)})" + if self.parameters + else "" + ) + return f"{self.name}{params}" + + +@dataclass(eq=False) +class SelectExpression(Aliasable, Expression): + """ + An uninitializable Type for Select for use as a default where + a Select is required. + """ + + quantifier: str = "" # Distinct, All + projection: List[Union[Aliasable, Expression]] = field(default_factory=list) + from_: Optional[From] = None + group_by: List[Expression] = field(default_factory=list) + having: Optional[Expression] = None + where: Optional[Expression] = None + lateral_views: List[LateralView] = field(default_factory=list) + set_op: Optional[SetOp] = None + limit: Optional[Expression] = None + organization: Optional[Organization] = None + hints: Optional[List[Hint]] = None + + def add_set_op(self, set_op: SetOp): + if self.set_op: + self.set_op.right.add_set_op(set_op) + else: + self.set_op = set_op + + def add_aliases_to_unnamed_columns(self) -> None: + """ + Add an alias to any unnamed columns in the projection (`col{n}`) + """ + projection = [] + for i, expression in enumerate(self.projection): + if not isinstance(expression, Aliasable): + name = f"col{i}" + projection.append(expression.set_alias(Name(name))) + else: + projection.append(expression) + self.projection = projection + + def where_clause_expressions_list(self) -> Optional[List[Expression]]: + """ + Converts the WHERE clause to a list of expressions separated by AND operators + """ + if not self.where: + return self.where + + filters = [] + processing = collections.deque([self.where]) + while processing: + current_clause = processing.pop() + if current_clause: + if ( + isinstance(current_clause, BinaryOp) + and current_clause.op == BinaryOpKind.And + ): + processing.append(current_clause.left) + processing.append(current_clause.right) + else: + filters.append(current_clause) + return filters + + @property + def column_mapping(self) -> Dict[str, "Column"]: + """ + Returns a dictionary with the output column names mapped to the columns + """ + return {col.alias_or_name.name: col for col in self.projection} + + +class Select(SelectExpression): + """ + A single select statement type + """ + + def __str__(self) -> str: + parts = ["SELECT "] + if self.hints: + parts.append(f"/*+ {', '.join(str(hint) for hint in self.hints)} */\n") + if self.quantifier: + parts.append(f"{self.quantifier}\n") + parts.append(",\n\t".join(str(exp) for exp in self.projection)) + if self.from_ is not None: + parts.extend(("\n", str(self.from_), "\n")) + for view in self.lateral_views: + parts.append(f"\n{view}") + if self.where is not None: + parts.extend(("WHERE ", str(self.where), "\n")) + if self.group_by: + parts.extend(("GROUP BY ", ", ".join(str(exp) for exp in self.group_by))) + if self.having is not None: + parts.extend(("HAVING ", str(self.having), "\n")) + select = " ".join(parts).strip() + if self.parenthesized: + select = f"({select})" + if self.set_op: + select += f"\n{self.set_op}" + + if self.organization: + select += f"\n{self.organization}" + if self.limit: + select += f"\nLIMIT {self.limit}" + + if self.alias: + as_ = " AS " if self.as_ else " " + return f"{select}{as_}{self.alias}" + if isinstance(self.parent, Alias): + if self.set_op: + return f"({select})" + return select + + @property + def type(self) -> ColumnType: + if len(self.projection) != 1: + raise DJParseException( + "Can only infer type of a SELECT when it " + f"has a single expression in its projection. In {self}.", + ) + return self.projection[0].type + + async def compile(self, ctx: CompileContext): + if not self.group_by and self.having: + ctx.exception.errors.append( + DJError( + code=ErrorCode.INVALID_SQL_QUERY, + message=( + "HAVING without a GROUP BY is not allowed. " + "Did you want to use a WHERE clause instead?" + ), + context=str(self), + ), + ) + await super().compile(ctx) + + +@dataclass(eq=False) +class Query(TableExpression, UnNamed): + """ + Overarching query type + """ + + select: SelectExpression = field(default_factory=SelectExpression) + ctes: List["Query"] = field(default_factory=list) + + def is_compiled(self) -> bool: + return not any( + self.filter(lambda node: node is not self and not node.is_compiled()), + ) + + async def compile(self, ctx: CompileContext): + if self._is_compiled: + return + + def _compile(info: Tuple[Column, List[TableExpression]]): + """ + Given a list of table sources, find a matching origin table for the column. + """ + col, table_options = info + matching_origin_tables = 0 + for option in table_options: + namespace = col.namespace[0].name if col.namespace else None + table_alias = option.alias.name if option.alias else None + if namespace is None or namespace == table_alias: + result = option.add_column_reference(col) + if result: + matching_origin_tables += 1 + col._is_compiled = True + if matching_origin_tables > 1: + ctx.exception.errors.append( + DJError( + code=ErrorCode.INVALID_COLUMN, + message=f"Column `{col.name.name}` found in multiple tables." + " Consider using fully qualified name.", + ), + ) + + # Work backwards from the table expressions on the query's SELECT clause + # and assign references between the columns and the tables + nearest_query = self.get_nearest_parent_of_type(Query) + cte_mapping = { + cte.alias_or_name.name: cte + for cte in (nearest_query.ctes if nearest_query else []) + } + table_options = ( + [ + tbl + for tbl in self.select.from_.find_all(TableExpression) + if tbl.get_nearest_parent_of_type(Query) is self + ] + if self.select.from_ + else [] + ) + if table_options: + for idx, option in enumerate(table_options): + if isinstance(option, Table): + if option.name.name in cte_mapping: + table_options[idx] = cte_mapping[option.name.name] + await table_options[idx].compile(ctx) + + expressions_to_compile = [ + self.select.projection, + self.select.group_by, + self.select.having, + self.select.where, + self.select.organization, + ] + columns_to_compile = [] + for expression in expressions_to_compile: + if expression and not isinstance(expression, list): + columns_to_compile += list(expression.find_all(Column)) + if isinstance(expression, list): + columns_to_compile += [ + col for expr in expression for col in expr.find_all(Column) + ] + + with ThreadPoolExecutor() as executor: + list( + executor.map( + _compile, + [(col, table_options) for col in columns_to_compile], + ), + ) + + for child in self.children: + if child is not self and not child.is_compiled(): + await child.compile(ctx) + + for expr in self.select.projection: + self._columns += expr.columns + self._is_compiled = True + + def bake_ctes(self) -> "Query": + """ + Add ctes into the select and return the select + + Note: This destroys the structure of the query which cannot be undone + you may want to deepcopy it first + """ + + for cte in self.ctes: + for tbl in self.filter( + lambda node: isinstance(node, Table) + and node.identifier(False) == cte.alias_or_name.identifier(False), + ): + tbl.swap(cte) + self.ctes = [] + return self + + def to_cte(self, cte_name: Name, parent_ast: Optional["Query"] = None) -> "Query": + """ + Prepares the query to be a CTE + """ + self.alias = cte_name + self.parenthesized = True + self.as_ = True + if parent_ast: + self.set_parent(parent_ast, "ctes") + return self + + def __str__(self) -> str: + is_cte = self.parent is not None and self.parent_key == "ctes" + ctes = ",\n".join(str(cte) for cte in self.ctes) + if ctes: + ctes += "\n\n" + with_ = f"WITH\n{ctes}" if ctes else "" + + parts = [f"{with_}{self.select}\n"] + query = "".join(parts) + newline = "\n" if is_cte else "" + if self.parenthesized: + query = f"({newline}{query.strip()}{newline})" + if self.alias: + as_ = " AS " if self.as_ else " " + if is_cte: + query = f"{self.alias}{as_}{query}" + else: + query = f"{query}{as_}{self.alias}" + return query + + def set_alias(self: TNode, alias: "Name") -> TNode: + self.alias = alias + for col in self._columns: + if isinstance(col, Column) and col.table is not None: + col.table.alias = self.alias + return self + + async def extract_dependencies( + self, + context: Optional[CompileContext] = None, + ) -> Tuple[Dict[NodeRevision, List[Table]], Dict[str, List[Table]]]: + """ + Find all dependencies in a compiled query + """ + + if not self.is_compiled(): + if not context: + raise DJQueryBuildException( + "Context not provided for query compilation!", + ) + await self.compile(context) + + deps: Dict[NodeRevision, List[Table]] = {} + danglers: Dict[str, List[Table]] = {} + for table in self.find_all(Table): + if node := table.dj_node: + deps[node] = deps.get(node, []) + deps[node].append(table) + else: + name = table.identifier(quotes=False) + danglers[name] = danglers.get(name, []) + danglers[name].append(table) + + return deps, danglers + + @property + def type(self) -> ColumnType: + return self.select.type diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/__init__.py b/datajunction-server/datajunction_server/sql/parsing/backends/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/antlr4.py b/datajunction-server/datajunction_server/sql/parsing/backends/antlr4.py new file mode 100644 index 000000000..7616c600a --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/backends/antlr4.py @@ -0,0 +1,1221 @@ +# mypy: ignore-errors +import copy +import inspect +import logging +from functools import lru_cache +from typing import TYPE_CHECKING, List, Optional, Tuple, Union, cast + +import antlr4 +from antlr4 import InputStream, RecognitionException +from antlr4.error.ErrorListener import ErrorListener +from antlr4.error.Errors import ParseCancellationException +from antlr4.error.ErrorStrategy import BailErrorStrategy + +import datajunction_server.sql.parsing.types as ct +from datajunction_server.sql.parsing import ast +from datajunction_server.sql.parsing.ast import UnaryOpKind +from datajunction_server.sql.parsing.backends.exceptions import DJParseException +from datajunction_server.sql.parsing.backends.grammar.generated.SqlBaseLexer import ( + SqlBaseLexer, +) +from datajunction_server.sql.parsing.backends.grammar.generated.SqlBaseParser import ( + SqlBaseParser, +) +from datajunction_server.sql.parsing.backends.grammar.generated.SqlBaseParser import ( + SqlBaseParser as sbp, +) + +if TYPE_CHECKING: + from datajunction_server.sql.parsing.types import ColumnType + +logger = logging.getLogger(__name__) + + +class RemoveIdentifierBackticks(antlr4.ParseTreeListener): + @staticmethod + def exitQuotedIdentifier(ctx): + def identity(token): + return token + + return identity + + @staticmethod + def enterNonReserved(ctx): + def add_backtick(token): + return "`{0}`".format(token) + + return add_backtick + + +class ParseErrorListener(ErrorListener): + def syntaxError( + self, + recognizer, + offendingSymbol, + line, + column, + msg, + e, + ): + raise SqlSyntaxError(f"Parse error {line}:{column}:", msg) + + +class UpperCaseCharStream: + """ + Make SQL token detection case insensitive and allow identifier without + backticks to be seen as e.g. column names + """ + + def __init__(self, wrapped): + self.wrapped = wrapped + + def getText(self, interval, *args): + if args or (self.size() > 0 and (interval.b - interval.a >= 0)): + return self.wrapped.getText(interval, *args) + return "" + + def LA(self, i: int): + token = self.wrapped.LA(i) + if token in (0, -1): + return token + return ord(chr(token).upper()) + + def __getattr__(self, item): + return getattr(self.wrapped, item) + + +class ExplicitBailErrorStrategy(BailErrorStrategy): + """ + Bail Error Strategy throws a ParseCancellationException, + This strategy simply throw a more explicit exception + """ + + def recover(self, recognizer, e: RecognitionException): + try: + super(ExplicitBailErrorStrategy, self).recover(recognizer, e) + except ParseCancellationException: + raise SqlParsingError from e + + +class EarlyBailSqlLexer(SqlBaseLexer): + def recover(self, recognition_exc: RecognitionException): + raise SqlLexicalError from recognition_exc + + +def build_parser(stream, strict_mode=False, early_bail=True): + if not strict_mode: + stream = UpperCaseCharStream(stream) + if early_bail: + lexer = EarlyBailSqlLexer(stream) + else: + lexer = SqlBaseLexer(stream) + lexer.removeErrorListeners() + lexer.addErrorListener(ParseErrorListener()) + token_stream = antlr4.CommonTokenStream(lexer) + parser = SqlBaseParser(token_stream) + parser.addParseListener(RemoveIdentifierBackticks()) + parser.removeErrorListeners() + parser.addErrorListener(ParseErrorListener()) + if early_bail: + parser._errHandler = ExplicitBailErrorStrategy() + return parser + + +class SqlParsingError(Exception): + pass + + +class SqlLexicalError(SqlParsingError): + pass + + +class SqlSyntaxError(SqlParsingError): + pass + + +def string_to_ast(string, rule, *, strict_mode=False, debug=False, early_bail=False): + parser = build_string_parser(string, strict_mode, early_bail) + tree = getattr(parser, rule)() + if debug: + print_tree(tree, printer=logger.warning) + return tree + + +def build_string_parser(string, strict_mode=False, early_bail=True): + string_as_stream = InputStream(string) + parser = build_parser(string_as_stream, strict_mode, early_bail) + return parser + + +def parse_sql(string, rule, converter=None, debug=False): + tree = string_to_ast(string, rule, debug=debug) + return converter(tree) if converter else tree + + +def parse_statement(string, converter=None, debug=False): + return parse_sql(string, "singleStatement", converter, debug) + + +def print_tree(tree, printer=print): + for line in tree_to_strings(tree, indent=0): + printer(line) + + +def tree_to_strings(tree, indent=0): + symbol = ("[" + tree.symbol.text + "]") if hasattr(tree, "symbol") else "" + node_as_string = type(tree).__name__ + symbol + result = ["|" + "-" * indent + node_as_string] + if hasattr(tree, "children") and tree.children: + for child in tree.children: + result += tree_to_strings(child, indent + 1) + return result + + +def parse_rule(sql: str, rule: str) -> Union[ast.Node, "ColumnType"]: + """ + Parse a string into a DJ ast using the ANTLR4 backend. + """ + antlr_tree = parse_sql(sql, rule) + ast_tree = visit(antlr_tree) + return ast_tree + + +@lru_cache(maxsize=128) +def _cached_parse(sql: Optional[str]) -> ast.Query: + """ + Parse a string sql query into a DJ query AST and cache it. + """ + return parse(sql) + + +def parse(sql: Optional[str]) -> ast.Query: + """ + Parse a string sql query into a DJ ast Query + """ + if not sql: + raise DJParseException("Empty query provided!") + try: + return cast(ast.Query, parse_rule(sql, "singleStatement")) + except SqlParsingError as exc: + raise DJParseException(message=f"Error parsing SQL `{sql}`: {exc}") from exc + + +def cached_parse(sql: Optional[str]) -> ast.Query: + """ + Parse a string sql query into a DJ ast Query + """ + return copy.deepcopy(_cached_parse(sql)) + + +TERMINAL_NODE = antlr4.tree.Tree.TerminalNodeImpl + + +class Visitor: + def __init__(self): + self.registry = {} + + def register(self, func): + params = inspect.signature(func).parameters + type_ = params[list(params.keys())[0]].annotation + if type_ == inspect.Parameter.empty: + raise ValueError( + "No type annotation found for the first parameter of the visitor.", + ) + if type_ in self.registry: + raise ValueError( + f"A visitor is already registered for type {type_.__name__}.", + ) + self.registry[type_] = func + return func + + def __call__(self, ctx): + if type(ctx) == TERMINAL_NODE: + return None + func = self.registry.get(type(ctx), None) + if func is None: + line, col = ctx.start.line, ctx.start.column + raise TypeError( + f"{line}:{col} No visitor registered for type {type(ctx).__name__}", + ) + result = func(ctx) + + if result is None: + line, col = ctx.start.line, ctx.start.column + raise DJParseException(f"{line}:{col} Could not parse {ctx.getText()}") + if ( + hasattr(result, "parenthesized") + and result.parenthesized is None + and hasattr(ctx, "LEFT_PAREN") + ): + if text := ctx.getText(): + text = text.strip() + if (text[0] == "(") and (text[-1] == ")"): + result.parenthesized = True + if ( + hasattr(ctx, "AS") + and ctx.AS() + and hasattr(result, "as_") + and result.as_ is None + ): + result = result.set_as(True) + return result + + +visit = Visitor() + + +@visit.register +def _(ctx: list, nones=False): + return list( + filter( + lambda child: child is not None if nones is False else True, + map(visit, ctx), + ), + ) + + +@visit.register +def _(ctx: sbp.Any_valueContext): + return ast.Function(ast.Name("ANY_VALUE"), args=[visit(ctx.expression())]) + + +@visit.register +def _(ctx: sbp.SingleStatementContext): + return visit(ctx.statement()) + + +@visit.register +def _(ctx: sbp.StatementDefaultContext): + return visit(ctx.query()) + + +@visit.register +def _(ctx: sbp.InlineTableDefault1Context): + return visit(ctx.inlineTable()) + + +@visit.register +def _(ctx: sbp.InlineTableDefault2Context): + return visit(ctx.inlineTable()) + + +@visit.register +def _(ctx: sbp.RowConstructorContext): + namedExpr = visit(ctx.namedExpression()) + return namedExpr + + +@visit.register +def _(ctx: sbp.InlineTableContext): + args = visit(ctx.expression()) + alias, columns = visit(ctx.tableAlias()) + + # Generate default column aliases if they weren't specified + col_args = args[0] if isinstance(args[0], list) else args + inline_table_columns = ( + [ast.Column(col, _type=value.type) for col, value in zip(columns, col_args)] + if columns + else [ + ast.Column(ast.Name(f"col{idx + 1}"), _type=value.type) + for idx, value in enumerate(col_args) + ] + ) + return ast.InlineTable( + name=alias, + _columns=inline_table_columns, + explicit_columns=len(columns) > 0, + values=[[value] if not isinstance(value, list) else value for value in args], + ) + + +@visit.register +def _(ctx: sbp.QueryContext): + ctes = [] + if ctes_ctx := ctx.ctes(): + ctes = visit(ctes_ctx) + limit, organization = visit(ctx.queryOrganization()) + select = visit(ctx.queryTerm()) + select.limit = limit + select.organization = organization + return ast.Query(ctes=ctes, select=select) + + +@visit.register +def _(ctx: sbp.QueryOrganizationContext): + order = visit(ctx.order) + sort = visit(ctx.sort) + org = ast.Organization(order, sort) + limit = None + if ctx.limit: + limit = visit(ctx.limit) + return limit, org + + +@visit.register +def _(ctx: sbp.SortItemContext): + expr = visit(ctx.expression()) + order = "" + if ordering := ctx.ordering: + order = ordering.text.upper() + nulls = "" + if null_order := ctx.nullOrder: + nulls = "NULLS " + null_order.text + return ast.SortItem(expr, order, nulls) + + +@visit.register +def _(ctx: sbp.ExpressionContext): + return visit(ctx.booleanExpression()) + + +@visit.register +def _(ctx: sbp.BooleanLiteralContext): + boolean_value_mapping = {"true": True, "false": False} + boolean_value = ctx.booleanValue().getText().lower() + if boolean_value in boolean_value_mapping: + return ast.Boolean(boolean_value_mapping[boolean_value]) + raise DJParseException(f"Invalid boolean value {boolean_value}!") + + +@visit.register +def _(ctx: sbp.BooleanValueContext): + return ast.Boolean(visit(ctx.getText())) + + +@visit.register +def _(ctx: sbp.PredicatedContext): + if value_expr := ctx.valueExpression(): + if not ctx.predicate(): + return visit(value_expr) + if predicate_ctx := ctx.predicate(): + return visit(predicate_ctx) + + +@visit.register +def _(ctx: sbp.PredicateContext) -> ast.Predicate: + negated = True if ctx.NOT() else False + any = True if ctx.ANY() else False + all = True if ctx.ALL() else False + some = True if ctx.SOME() else False + expr = visit(ctx.parentCtx.valueExpression()) + if ctx.BETWEEN(): + low = visit(ctx.lower) + high = visit(ctx.upper) + return ast.Between(negated, expr, low, high) + if ctx.DISTINCT(): + right = visit(ctx.right) + return ast.IsDistinctFrom(negated, expr, right) + if ctx.LIKE(): + pattern = visit(ctx.pattern) + return ast.Like(negated, expr, all or any or some or "", pattern, ctx.ESCAPE()) + if ctx.ILIKE(): + pattern = visit(ctx.pattern) + return ast.Like( + negated, + expr, + all or any or some or "", + pattern, + ctx.ESCAPE(), + case_sensitive=False, + ) + if ctx.IN(): + if source_ctx := ctx.query(): + source = visit(source_ctx.queryTerm()) + source.parenthesized = True + if source_ctx := ctx.expression(): + source = visit(source_ctx) + return ast.In(negated, expr, source) + if ctx.IS(): + if ctx.NULL(): + return ast.IsNull(negated, expr) + if ctx.TRUE(): + return ast.IsBoolean(negated, expr, "TRUE") + if ctx.FALSE(): + return ast.IsBoolean(negated, expr, "FALSE") + if ctx.UNKNOWN(): + return ast.IsBoolean(negated, expr, "UNKNOWN") + if ctx.RLIKE(): + pattern = visit(ctx.pattern) + return ast.Rlike(negated, expr, pattern) + return + + +@visit.register +def _(ctx: sbp.SubscriptContext): + return ast.Subscript( + expr=visit(ctx.primaryExpression()), + index=visit(ctx.valueExpression()), + ) + + +@visit.register +def _(ctx: sbp.ValueExpressionContext): + if primary := ctx.primaryExpression(): + return visit(primary) + + +@visit.register +def _(ctx: sbp.ValueExpressionDefaultContext): + return visit(ctx.primaryExpression()) + + +@visit.register +def _(ctx: sbp.ArithmeticBinaryContext): + return ast.BinaryOp( + ast.BinaryOpKind(ctx.operator.text.upper()), + visit(ctx.left), + visit(ctx.right), + ) + + +@visit.register +def _(ctx: sbp.ColumnReferenceContext): + return ast.Column(visit(ctx.identifier())) + + +@visit.register +def _(ctx: sbp.QueryTermDefaultContext): + return visit(ctx.queryPrimary()) + + +@visit.register +def _(ctx: sbp.QueryPrimaryDefaultContext): + return visit(ctx.querySpecification()) + + +@visit.register +def _(ctx: sbp.QueryTermContext): + if primary_query := ctx.queryPrimary(): + return visit(primary_query) + + +@visit.register +def _(ctx: sbp.QueryPrimaryContext): + return visit(ctx.querySpecification()) + + +@visit.register +def _(ctx: sbp.RegularQuerySpecificationContext): + quantifier, projection, hints = visit(ctx.selectClause()) + from_ = visit(ctx.fromClause()) if ctx.fromClause() else None + laterals = visit(ctx.lateralView()) + group_by = visit(ctx.aggregationClause()) if ctx.aggregationClause() else [] + where = None + if where_clause := ctx.whereClause(): + where = visit(where_clause) + having = None + if having_clause := ctx.havingClause(): + having = visit(having_clause) + select = ast.Select( + quantifier=quantifier, + projection=projection, + from_=from_, + lateral_views=laterals, + where=where, + group_by=group_by, + having=having, + hints=hints, + ) + if from_ and from_.laterals: + select.lateral_views += from_.laterals + del from_.laterals + return select + + +@visit.register +def _(ctx: sbp.HavingClauseContext): + return visit(ctx.booleanExpression()) + + +@visit.register +def _(ctx: sbp.AggregationClauseContext): + return visit(ctx.groupByClause()) + + +@visit.register +def _(ctx: sbp.GroupByClauseContext): + if grouping_analytics := ctx.groupingAnalytics(): + return visit(grouping_analytics) + if expression := ctx.expression(): + return visit(expression) + + +@visit.register +def _(ctx: sbp.GroupingAnalyticsContext): + grouping_set = visit(ctx.groupingSet()) + if ctx.ROLLUP(): + return ast.Function(ast.Name(name="ROLLUP"), args=grouping_set) + if ctx.CUBE(): + return ast.Function(ast.Name(name="CUBE"), args=grouping_set) + return grouping_set + + +@visit.register +def _(ctx: sbp.GroupingSetContext): + return ast.Column(ctx.getText()) + + +@visit.register +def _(ctx: sbp.SelectClauseContext): + quantifier = "" + if quant := ctx.setQuantifier(): + quantifier = visit(quant) + projection = visit(ctx.namedExpressionSeq()) + hints = [statement for hint in ctx.hints for statement in visit(hint)] + return quantifier, projection, hints + + +@visit.register +def _(ctx: sbp.HintContext) -> List[ast.Hint]: + return [visit(statement) for statement in ctx.hintStatements] + + +@visit.register +def _(ctx: sbp.HintStatementContext) -> ast.Hint: + name = visit(ctx.hintName) + parameters = [visit(param) for param in ctx.parameters] + return ast.Hint( + name=name, + parameters=parameters, + ) + + +@visit.register +def _(ctx: sbp.SetQuantifierContext): + if ctx.DISTINCT(): + return "DISTINCT" + if ctx.ALL(): + return "ALL" + return "" + + +@visit.register +def _(ctx: sbp.NamedExpressionSeqContext): + return visit(ctx.namedExpression()) + + +@visit.register +def _(ctx: sbp.NamedExpressionContext): + expr = visit(ctx.expression()) + if alias := ctx.name: + return expr.set_alias(visit(alias)) + + if col_names := ctx.identifierList(): + if not isinstance(expr, ast.TableExpression): + raise SqlSyntaxError( + f"{ctx.start.line}:{ctx.start.column} Cannot use an identifier" + "list as an alias on a non-Table Expression.", + ) + expr.column_list = [ast.Column(name) for name in visit(col_names)] + if ctx.AS(): + expr.set_as(True) + return expr + + +@visit.register +def _(ctx: sbp.ErrorCapturingIdentifierContext): + name = visit(ctx.identifier()) + if extra := visit(ctx.errorCapturingIdentifierExtra()): + name.name += extra + name.quote_style = '"' + return name + + +@visit.register +def _(ctx: sbp.ErrorIdentContext): + return ctx.getText() + + +@visit.register +def _(ctx: sbp.RealIdentContext): + return "" + + +@visit.register +def _(ctx: sbp.FirstContext): + return ast.Function(ast.Name("FIRST"), args=[visit(ctx.expression())]) + + +@visit.register +def _(ctx: sbp.IdentifierContext): + return visit(ctx.strictIdentifier()) + + +@visit.register +def _(ctx: sbp.UnquotedIdentifierContext): + return ast.Name(ctx.getText()) + + +@visit.register +def _(ctx: sbp.ConstantDefaultContext): + return visit(ctx.constant()) + + +@visit.register +def _(ctx: sbp.NumericLiteralContext): + return ast.Number(ctx.number().getText()) + + +@visit.register +def _(ctx: sbp.StringLitContext): + if string := ctx.STRING(): + return string.getSymbol().text.strip("'") + return ctx.DOUBLEQUOTED_STRING().getSymbol().text.strip('"') + + +@visit.register +def _(ctx: sbp.DereferenceContext): + base = visit(ctx.base) + field = visit(ctx.fieldName) + if isinstance(base, ast.Subscript) and isinstance(base.expr, ast.Column): + field.namespace = base.expr.name + if isinstance(base, ast.Column): + field.namespace = base.name + base.name = field + return base + + +@visit.register +def _(ctx: sbp.FunctionCallContext): + name = visit(ctx.functionName()) + quantifier = ( + ast.SetQuantifier(visit(ctx.setQuantifier())) if ctx.setQuantifier() else None + ) + over = visit(ctx.windowSpec()) if ctx.windowSpec() else None + args = visit(ctx.argument) + return ast.Function(name, args, quantifier=quantifier, over=over) + + +@visit.register +def _(ctx: sbp.WindowDefContext): + partition_by = visit(ctx.partition) + order_by = visit(ctx.sortItem()) + window_frame = visit(ctx.windowFrame()) if ctx.windowFrame() else None + return ast.Over( + partition_by=partition_by, + order_by=order_by, + window_frame=window_frame, + ) + + +@visit.register +def _(ctx: sbp.WindowFrameContext): + start = visit(ctx.start) + end = visit(ctx.end) if ctx.end else None + return ast.Frame(ctx.frameType.text, start=start, end=end) + + +@visit.register +def _(ctx: sbp.FrameBoundContext): + return ast.FrameBound(start=ctx.start.text, stop=ctx.stop.text) + + +@visit.register +def _(ctx: sbp.FunctionNameContext): + if qual_name := ctx.qualifiedName(): + return visit(qual_name) + return ast.Name(ctx.getText()) + + +@visit.register +def _(ctx: sbp.QualifiedNameContext): + names = visit(ctx.children) + for i in range(len(names) - 1, 0, -1): + names[i].namespace = names[i - 1] + return names[-1] + + +@visit.register +def _(ctx: sbp.StarContext): + namespace = None + if qual_name := ctx.qualifiedName(): + namespace = visit(qual_name) + star = ast.Wildcard() + star.name.namespace = namespace + return star + + +@visit.register +def _(ctx: sbp.TableNameContext): + if ctx.temporalClause(): + return + name = visit(ctx.multipartIdentifier()) + table_alias = ctx.tableAlias() + alias, cols = visit(table_alias) + table = ast.Table(name, column_list=cols) + if alias: + table = table.set_alias(ast.Name(alias)) + if table_alias.AS(): + table = table.set_as(True) + return table + + +@visit.register +def _(ctx: sbp.MultipartIdentifierContext): + names = visit(ctx.children) + for i in range(len(names) - 1, 0, -1): + names[i].namespace = names[i - 1] + return names[-1] + + +@visit.register +def _(ctx: sbp.QuotedIdentifierAlternativeContext): + return visit(ctx.quotedIdentifier()) + + +@visit.register +def _(ctx: sbp.QuotedIdentifierContext): + if ident := ctx.BACKQUOTED_IDENTIFIER(): + return ast.Name(ident.getText()[1:-1], quote_style="`") + return ast.Name(ctx.DOUBLEQUOTED_STRING().getText()[1:-1], quote_style='"') + + +@visit.register +def _(ctx: sbp.LateralViewContext): + outer = bool(ctx.OUTER()) + func_name = visit(ctx.qualifiedName()) + func_args = visit(ctx.expression()) + table_name = visit(ctx.tblName) + function_table_name = table_name if table_name != ast.Name(name="AS") else None + func = ast.FunctionTable(func_name, args=func_args, alias=function_table_name) + func.set_alias(function_table_name) + if function_table_name: + func.column_list = [ + ast.Column(name=ast.Name(name=name.name, namespace=function_table_name)) + for name in visit(ctx.colName) + ] + else: + func.column_list = [ast.Column(name=name) for name in visit(ctx.colName)] + if ctx.AS() or table_name == ast.Name(name="AS"): + func.set_as(True) + return ast.LateralView(outer, func) + + +@visit.register +def _(ctx: sbp.RelationContext): + primary_relation = visit(ctx.relationPrimary()) + extensions = visit(ctx.relationExtension()) if ctx.relationExtension() else [] + return ast.Relation(primary_relation, extensions) + + +@visit.register +def _(ctx: sbp.RelationExtensionContext): + if join_rel := ctx.joinRelation(): + return visit(join_rel) + + +@visit.register +def _(ctx: sbp.JoinTypeContext): + anti = f"{ctx.ANTI()} " if ctx.ANTI() else "" + cross = f"{ctx.CROSS()} " if ctx.CROSS() else "" + full = f"{ctx.FULL()} " if ctx.FULL() else "" + inner = f"{ctx.INNER()} " if ctx.INNER() else "" + outer = f"{ctx.OUTER()} " if ctx.OUTER() else "" + semi = f"{ctx.SEMI()} " if ctx.SEMI() else "" + left = f"{ctx.LEFT()} " if ctx.LEFT() else "" + right = f"{ctx.RIGHT()} " if ctx.RIGHT() else "" + return f"{left}{right}{cross}{full}{semi}{outer}{inner}{anti}" + + +@visit.register +def _(ctx: sbp.JoinRelationContext): + kind = visit(ctx.joinType()) + lateral = bool(ctx.LATERAL()) + natural = bool(ctx.NATURAL()) + criteria = None + right = visit(ctx.right) + if join_criteria := ctx.joinCriteria(): + criteria = visit(join_criteria) + return ast.Join(kind, right, criteria=criteria, lateral=lateral, natural=natural) + + +@visit.register +def _(ctx: sbp.TableValuedFunctionContext): + return visit(ctx.functionTable()) + + +@visit.register +def _(ctx: sbp.FunctionTableContext): + name = visit(ctx.funcName) + args = visit(ctx.expression()) + alias, cols = visit(ctx.tableAlias()) + func_table = ast.FunctionTable( + name, + args=args, + column_list=[ast.Column(name) for name in cols], + alias=alias, + ) + if not cols: + func_table.column_list = alias + return func_table + + +@visit.register +def _(ctx: sbp.TableAliasContext): + if ident := ctx.strictIdentifier(): + identifier_list = visit(ctx.identifierList()) if ctx.identifierList() else [] + return visit(ident), identifier_list + return None, [] + + +@visit.register +def _(ctx: sbp.IdentifierListContext): + return visit(ctx.identifierSeq()) + + +@visit.register +def _(ctx: sbp.IdentifierSeqContext): + return visit(ctx.ident) + + +@visit.register +def _(ctx: sbp.FromClauseContext): + relations = visit(ctx.relation()) + laterals = visit(ctx.lateralView()) + if ctx.pivotClause() or ctx.unpivotClause(): + return + from_ = ast.From(relations) + from_.laterals = laterals + return from_ + + +@visit.register +def _(ctx: sbp.JoinCriteriaContext): + if expr := ctx.booleanExpression(): + return ast.JoinCriteria(on=visit(expr)) + return ast.JoinCriteria(using=(visit(ctx.identifierList()))) + + +@visit.register +def _(ctx: sbp.ComparisonContext): + left, right = visit(ctx.left), visit(ctx.right) + op = visit(ctx.comparisonOperator()) + return ast.BinaryOp(ast.BinaryOpKind(op.upper()), left, right) + + +@visit.register +def _(ctx: sbp.ComparisonOperatorContext): + return ctx.getText() + + +@visit.register +def _(ctx: sbp.WhereClauseContext): + return visit(ctx.booleanExpression()) + + +@visit.register +def _(ctx: sbp.StringLiteralContext): + return ast.String(ctx.getText()) + + +@visit.register +def _(ctx: sbp.CtesContext) -> List[ast.Select]: + names = {} + ctes = [] + for namedQuery in ctx.namedQuery(): + if namedQuery.name in names: + raise SqlSyntaxError(f"Duplicate CTE definition names: {namedQuery.name}") + query = visit(namedQuery.query()) + query = query.set_alias(visit(namedQuery.name)) + if namedQuery.AS(): + query = query.set_as(True) + query.parenthesized = True + ctes.append(query) + return ctes + + +@visit.register +def _(ctx: sbp.NamedQueryContext) -> ast.Select: + return visit(ctx.query()) + + +@visit.register +def _(ctx: sbp.LogicalBinaryContext) -> ast.BinaryOp: + return ast.BinaryOp( + ast.BinaryOpKind(ctx.operator.text.upper()), + visit(ctx.left), + visit(ctx.right), + ) + + +@visit.register +def _(ctx: sbp.SubqueryExpressionContext): + return visit(ctx.query()) + + +@visit.register +def _(ctx: sbp.SetOperationContext): + operator = ctx.operator.text + quantifier = f" {visit(ctx.setQuantifier())}" if ctx.setQuantifier() else "" + left = visit(ctx.left) + left.add_set_op( + ast.SetOp( + kind=f"{operator}{quantifier}", + right=visit(ctx.right), + ), + ) + return left + + +@visit.register +def _(ctx: sbp.AliasedQueryContext) -> ast.Select: + query = visit(ctx.query()) + query.parenthesized = True + table_alias = ctx.tableAlias() + ident, _ = visit(table_alias) + if ident: + query = query.set_alias(ident) + if table_alias.AS(): + query = query.set_as(True) + return query + + +@visit.register +def _(ctx: sbp.SimpleCaseContext) -> ast.Case: + expr = visit(ctx.value) + conditions = [] + results = [] + for when in ctx.whenClause(): + condition, result = visit(when) + conditions.append(condition) + results.append(result) + return ast.Case( + expr, + conditions=conditions, + else_result=visit(ctx.elseExpression) if ctx.elseExpression else None, + results=results, + ) + + +@visit.register +def _(ctx: sbp.SearchedCaseContext) -> ast.Case: + conditions = [] + results = [] + for when in ctx.whenClause(): + condition, result = visit(when) + conditions.append(condition) + results.append(result) + return ast.Case( + None, + conditions=conditions, + else_result=visit(ctx.elseExpression) if ctx.elseExpression else None, + results=results, + ) + + +@visit.register +def _(ctx: sbp.WhenClauseContext) -> Tuple[ast.Expression, ast.Expression]: + condition, result = visit(ctx.condition), visit(ctx.result) + return condition, result + + +@visit.register +def _(ctx: sbp.ParenthesizedExpressionContext) -> ast.Expression: + expr = visit(ctx.expression()) + expr.parenthesized = True + return expr + + +@visit.register +def _(ctx: sbp.NullLiteralContext) -> ast.Null: + return ast.Null() + + +@visit.register +def _(ctx: sbp.CastContext) -> ast.Cast: + data_type = visit(ctx.dataType()) + expression = visit(ctx.expression()) + return ast.Cast(data_type=data_type, expression=expression) + + +@visit.register +def _(ctx: sbp.ExistsContext) -> ast.UnaryOp: + expr = visit(ctx.query().queryTerm()) + expr.parenthesized = True + return ast.UnaryOp(op=UnaryOpKind.Exists, expr=expr) + + +@visit.register +def _(ctx: sbp.LogicalNotContext) -> ast.UnaryOp: + return ast.UnaryOp(op=UnaryOpKind.Not, expr=visit(ctx.booleanExpression())) + + +@visit.register +def _(ctx: sbp.IntervalLiteralContext) -> ast.Interval: + return visit(ctx.interval()) + + +@visit.register +def _(ctx: sbp.SubqueryContext): + return visit(ctx.query().queryTerm()) + + +@visit.register +def _(ctx: sbp.StructContext) -> ast.Function: + return ast.Function( + ast.Name("struct"), + args=visit(ctx.argument), + ) + + +@visit.register +def _(ctx: sbp.IntervalContext) -> ast.Interval: + return visit(ctx.children[1]) + + +@visit.register +def _(ctx: sbp.ErrorCapturingMultiUnitsIntervalContext) -> ast.Interval: + from_ = visit(ctx.body) + to = None + if utu := ctx.unitToUnitInterval(): + to = visit(utu) + interval = ast.Interval(from_ + to.from_, to.to) + else: + interval = ast.Interval(from_) + return interval + + +@visit.register +def _(ctx: sbp.UnitToUnitIntervalContext) -> ast.Interval: + value = visit(ctx.value) + from_ = visit(ctx.from_) + to = visit(ctx.to) + return ast.Interval([ast.IntervalUnit(from_, value)], ast.IntervalUnit(to)) + + +@visit.register +def _(ctx: sbp.MultiUnitsIntervalContext) -> List[ast.IntervalUnit]: + units = [] + for pair_i in range(0, len(ctx.children), 2): + value, unit = ctx.children[pair_i : pair_i + 2] + value = visit(value) + unit = visit(unit) + units.append(ast.IntervalUnit(unit, value)) + return units + + +@visit.register +def _(ctx: sbp.ErrorCapturingUnitToUnitIntervalContext) -> ast.Interval: + if ctx.error1 or ctx.error2: + raise SqlSyntaxError( + f"{ctx.start.line}:{ctx.start.column} Error capturing unit-to-unit interval.", + ) + return visit(ctx.body) + + +@visit.register +def _(ctx: sbp.IntervalValueContext) -> ast.Number: + if stringlit_ctx := ctx.stringLit(): + return ast.Number(visit(stringlit_ctx)) + return ast.Number(ctx.getText()) + + +@visit.register +def _(ctx: sbp.UnitInMultiUnitsContext) -> str: + return ctx.getText().upper().rstrip("S") + + +@visit.register +def _(ctx: sbp.UnitInUnitToUnitContext) -> str: + return ctx.getText().upper() + + +@visit.register +def _(ctx: sbp.TrimContext) -> ast.Function: + both = "BOTH " if ctx.BOTH() else "" + leading = "LEADING " if ctx.LEADING() else "" + trailing = "TRAILING " if ctx.TRAILING() else "" + from_ = "FROM " if ctx.FROM() else "" + return ast.Function( + ast.Name("TRIM"), + [visit(ctx.srcStr)], + f"{both or leading or trailing}{from_}", + ) + + +@visit.register +def _(ctx: sbp.LambdaContext) -> ast.Lambda: + identifier = visit(ctx.identifier()) + expr = visit(ctx.expression()) + lambda_expr = ast.Lambda(identifiers=identifier, expr=expr) + lambda_expr._type = ct.LambdaType + return lambda_expr + + +@visit.register +def _(ctx: sbp.PrimitiveDataTypeContext) -> ast.Value: + column_type = ctx.getText().strip() + decimal_match = ct.DECIMAL_REGEX.match(column_type) + if decimal_match: + precision = int(decimal_match.group("precision")) + scale = int(decimal_match.group("scale")) + return ct.DecimalType(precision, scale) + + fixed_match = ct.FIXED_PARSER.match(column_type) + if fixed_match: + length = int(fixed_match.group("length")) + return ct.FixedType(length) + + varchar_match = ct.VARCHAR_PARSER.match(column_type) + if varchar_match: + length = varchar_match.group("length") + return ct.VarcharType(length) if length else ct.VarcharType() + + column_type = column_type.lower().strip("()") + try: + return ct.PRIMITIVE_TYPES[column_type] + except KeyError as exc: + raise DJParseException( + f"DJ does not recognize the type `{ctx.getText()}`.", + ) from exc + + +@visit.register +def _(ctx: sbp.ComplexDataTypeContext) -> ct.ColumnType: + if ctx.ARRAY(): + return ct.ListType(visit(ctx.dataType())[0]) + if ctx.MAP(): + return ct.MapType(*visit(ctx.dataType())) + if ctx.STRUCT(): + if type_list := ctx.complexColTypeList(): + return ct.StructType(*visit(type_list)) + else: + return ct.StructType() + + +@visit.register +def _(ctx: sbp.ComplexColTypeListContext) -> List[ct.NestedField]: + return visit(ctx.complexColType()) + + +@visit.register +def _(ctx: sbp.ComplexColTypeContext) -> ct.NestedField: + name = visit(ctx.identifier()) + type = visit(ctx.dataType()) + optional = not ctx.NOT() + doc = None + if comment := ctx.commentSpec(): + doc = comment.getText() + return ct.NestedField(name, type, optional, doc) + + +@visit.register +def _(ctx: sbp.YearMonthIntervalDataTypeContext) -> ct.YearMonthIntervalType: + from_ = ctx.from_.getText().upper() + to = ctx.to.text.upper() if ctx.to else None + return ct.YearMonthIntervalType(from_=from_, to_=to) + + +@visit.register +def _(ctx: sbp.DayTimeIntervalDataTypeContext) -> ct.DayTimeIntervalType: + from_ = ctx.from_.text.upper() + to = ctx.to.text.upper() if ctx.to else None + return ct.DayTimeIntervalType(from_=from_, to_=to) + + +@visit.register +def _(ctx: sbp.ExtractContext): + return ast.Function( + ast.Name("EXTRACT"), + args=[visit(ctx.field), visit(ctx.source)], + ) diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/exceptions.py b/datajunction-server/datajunction_server/sql/parsing/backends/exceptions.py new file mode 100644 index 000000000..870ec0807 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/backends/exceptions.py @@ -0,0 +1,13 @@ +""" +defines exceptions used for backend parsing +""" + +from http import HTTPStatus + +from datajunction_server.errors import DJException + + +class DJParseException(DJException): + """Exception type raised upon problem creating a DJ sql ast""" + + http_status_code: int = HTTPStatus.UNPROCESSABLE_ENTITY diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/grammar/SqlBaseLexer.g4 b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/SqlBaseLexer.g4 new file mode 100644 index 000000000..6d0862290 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/SqlBaseLexer.g4 @@ -0,0 +1,511 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is an adaptation of Presto's presto-parser/src/main/antlr4/com/facebook/presto/sql/parser/SqlBase.g4 grammar. + */ + +lexer grammar SqlBaseLexer; + +@members { + /** + * When true, parser should throw ParseException for unclosed bracketed comment. + */ + public boolean has_unclosed_bracketed_comment = false; + + /** + * Verify whether current token is a valid decimal token (which contains dot). + * Returns true if the character that follows the token is not a digit or letter or underscore. + * + * For example: + * For char stream "2.3", "2." is not a valid decimal token, because it is followed by digit '3'. + * For char stream "2.3_", "2.3" is not a valid decimal token, because it is followed by '_'. + * For char stream "2.3W", "2.3" is not a valid decimal token, because it is followed by 'W'. + * For char stream "12.0D 34.E2+0.12 " 12.0D is a valid decimal token because it is followed + * by a space. 34.E2 is a valid decimal token because it is followed by symbol '+' + * which is not a digit or letter or underscore. + */ + public boolean isValidDecimal() { + int nextChar = _input.LA(1); + if (nextChar >= 'A' && nextChar <= 'Z' || nextChar >= '0' && nextChar <= '9' || + nextChar == '_') { + return false; + } else { + return true; + } + } + + /** + * This method will be called when we see '/*' and try to match it as a bracketed comment. + * If the next character is '+', it should be parsed as hint later, and we cannot match + * it as a bracketed comment. + * + * Returns true if the next character is '+'. + */ + public boolean isHint() { + int nextChar = _input.LA(1); + if (nextChar == '+') { + return true; + } else { + return false; + } + } + + /** + * This method will be called when the character stream ends and try to find out the + * unclosed bracketed comment. + * If the method be called, it means the end of the entire character stream match, + * and we set the flag and fail later. + */ + public void markUnclosedComment() { + has_unclosed_bracketed_comment = true; + } +} + +SEMICOLON: ';'; + +LEFT_PAREN: '('; +RIGHT_PAREN: ')'; +COMMA: ','; +DOT: '.'; +LEFT_BRACKET: '['; +RIGHT_BRACKET: ']'; + +// NOTE: If you add a new token in the list below, you should update the list of keywords +// and reserved tag in `docs/sql-ref-ansi-compliance.md#sql-keywords`. + +//============================ +// Start of the keywords list +//============================ +//--SPARK-KEYWORD-LIST-START +ADD: 'ADD'; +AFTER: 'AFTER'; +ALL: 'ALL'; +ALTER: 'ALTER'; +ALWAYS: 'ALWAYS'; +ANALYZE: 'ANALYZE'; +AND: 'AND'; +ANTI: 'ANTI'; +ANY: 'ANY'; +ANY_VALUE: 'ANY_VALUE'; +ARCHIVE: 'ARCHIVE'; +ARRAY: 'ARRAY'; +AS: 'AS'; +ASC: 'ASC'; +AT: 'AT'; +AUTHORIZATION: 'AUTHORIZATION'; +BETWEEN: 'BETWEEN'; +BOTH: 'BOTH'; +BUCKET: 'BUCKET'; +BUCKETS: 'BUCKETS'; +BY: 'BY'; +CACHE: 'CACHE'; +CASCADE: 'CASCADE'; +CASE: 'CASE'; +CAST: 'CAST'; +CATALOG: 'CATALOG'; +CATALOGS: 'CATALOGS'; +CHANGE: 'CHANGE'; +CHECK: 'CHECK'; +CLEAR: 'CLEAR'; +CLUSTER: 'CLUSTER'; +CLUSTERED: 'CLUSTERED'; +CODEGEN: 'CODEGEN'; +COLLATE: 'COLLATE'; +COLLECTION: 'COLLECTION'; +COLUMN: 'COLUMN'; +COLUMNS: 'COLUMNS'; +COMMENT: 'COMMENT'; +COMMIT: 'COMMIT'; +COMPACT: 'COMPACT'; +COMPACTIONS: 'COMPACTIONS'; +COMPUTE: 'COMPUTE'; +CONCATENATE: 'CONCATENATE'; +CONSTRAINT: 'CONSTRAINT'; +COST: 'COST'; +CREATE: 'CREATE'; +CROSS: 'CROSS'; +CUBE: 'CUBE'; +CURRENT: 'CURRENT'; +CURRENT_DATE: 'CURRENT_DATE'; +CURRENT_TIME: 'CURRENT_TIME'; +CURRENT_TIMESTAMP: 'CURRENT_TIMESTAMP'; +CURRENT_USER: 'CURRENT_USER'; +DAY: 'DAY'; +DAYS: 'DAYS'; +DAYOFYEAR: 'DAYOFYEAR'; +DATA: 'DATA'; +DATABASE: 'DATABASE'; +DATABASES: 'DATABASES'; +DATEADD: 'DATEADD'; +DATEDIFF: 'DATEDIFF'; +DBPROPERTIES: 'DBPROPERTIES'; +DEFAULT: 'DEFAULT'; +DEFINED: 'DEFINED'; +DELETE: 'DELETE'; +DELIMITED: 'DELIMITED'; +DESC: 'DESC'; +DESCRIBE: 'DESCRIBE'; +DFS: 'DFS'; +DIRECTORIES: 'DIRECTORIES'; +DIRECTORY: 'DIRECTORY'; +DISTINCT: 'DISTINCT'; +DISTRIBUTE: 'DISTRIBUTE'; +DIV: 'DIV'; +DROP: 'DROP'; +ELSE: 'ELSE'; +END: 'END'; +ESCAPE: 'ESCAPE'; +ESCAPED: 'ESCAPED'; +EXCEPT: 'EXCEPT'; +EXCHANGE: 'EXCHANGE'; +EXCLUDE: 'EXCLUDE'; +EXISTS: 'EXISTS'; +EXPLAIN: 'EXPLAIN'; +EXPORT: 'EXPORT'; +EXTENDED: 'EXTENDED'; +EXTERNAL: 'EXTERNAL'; +EXTRACT: 'EXTRACT'; +FALSE: 'FALSE'; +FETCH: 'FETCH'; +FIELDS: 'FIELDS'; +FILTER: 'FILTER'; +FILEFORMAT: 'FILEFORMAT'; +FIRST: 'FIRST'; +FOLLOWING: 'FOLLOWING'; +FOR: 'FOR'; +FOREIGN: 'FOREIGN'; +FORMAT: 'FORMAT'; +FORMATTED: 'FORMATTED'; +FROM: 'FROM'; +FULL: 'FULL'; +FUNCTION: 'FUNCTION'; +FUNCTIONS: 'FUNCTIONS'; +GENERATED: 'GENERATED'; +GLOBAL: 'GLOBAL'; +GRANT: 'GRANT'; +GROUP: 'GROUP'; +GROUPING: 'GROUPING'; +HAVING: 'HAVING'; +HOUR: 'HOUR'; +HOURS: 'HOURS'; +IF: 'IF'; +IGNORE: 'IGNORE'; +IMPORT: 'IMPORT'; +IN: 'IN'; +INCLUDE: 'INCLUDE'; +INDEX: 'INDEX'; +INDEXES: 'INDEXES'; +INNER: 'INNER'; +INPATH: 'INPATH'; +INPUTFORMAT: 'INPUTFORMAT'; +INSERT: 'INSERT'; +INTERSECT: 'INTERSECT'; +INTERVAL: 'INTERVAL'; +INTO: 'INTO'; +IS: 'IS'; +ITEMS: 'ITEMS'; +JOIN: 'JOIN'; +KEYS: 'KEYS'; +LAST: 'LAST'; +LATERAL: 'LATERAL'; +LAZY: 'LAZY'; +LEADING: 'LEADING'; +LEFT: 'LEFT'; +LIKE: 'LIKE'; +ILIKE: 'ILIKE'; +LIMIT: 'LIMIT'; +LINES: 'LINES'; +LIST: 'LIST'; +LOAD: 'LOAD'; +LOCAL: 'LOCAL'; +LOCATION: 'LOCATION'; +LOCK: 'LOCK'; +LOCKS: 'LOCKS'; +LOGICAL: 'LOGICAL'; +MACRO: 'MACRO'; +MAP: 'MAP'; +MATCHED: 'MATCHED'; +MERGE: 'MERGE'; +MICROSECOND: 'MICROSECOND'; +MICROSECONDS: 'MICROSECONDS'; +MILLISECOND: 'MILLISECOND'; +MILLISECONDS: 'MILLISECONDS'; +MINUTE: 'MINUTE'; +MINUTES: 'MINUTES'; +MONTH: 'MONTH'; +MONTHS: 'MONTHS'; +MSCK: 'MSCK'; +NAMESPACE: 'NAMESPACE'; +NAMESPACES: 'NAMESPACES'; +NANOSECOND: 'NANOSECOND'; +NANOSECONDS: 'NANOSECONDS'; +NATURAL: 'NATURAL'; +NO: 'NO'; +NOT: 'NOT' | '!'; +NULL: 'NULL'; +NULLS: 'NULLS'; +OF: 'OF'; +OFFSET: 'OFFSET'; +ON: 'ON'; +ONLY: 'ONLY'; +OPTION: 'OPTION'; +OPTIONS: 'OPTIONS'; +OR: 'OR'; +ORDER: 'ORDER'; +OUT: 'OUT'; +OUTER: 'OUTER'; +OUTPUTFORMAT: 'OUTPUTFORMAT'; +OVER: 'OVER'; +OVERLAPS: 'OVERLAPS'; +OVERLAY: 'OVERLAY'; +OVERWRITE: 'OVERWRITE'; +PARTITION: 'PARTITION'; +PARTITIONED: 'PARTITIONED'; +PARTITIONS: 'PARTITIONS'; +PERCENTILE_CONT: 'PERCENTILE_CONT'; +PERCENTILE_DISC: 'PERCENTILE_DISC'; +PERCENTLIT: 'PERCENT'; +PIVOT: 'PIVOT'; +PLACING: 'PLACING'; +POSITION: 'POSITION'; +PRECEDING: 'PRECEDING'; +PRIMARY: 'PRIMARY'; +PRINCIPALS: 'PRINCIPALS'; +PROPERTIES: 'PROPERTIES'; +PURGE: 'PURGE'; +QUARTER: 'QUARTER'; +QUERY: 'QUERY'; +RANGE: 'RANGE'; +RECORDREADER: 'RECORDREADER'; +RECORDWRITER: 'RECORDWRITER'; +RECOVER: 'RECOVER'; +REDUCE: 'REDUCE'; +REFERENCES: 'REFERENCES'; +REFRESH: 'REFRESH'; +RENAME: 'RENAME'; +REPAIR: 'REPAIR'; +REPEATABLE: 'REPEATABLE'; +REPLACE: 'REPLACE'; +RESET: 'RESET'; +RESPECT: 'RESPECT'; +RESTRICT: 'RESTRICT'; +REVOKE: 'REVOKE'; +RIGHT: 'RIGHT'; +RLIKE: 'RLIKE' | 'REGEXP'; +ROLE: 'ROLE'; +ROLES: 'ROLES'; +ROLLBACK: 'ROLLBACK'; +ROLLUP: 'ROLLUP'; +ROW: 'ROW'; +ROWS: 'ROWS'; +SECOND: 'SECOND'; +SECONDS: 'SECONDS'; +SCHEMA: 'SCHEMA'; +SCHEMAS: 'SCHEMAS'; +SELECT: 'SELECT'; +SEMI: 'SEMI'; +SEPARATED: 'SEPARATED'; +SERDE: 'SERDE'; +SERDEPROPERTIES: 'SERDEPROPERTIES'; +SESSION_USER: 'SESSION_USER'; +SET: 'SET'; +SETMINUS: 'MINUS'; +SETS: 'SETS'; +SHOW: 'SHOW'; +SKEWED: 'SKEWED'; +SOME: 'SOME'; +SORT: 'SORT'; +SORTED: 'SORTED'; +SOURCE: 'SOURCE'; +START: 'START'; +STATISTICS: 'STATISTICS'; +STORED: 'STORED'; +STRATIFY: 'STRATIFY'; +STRUCT: 'STRUCT'; +SUBSTR: 'SUBSTR'; +SUBSTRING: 'SUBSTRING'; +SYNC: 'SYNC'; +SYSTEM_TIME: 'SYSTEM_TIME'; +SYSTEM_VERSION: 'SYSTEM_VERSION'; +TABLE: 'TABLE'; +TABLES: 'TABLES'; +TABLESAMPLE: 'TABLESAMPLE'; +TARGET: 'TARGET'; +TBLPROPERTIES: 'TBLPROPERTIES'; +TEMPORARY: 'TEMPORARY' | 'TEMP'; +TERMINATED: 'TERMINATED'; +THEN: 'THEN'; +TIME: 'TIME'; +TIMESTAMP: 'TIMESTAMP'; +TIMESTAMPADD: 'TIMESTAMPADD'; +TIMESTAMPDIFF: 'TIMESTAMPDIFF'; +TO: 'TO'; +TOUCH: 'TOUCH'; +TRAILING: 'TRAILING'; +TRANSACTION: 'TRANSACTION'; +TRANSACTIONS: 'TRANSACTIONS'; +TRANSFORM: 'TRANSFORM'; +TRIM: 'TRIM'; +TRUE: 'TRUE'; +TRUNCATE: 'TRUNCATE'; +TRY_CAST: 'TRY_CAST'; +TYPE: 'TYPE'; +UNARCHIVE: 'UNARCHIVE'; +UNBOUNDED: 'UNBOUNDED'; +UNCACHE: 'UNCACHE'; +UNION: 'UNION'; +UNIQUE: 'UNIQUE'; +UNKNOWN: 'UNKNOWN'; +UNLOCK: 'UNLOCK'; +UNPIVOT: 'UNPIVOT'; +UNSET: 'UNSET'; +UPDATE: 'UPDATE'; +USE: 'USE'; +USER: 'USER'; +USING: 'USING'; +VALUES: 'VALUES'; +VERSION: 'VERSION'; +VIEW: 'VIEW'; +VIEWS: 'VIEWS'; +WEEK: 'WEEK'; +WEEKS: 'WEEKS'; +WHEN: 'WHEN'; +WHERE: 'WHERE'; +WINDOW: 'WINDOW'; +WITH: 'WITH'; +WITHIN: 'WITHIN'; +YEAR: 'YEAR'; +YEARS: 'YEARS'; +ZONE: 'ZONE'; +//--SPARK-KEYWORD-LIST-END +//============================ +// End of the keywords list +//============================ + +EQ : '=' | '=='; +NSEQ: '<=>'; +NEQ : '<>'; +NEQJ: '!='; +LT : '<'; +LTE : '<=' | '!>'; +GT : '>'; +GTE : '>=' | '!<'; + +PLUS: '+'; +MINUS: '-'; +ASTERISK: '*'; +SLASH: '/'; +PERCENT: '%'; +TILDE: '~'; +AMPERSAND: '&'; +PIPE: '|'; +CONCAT_PIPE: '||'; +HAT: '^'; +COLON: ':'; +ARROW: '->'; +HENT_START: '/*+'; +HENT_END: '*/'; + +STRING + : '\'' ( ~('\''|'\\') | ('\\' .) )* '\'' + | 'R\'' (~'\'')* '\'' + | 'R"'(~'"')* '"' + ; + +DOUBLEQUOTED_STRING + :'"' ( ~('"'|'\\') | ('\\' .) )* '"' + ; + +BIGINT_LITERAL + : DIGIT+ 'L' + ; + +SMALLINT_LITERAL + : DIGIT+ 'S' + ; + +TINYINT_LITERAL + : DIGIT+ 'Y' + ; + +INTEGER_VALUE + : DIGIT+ + ; + +EXPONENT_VALUE + : DIGIT+ EXPONENT + | DECIMAL_DIGITS EXPONENT {isValidDecimal()}? + ; + +DECIMAL_VALUE + : DECIMAL_DIGITS {isValidDecimal()}? + ; + +FLOAT_LITERAL + : DIGIT+ EXPONENT? 'F' + | DECIMAL_DIGITS EXPONENT? 'F' {isValidDecimal()}? + ; + +DOUBLE_LITERAL + : DIGIT+ EXPONENT? 'D' + | DECIMAL_DIGITS EXPONENT? 'D' {isValidDecimal()}? + ; + +BIGDECIMAL_LITERAL + : DIGIT+ EXPONENT? 'BD' + | DECIMAL_DIGITS EXPONENT? 'BD' {isValidDecimal()}? + ; + +IDENTIFIER + : (LETTER | DIGIT | '_')+ + ; + +BACKQUOTED_IDENTIFIER + : '`' ( ~'`' | '``' )* '`' + ; + +fragment DECIMAL_DIGITS + : DIGIT+ '.' DIGIT* + | '.' DIGIT+ + ; + +fragment EXPONENT + : 'E' [+-]? DIGIT+ + ; + +fragment DIGIT + : [0-9] + ; + +fragment LETTER + : [A-Z] + ; + +SIMPLE_COMMENT + : '--' ('\\\n' | ~[\r\n])* '\r'? '\n'? -> channel(HIDDEN) + ; + +BRACKETED_COMMENT + : '/*' {!isHint()}? ( BRACKETED_COMMENT | . )*? ('*/' | {markUnclosedComment();} EOF) -> channel(HIDDEN) + ; + +WS + : [ \r\n\t]+ -> channel(HIDDEN) + ; + +// Catch-all for anything we can't recognize. +// We use this to be able to ignore and recover all the text +// when splitting statements with DelimiterLexer +UNRECOGNIZED + : . + ; diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/grammar/SqlBaseParser.g4 b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/SqlBaseParser.g4 new file mode 100644 index 000000000..38fa9b889 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/SqlBaseParser.g4 @@ -0,0 +1,1714 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is an adaptation of Presto's presto-parser/src/main/antlr4/com/facebook/presto/sql/parser/SqlBase.g4 grammar. + */ + +parser grammar SqlBaseParser; + +options { tokenVocab = SqlBaseLexer; } + +singleStatement + : statement SEMICOLON* EOF + ; + +singleExpression + : namedExpression EOF + ; + +singleTableIdentifier + : tableIdentifier EOF + ; + +singleMultipartIdentifier + : multipartIdentifier EOF + ; + +singleFunctionIdentifier + : functionIdentifier EOF + ; + +singleDataType + : dataType EOF + ; + +singleTableSchema + : colTypeList EOF + ; + +statement + : query #statementDefault + | ctes? dmlStatementNoWith #dmlStatement + | USE multipartIdentifier #use + | USE namespace multipartIdentifier #useNamespace + | SET CATALOG (identifier | stringLit) #setCatalog + | CREATE namespace (IF NOT EXISTS)? multipartIdentifier + (commentSpec | + locationSpec | + (WITH (DBPROPERTIES | PROPERTIES) propertyList))* #createNamespace + | ALTER namespace multipartIdentifier + SET (DBPROPERTIES | PROPERTIES) propertyList #setNamespaceProperties + | ALTER namespace multipartIdentifier + SET locationSpec #setNamespaceLocation + | DROP namespace (IF EXISTS)? multipartIdentifier + (RESTRICT | CASCADE)? #dropNamespace + | SHOW namespaces ((FROM | IN) multipartIdentifier)? + (LIKE? pattern=stringLit)? #showNamespaces + | createTableHeader (LEFT_PAREN createOrReplaceTableColTypeList RIGHT_PAREN)? tableProvider? + createTableClauses + (AS? query)? #createTable + | CREATE TABLE (IF NOT EXISTS)? target=tableIdentifier + LIKE source=tableIdentifier + (tableProvider | + rowFormat | + createFileFormat | + locationSpec | + (TBLPROPERTIES tableProps=propertyList))* #createTableLike + | replaceTableHeader (LEFT_PAREN createOrReplaceTableColTypeList RIGHT_PAREN)? tableProvider? + createTableClauses + (AS? query)? #replaceTable + | ANALYZE TABLE multipartIdentifier partitionSpec? COMPUTE STATISTICS + (identifier | FOR COLUMNS identifierSeq | FOR ALL COLUMNS)? #analyze + | ANALYZE TABLES ((FROM | IN) multipartIdentifier)? COMPUTE STATISTICS + (identifier)? #analyzeTables + | ALTER TABLE multipartIdentifier + ADD (COLUMN | COLUMNS) + columns=qualifiedColTypeWithPositionList #addTableColumns + | ALTER TABLE multipartIdentifier + ADD (COLUMN | COLUMNS) + LEFT_PAREN columns=qualifiedColTypeWithPositionList RIGHT_PAREN #addTableColumns + | ALTER TABLE table=multipartIdentifier + RENAME COLUMN + from=multipartIdentifier TO to=errorCapturingIdentifier #renameTableColumn + | ALTER TABLE multipartIdentifier + DROP (COLUMN | COLUMNS) (IF EXISTS)? + LEFT_PAREN columns=multipartIdentifierList RIGHT_PAREN #dropTableColumns + | ALTER TABLE multipartIdentifier + DROP (COLUMN | COLUMNS) (IF EXISTS)? + columns=multipartIdentifierList #dropTableColumns + | ALTER (TABLE | VIEW) from=multipartIdentifier + RENAME TO to=multipartIdentifier #renameTable + | ALTER (TABLE | VIEW) multipartIdentifier + SET TBLPROPERTIES propertyList #setTableProperties + | ALTER (TABLE | VIEW) multipartIdentifier + UNSET TBLPROPERTIES (IF EXISTS)? propertyList #unsetTableProperties + | ALTER TABLE table=multipartIdentifier + (ALTER | CHANGE) COLUMN? column=multipartIdentifier + alterColumnAction? #alterTableAlterColumn + | ALTER TABLE table=multipartIdentifier partitionSpec? + CHANGE COLUMN? + colName=multipartIdentifier colType colPosition? #hiveChangeColumn + | ALTER TABLE table=multipartIdentifier partitionSpec? + REPLACE COLUMNS + LEFT_PAREN columns=qualifiedColTypeWithPositionList + RIGHT_PAREN #hiveReplaceColumns + | ALTER TABLE multipartIdentifier (partitionSpec)? + SET SERDE stringLit (WITH SERDEPROPERTIES propertyList)? #setTableSerDe + | ALTER TABLE multipartIdentifier (partitionSpec)? + SET SERDEPROPERTIES propertyList #setTableSerDe + | ALTER (TABLE | VIEW) multipartIdentifier ADD (IF NOT EXISTS)? + partitionSpecLocation+ #addTablePartition + | ALTER TABLE multipartIdentifier + from=partitionSpec RENAME TO to=partitionSpec #renameTablePartition + | ALTER (TABLE | VIEW) multipartIdentifier + DROP (IF EXISTS)? partitionSpec (COMMA partitionSpec)* PURGE? #dropTablePartitions + | ALTER TABLE multipartIdentifier + (partitionSpec)? SET locationSpec #setTableLocation + | ALTER TABLE multipartIdentifier RECOVER PARTITIONS #recoverPartitions + | DROP TABLE (IF EXISTS)? multipartIdentifier PURGE? #dropTable + | DROP VIEW (IF EXISTS)? multipartIdentifier #dropView + | CREATE (OR REPLACE)? (GLOBAL? TEMPORARY)? + VIEW (IF NOT EXISTS)? multipartIdentifier + identifierCommentList? + (commentSpec | + (PARTITIONED ON identifierList) | + (TBLPROPERTIES propertyList))* + AS query #createView + | CREATE (OR REPLACE)? GLOBAL? TEMPORARY VIEW + tableIdentifier (LEFT_PAREN colTypeList RIGHT_PAREN)? tableProvider + (OPTIONS propertyList)? #createTempViewUsing + | ALTER VIEW multipartIdentifier AS? query #alterViewQuery + | CREATE (OR REPLACE)? TEMPORARY? FUNCTION (IF NOT EXISTS)? + multipartIdentifier AS className=stringLit + (USING resource (COMMA resource)*)? #createFunction + | DROP TEMPORARY? FUNCTION (IF EXISTS)? multipartIdentifier #dropFunction + | EXPLAIN (LOGICAL | FORMATTED | EXTENDED | CODEGEN | COST)? + statement #explain + | SHOW TABLES ((FROM | IN) multipartIdentifier)? + (LIKE? pattern=stringLit)? #showTables + | SHOW TABLE EXTENDED ((FROM | IN) ns=multipartIdentifier)? + LIKE pattern=stringLit partitionSpec? #showTableExtended + | SHOW TBLPROPERTIES table=multipartIdentifier + (LEFT_PAREN key=propertyKey RIGHT_PAREN)? #showTblProperties + | SHOW COLUMNS (FROM | IN) table=multipartIdentifier + ((FROM | IN) ns=multipartIdentifier)? #showColumns + | SHOW VIEWS ((FROM | IN) multipartIdentifier)? + (LIKE? pattern=stringLit)? #showViews + | SHOW PARTITIONS multipartIdentifier partitionSpec? #showPartitions + | SHOW identifier? FUNCTIONS ((FROM | IN) ns=multipartIdentifier)? + (LIKE? (legacy=multipartIdentifier | pattern=stringLit))? #showFunctions + | SHOW CREATE TABLE multipartIdentifier (AS SERDE)? #showCreateTable + | SHOW CURRENT namespace #showCurrentNamespace + | SHOW CATALOGS (LIKE? pattern=stringLit)? #showCatalogs + | (DESC | DESCRIBE) FUNCTION EXTENDED? describeFuncName #describeFunction + | (DESC | DESCRIBE) namespace EXTENDED? + multipartIdentifier #describeNamespace + | (DESC | DESCRIBE) TABLE? option=(EXTENDED | FORMATTED)? + multipartIdentifier partitionSpec? describeColName? #describeRelation + | (DESC | DESCRIBE) QUERY? query #describeQuery + | COMMENT ON namespace multipartIdentifier IS + comment #commentNamespace + | COMMENT ON TABLE multipartIdentifier IS comment #commentTable + | REFRESH TABLE multipartIdentifier #refreshTable + | REFRESH FUNCTION multipartIdentifier #refreshFunction + | REFRESH (stringLit | .*?) #refreshResource + | CACHE LAZY? TABLE multipartIdentifier + (OPTIONS options=propertyList)? (AS? query)? #cacheTable + | UNCACHE TABLE (IF EXISTS)? multipartIdentifier #uncacheTable + | CLEAR CACHE #clearCache + | LOAD DATA LOCAL? INPATH path=stringLit OVERWRITE? INTO TABLE + multipartIdentifier partitionSpec? #loadData + | TRUNCATE TABLE multipartIdentifier partitionSpec? #truncateTable + | (MSCK)? REPAIR TABLE multipartIdentifier + (option=(ADD|DROP|SYNC) PARTITIONS)? #repairTable + | op=(ADD | LIST) identifier .*? #manageResource + | SET ROLE .*? #failNativeCommand + | SET TIME ZONE interval #setTimeZone + | SET TIME ZONE timezone #setTimeZone + | SET TIME ZONE .*? #setTimeZone + | SET configKey EQ configValue #setQuotedConfiguration + | SET configKey (EQ .*?)? #setConfiguration + | SET .*? EQ configValue #setQuotedConfiguration + | SET .*? #setConfiguration + | RESET configKey #resetQuotedConfiguration + | RESET .*? #resetConfiguration + | CREATE INDEX (IF NOT EXISTS)? identifier ON TABLE? + multipartIdentifier (USING indexType=identifier)? + LEFT_PAREN columns=multipartIdentifierPropertyList RIGHT_PAREN + (OPTIONS options=propertyList)? #createIndex + | DROP INDEX (IF EXISTS)? identifier ON TABLE? multipartIdentifier #dropIndex + | unsupportedHiveNativeCommands .*? #failNativeCommand + ; + +timezone + : stringLit + | LOCAL + ; + +configKey + : quotedIdentifier + ; + +configValue + : backQuotedIdentifier + ; + +unsupportedHiveNativeCommands + : kw1=CREATE kw2=ROLE + | kw1=DROP kw2=ROLE + | kw1=GRANT kw2=ROLE? + | kw1=REVOKE kw2=ROLE? + | kw1=SHOW kw2=GRANT + | kw1=SHOW kw2=ROLE kw3=GRANT? + | kw1=SHOW kw2=PRINCIPALS + | kw1=SHOW kw2=ROLES + | kw1=SHOW kw2=CURRENT kw3=ROLES + | kw1=EXPORT kw2=TABLE + | kw1=IMPORT kw2=TABLE + | kw1=SHOW kw2=COMPACTIONS + | kw1=SHOW kw2=CREATE kw3=TABLE + | kw1=SHOW kw2=TRANSACTIONS + | kw1=SHOW kw2=INDEXES + | kw1=SHOW kw2=LOCKS + | kw1=CREATE kw2=INDEX + | kw1=DROP kw2=INDEX + | kw1=ALTER kw2=INDEX + | kw1=LOCK kw2=TABLE + | kw1=LOCK kw2=DATABASE + | kw1=UNLOCK kw2=TABLE + | kw1=UNLOCK kw2=DATABASE + | kw1=CREATE kw2=TEMPORARY kw3=MACRO + | kw1=DROP kw2=TEMPORARY kw3=MACRO + | kw1=ALTER kw2=TABLE tableIdentifier kw3=NOT kw4=CLUSTERED + | kw1=ALTER kw2=TABLE tableIdentifier kw3=CLUSTERED kw4=BY + | kw1=ALTER kw2=TABLE tableIdentifier kw3=NOT kw4=SORTED + | kw1=ALTER kw2=TABLE tableIdentifier kw3=SKEWED kw4=BY + | kw1=ALTER kw2=TABLE tableIdentifier kw3=NOT kw4=SKEWED + | kw1=ALTER kw2=TABLE tableIdentifier kw3=NOT kw4=STORED kw5=AS kw6=DIRECTORIES + | kw1=ALTER kw2=TABLE tableIdentifier kw3=SET kw4=SKEWED kw5=LOCATION + | kw1=ALTER kw2=TABLE tableIdentifier kw3=EXCHANGE kw4=PARTITION + | kw1=ALTER kw2=TABLE tableIdentifier kw3=ARCHIVE kw4=PARTITION + | kw1=ALTER kw2=TABLE tableIdentifier kw3=UNARCHIVE kw4=PARTITION + | kw1=ALTER kw2=TABLE tableIdentifier kw3=TOUCH + | kw1=ALTER kw2=TABLE tableIdentifier partitionSpec? kw3=COMPACT + | kw1=ALTER kw2=TABLE tableIdentifier partitionSpec? kw3=CONCATENATE + | kw1=ALTER kw2=TABLE tableIdentifier partitionSpec? kw3=SET kw4=FILEFORMAT + | kw1=ALTER kw2=TABLE tableIdentifier partitionSpec? kw3=REPLACE kw4=COLUMNS + | kw1=START kw2=TRANSACTION + | kw1=COMMIT + | kw1=ROLLBACK + | kw1=DFS + ; + +createTableHeader + : CREATE TEMPORARY? EXTERNAL? TABLE (IF NOT EXISTS)? multipartIdentifier + ; + +replaceTableHeader + : (CREATE OR)? REPLACE TABLE multipartIdentifier + ; + +bucketSpec + : CLUSTERED BY identifierList + (SORTED BY orderedIdentifierList)? + INTO INTEGER_VALUE BUCKETS + ; + +skewSpec + : SKEWED BY identifierList + ON (constantList | nestedConstantList) + (STORED AS DIRECTORIES)? + ; + +locationSpec + : LOCATION stringLit + ; + +commentSpec + : COMMENT stringLit + ; + + +insertInto + : INSERT OVERWRITE TABLE? multipartIdentifier (partitionSpec (IF NOT EXISTS)?)? identifierList? #insertOverwriteTable + | INSERT INTO TABLE? multipartIdentifier partitionSpec? (IF NOT EXISTS)? identifierList? #insertIntoTable + | INSERT INTO TABLE? multipartIdentifier REPLACE whereClause #insertIntoReplaceWhere + | INSERT OVERWRITE LOCAL? DIRECTORY path=stringLit rowFormat? createFileFormat? #insertOverwriteHiveDir + | INSERT OVERWRITE LOCAL? DIRECTORY (path=stringLit)? tableProvider (OPTIONS options=propertyList)? #insertOverwriteDir + ; + +partitionSpecLocation + : partitionSpec locationSpec? + ; + +partitionSpec + : PARTITION LEFT_PAREN partitionVal (COMMA partitionVal)* RIGHT_PAREN + ; + +partitionVal + : identifier (EQ constant)? + | identifier EQ DEFAULT + ; + +namespace + : NAMESPACE + | DATABASE + | SCHEMA + ; + +namespaces + : NAMESPACES + | DATABASES + | SCHEMAS + ; + +describeFuncName + : qualifiedName + | stringLit + | comparisonOperator + | arithmeticOperator + | predicateOperator + ; + +describeColName + : nameParts+=identifier (DOT nameParts+=identifier)* + ; + +ctes + : WITH namedQuery (COMMA namedQuery)* + ; + +query + : ctes? queryTerm queryOrganization + ; + +namedQuery + : name=errorCapturingIdentifier (columnAliases=identifierList)? AS? LEFT_PAREN query RIGHT_PAREN + ; + +queryTerm + : queryPrimary #queryTermDefault + | left=queryTerm + operator=(INTERSECT | UNION | EXCEPT | SETMINUS) setQuantifier? right=queryTerm #setOperation + ; + +querySpecification + : transformClause + fromClause? + lateralView* + whereClause? + aggregationClause? + havingClause? + windowClause? #transformQuerySpecification + | selectClause + fromClause? + lateralView* + whereClause? + aggregationClause? + havingClause? + windowClause? #regularQuerySpecification + ; + +queryPrimary + : querySpecification #queryPrimaryDefault + | fromStatement #fromStmt + | TABLE multipartIdentifier #table + | inlineTable #inlineTableDefault1 + | LEFT_PAREN query RIGHT_PAREN #subquery + ; + +tableProvider + : USING multipartIdentifier + ; + +createTableClauses + :((OPTIONS options=propertyList) | + (PARTITIONED BY partitioning=partitionFieldList) | + skewSpec | + bucketSpec | + rowFormat | + createFileFormat | + locationSpec | + commentSpec | + (TBLPROPERTIES tableProps=propertyList))* + ; + +propertyList + : LEFT_PAREN property (COMMA property)* RIGHT_PAREN + ; + +property + : key=propertyKey (EQ? value=propertyValue)? + ; + +propertyKey + : identifier (DOT identifier)* + | stringLit + ; + +propertyValue + : INTEGER_VALUE + | DECIMAL_VALUE + | booleanValue + | stringLit + ; + +constantList + : LEFT_PAREN constant (COMMA constant)* RIGHT_PAREN + ; + +nestedConstantList + : LEFT_PAREN constantList (COMMA constantList)* RIGHT_PAREN + ; + +createFileFormat + : STORED AS fileFormat + | STORED BY storageHandler + ; + +fileFormat + : INPUTFORMAT inFmt=stringLit OUTPUTFORMAT outFmt=stringLit #tableFileFormat + | identifier #genericFileFormat + ; + +storageHandler + : stringLit (WITH SERDEPROPERTIES propertyList)? + ; + +resource + : identifier stringLit + ; + +dmlStatementNoWith + : insertInto query #singleInsertQuery + | fromClause multiInsertQueryBody+ #multiInsertQuery + | DELETE FROM multipartIdentifier tableAlias whereClause? #deleteFromTable + | UPDATE multipartIdentifier tableAlias setClause whereClause? #updateTable + | MERGE INTO target=multipartIdentifier targetAlias=tableAlias + USING (source=multipartIdentifier | + LEFT_PAREN sourceQuery=query RIGHT_PAREN) sourceAlias=tableAlias + ON mergeCondition=booleanExpression + matchedClause* + notMatchedClause* + notMatchedBySourceClause* #mergeIntoTable + ; + +queryOrganization + : (ORDER BY order+=sortItem (COMMA order+=sortItem)*)? + (CLUSTER BY clusterBy+=expression (COMMA clusterBy+=expression)*)? + (DISTRIBUTE BY distributeBy+=expression (COMMA distributeBy+=expression)*)? + (SORT BY sort+=sortItem (COMMA sort+=sortItem)*)? + windowClause? + (LIMIT (ALL | limit=expression))? + (OFFSET offset=expression)? + ; + +multiInsertQueryBody + : insertInto fromStatementBody + ; + +sortItem + : expression ordering=(ASC | DESC)? (NULLS nullOrder=(LAST | FIRST))? + ; + +fromStatement + : fromClause fromStatementBody+ + ; + +fromStatementBody + : transformClause + whereClause? + queryOrganization + | selectClause + lateralView* + whereClause? + aggregationClause? + havingClause? + windowClause? + queryOrganization + ; + + + +transformClause + : (SELECT kind=TRANSFORM LEFT_PAREN setQuantifier? expressionSeq RIGHT_PAREN + | kind=MAP setQuantifier? expressionSeq + | kind=REDUCE setQuantifier? expressionSeq) + inRowFormat=rowFormat? + (RECORDWRITER recordWriter=stringLit)? + USING script=stringLit + (AS (identifierSeq | colTypeList | (LEFT_PAREN (identifierSeq | colTypeList) RIGHT_PAREN)))? + outRowFormat=rowFormat? + (RECORDREADER recordReader=stringLit)? + ; + +selectClause + : SELECT (hints+=hint)* setQuantifier? namedExpressionSeq + ; + +setClause + : SET assignmentList + ; + +matchedClause + : WHEN MATCHED (AND matchedCond=booleanExpression)? THEN matchedAction + ; +notMatchedClause + : WHEN NOT MATCHED (BY TARGET)? (AND notMatchedCond=booleanExpression)? THEN notMatchedAction + ; + +notMatchedBySourceClause + : WHEN NOT MATCHED BY SOURCE (AND notMatchedBySourceCond=booleanExpression)? THEN notMatchedBySourceAction + ; + +matchedAction + : DELETE + | UPDATE SET ASTERISK + | UPDATE SET assignmentList + ; + +notMatchedAction + : INSERT ASTERISK + | INSERT LEFT_PAREN columns=multipartIdentifierList RIGHT_PAREN + VALUES LEFT_PAREN expression (COMMA expression)* RIGHT_PAREN + ; + +notMatchedBySourceAction + : DELETE + | UPDATE SET assignmentList + ; + +assignmentList + : assignment (COMMA assignment)* + ; + +assignment + : key=multipartIdentifier EQ value=expression + ; + +whereClause + : WHERE booleanExpression + ; + +havingClause + : HAVING booleanExpression + ; + +hint + : HENT_START hintStatements+=hintStatement (COMMA? hintStatements+=hintStatement)* HENT_END + ; + +hintStatement + : hintName=identifier + | hintName=identifier LEFT_PAREN parameters+=primaryExpression (COMMA parameters+=primaryExpression)* RIGHT_PAREN + ; + +fromClause + : FROM relation (COMMA relation)* lateralView* pivotClause? unpivotClause? + ; + +temporalClause + : FOR? (SYSTEM_VERSION | VERSION) AS OF version + | FOR? (SYSTEM_TIME | TIMESTAMP) AS OF timestamp=valueExpression + ; + +aggregationClause + : GROUP BY groupingExpressionsWithGroupingAnalytics+=groupByClause + (COMMA groupingExpressionsWithGroupingAnalytics+=groupByClause)* + | GROUP BY groupingExpressions+=expression (COMMA groupingExpressions+=expression)* ( + WITH kind=ROLLUP + | WITH kind=CUBE + | kind=GROUPING SETS LEFT_PAREN groupingSet (COMMA groupingSet)* RIGHT_PAREN)? + ; + +groupByClause + : groupingAnalytics + | expression + ; + +groupingAnalytics + : (ROLLUP | CUBE) LEFT_PAREN groupingSet (COMMA groupingSet)* RIGHT_PAREN + | GROUPING SETS LEFT_PAREN groupingElement (COMMA groupingElement)* RIGHT_PAREN + ; + +groupingElement + : groupingAnalytics + | groupingSet + ; + +groupingSet + : LEFT_PAREN (expression (COMMA expression)*)? RIGHT_PAREN + | expression + ; + +pivotClause + : PIVOT LEFT_PAREN aggregates=namedExpressionSeq FOR pivotColumn IN LEFT_PAREN pivotValues+=pivotValue (COMMA pivotValues+=pivotValue)* RIGHT_PAREN RIGHT_PAREN + ; + +pivotColumn + : identifiers+=identifier + | LEFT_PAREN identifiers+=identifier (COMMA identifiers+=identifier)* RIGHT_PAREN + ; + +pivotValue + : expression (AS? identifier)? + ; + +unpivotClause + : UNPIVOT nullOperator=unpivotNullClause? LEFT_PAREN + operator=unpivotOperator + RIGHT_PAREN (AS? identifier)? + ; + +unpivotNullClause + : (INCLUDE | EXCLUDE) NULLS + ; + +unpivotOperator + : (unpivotSingleValueColumnClause | unpivotMultiValueColumnClause) + ; + +unpivotSingleValueColumnClause + : unpivotValueColumn FOR unpivotNameColumn IN LEFT_PAREN unpivotColumns+=unpivotColumnAndAlias (COMMA unpivotColumns+=unpivotColumnAndAlias)* RIGHT_PAREN + ; + +unpivotMultiValueColumnClause + : LEFT_PAREN unpivotValueColumns+=unpivotValueColumn (COMMA unpivotValueColumns+=unpivotValueColumn)* RIGHT_PAREN + FOR unpivotNameColumn + IN LEFT_PAREN unpivotColumnSets+=unpivotColumnSet (COMMA unpivotColumnSets+=unpivotColumnSet)* RIGHT_PAREN + ; + +unpivotColumnSet + : LEFT_PAREN unpivotColumns+=unpivotColumn (COMMA unpivotColumns+=unpivotColumn)* RIGHT_PAREN unpivotAlias? + ; + +unpivotValueColumn + : identifier + ; + +unpivotNameColumn + : identifier + ; + +unpivotColumnAndAlias + : unpivotColumn unpivotAlias? + ; + +unpivotColumn + : multipartIdentifier + ; + +unpivotAlias + : AS? identifier + ; + +lateralView + : LATERAL VIEW (OUTER)? qualifiedName LEFT_PAREN (expression (COMMA expression)*)? RIGHT_PAREN tblName=identifier (AS? colName+=identifier (COMMA colName+=identifier)*)? + ; + +setQuantifier + : DISTINCT + | ALL + ; + +relation + : LATERAL? relationPrimary relationExtension* + ; + +relationExtension + : joinRelation + | pivotClause + | unpivotClause + ; + +joinRelation + : (joinType) JOIN LATERAL? right=relationPrimary joinCriteria? + | NATURAL joinType JOIN LATERAL? right=relationPrimary + ; + +joinType + : INNER? + | CROSS + | LEFT OUTER? + | LEFT? SEMI + | RIGHT OUTER? + | FULL OUTER? + | LEFT? ANTI + ; + +joinCriteria + : ON booleanExpression + | USING identifierList + ; + +sample + : TABLESAMPLE LEFT_PAREN sampleMethod? RIGHT_PAREN (REPEATABLE LEFT_PAREN seed=INTEGER_VALUE RIGHT_PAREN)? + ; + +sampleMethod + : negativeSign=MINUS? percentage=(INTEGER_VALUE | DECIMAL_VALUE) PERCENTLIT #sampleByPercentile + | expression ROWS #sampleByRows + | sampleType=BUCKET numerator=INTEGER_VALUE OUT OF denominator=INTEGER_VALUE + (ON (identifier | qualifiedName LEFT_PAREN RIGHT_PAREN))? #sampleByBucket + | bytes=expression #sampleByBytes + ; + +identifierList + : LEFT_PAREN identifierSeq RIGHT_PAREN + ; + +identifierSeq + : ident+=errorCapturingIdentifier (COMMA ident+=errorCapturingIdentifier)* + ; + +orderedIdentifierList + : LEFT_PAREN orderedIdentifier (COMMA orderedIdentifier)* RIGHT_PAREN + ; + +orderedIdentifier + : ident=errorCapturingIdentifier ordering=(ASC | DESC)? + ; + +identifierCommentList + : LEFT_PAREN identifierComment (COMMA identifierComment)* RIGHT_PAREN + ; + +identifierComment + : identifier commentSpec? + ; + +relationPrimary + : multipartIdentifier temporalClause? + sample? tableAlias #tableName + | LEFT_PAREN query RIGHT_PAREN sample? tableAlias #aliasedQuery + | LEFT_PAREN relation RIGHT_PAREN sample? tableAlias #aliasedRelation + | inlineTable #inlineTableDefault2 + | functionTable #tableValuedFunction + ; + +inlineTable + : VALUES expression (COMMA expression)* tableAlias + ; + +functionTable + : funcName=functionName LEFT_PAREN (expression (COMMA expression)*)? RIGHT_PAREN tableAlias + ; + +tableAlias + : (AS? strictIdentifier identifierList?)? + ; + +rowFormat + : ROW FORMAT SERDE name=stringLit (WITH SERDEPROPERTIES props=propertyList)? #rowFormatSerde + | ROW FORMAT DELIMITED + (FIELDS TERMINATED BY fieldsTerminatedBy=stringLit (ESCAPED BY escapedBy=stringLit)?)? + (COLLECTION ITEMS TERMINATED BY collectionItemsTerminatedBy=stringLit)? + (MAP KEYS TERMINATED BY keysTerminatedBy=stringLit)? + (LINES TERMINATED BY linesSeparatedBy=stringLit)? + (NULL DEFINED AS nullDefinedAs=stringLit)? #rowFormatDelimited + ; + +multipartIdentifierList + : multipartIdentifier (COMMA multipartIdentifier)* + ; + +multipartIdentifier + : parts+=errorCapturingIdentifier (DOT parts+=errorCapturingIdentifier)* + ; + +multipartIdentifierPropertyList + : multipartIdentifierProperty (COMMA multipartIdentifierProperty)* + ; + +multipartIdentifierProperty + : multipartIdentifier (OPTIONS options=propertyList)? + ; + +tableIdentifier + : (db=errorCapturingIdentifier DOT)? table=errorCapturingIdentifier + ; + +functionIdentifier + : (db=errorCapturingIdentifier DOT)? function=errorCapturingIdentifier + ; + +namedExpression + : expression (AS? (name=errorCapturingIdentifier | identifierList))? + ; + +namedExpressionSeq + : namedExpression (COMMA namedExpression)* + ; + +partitionFieldList + : LEFT_PAREN fields+=partitionField (COMMA fields+=partitionField)* RIGHT_PAREN + ; + +partitionField + : transform #partitionTransform + | colType #partitionColumn + ; + +transform + : qualifiedName #identityTransform + | transformName=identifier + LEFT_PAREN argument+=transformArgument (COMMA argument+=transformArgument)* RIGHT_PAREN #applyTransform + ; + +transformArgument + : qualifiedName + | constant + ; + +expression + : booleanExpression + ; + +expressionSeq + : expression (COMMA expression)* + ; + +booleanExpression + : NOT booleanExpression #logicalNot + | EXISTS LEFT_PAREN query RIGHT_PAREN #exists + | valueExpression predicate? #predicated + | left=booleanExpression operator=AND right=booleanExpression #logicalBinary + | left=booleanExpression operator=OR right=booleanExpression #logicalBinary + ; + +predicate + : NOT? kind=BETWEEN lower=valueExpression AND upper=valueExpression + | NOT? kind=IN LEFT_PAREN expression (COMMA expression)* RIGHT_PAREN + | NOT? kind=IN LEFT_PAREN query RIGHT_PAREN + | NOT? kind=RLIKE pattern=valueExpression + | NOT? kind=(LIKE | ILIKE) quantifier=(ANY | SOME | ALL) (LEFT_PAREN RIGHT_PAREN | LEFT_PAREN expression (COMMA expression)* RIGHT_PAREN) + | NOT? kind=(LIKE | ILIKE) pattern=valueExpression (ESCAPE escapeChar=stringLit)? + | IS NOT? kind=NULL + | IS NOT? kind=(TRUE | FALSE | UNKNOWN) + | IS NOT? kind=DISTINCT FROM right=valueExpression + ; + +valueExpression + : primaryExpression #valueExpressionDefault + | operator=(MINUS | PLUS | TILDE) valueExpression #arithmeticUnary + | left=valueExpression operator=(ASTERISK | SLASH | PERCENT | DIV) right=valueExpression #arithmeticBinary + | left=valueExpression operator=(PLUS | MINUS | CONCAT_PIPE) right=valueExpression #arithmeticBinary + | left=valueExpression operator=AMPERSAND right=valueExpression #arithmeticBinary + | left=valueExpression operator=HAT right=valueExpression #arithmeticBinary + | left=valueExpression operator=PIPE right=valueExpression #arithmeticBinary + | left=valueExpression comparisonOperator right=valueExpression #comparison + ; + +datetimeUnit + : YEAR | QUARTER | MONTH + | WEEK | DAY | DAYOFYEAR + | HOUR | MINUTE | SECOND | MILLISECOND | MICROSECOND + ; + +primaryExpression + : name=(CURRENT_DATE | CURRENT_TIMESTAMP | CURRENT_USER | USER) #currentLike + | name=(TIMESTAMPADD | DATEADD) LEFT_PAREN unit=datetimeUnit COMMA unitsAmount=valueExpression COMMA timestamp=valueExpression RIGHT_PAREN #timestampadd + | name=(TIMESTAMPDIFF | DATEDIFF) LEFT_PAREN unit=datetimeUnit COMMA startTimestamp=valueExpression COMMA endTimestamp=valueExpression RIGHT_PAREN #timestampdiff + | CASE whenClause+ (ELSE elseExpression=expression)? END #searchedCase + | CASE value=expression whenClause+ (ELSE elseExpression=expression)? END #simpleCase + | name=(CAST | TRY_CAST) LEFT_PAREN expression AS dataType RIGHT_PAREN #cast + | STRUCT LEFT_PAREN (argument+=namedExpression (COMMA argument+=namedExpression)*)? RIGHT_PAREN #struct + | FIRST LEFT_PAREN expression (IGNORE NULLS)? RIGHT_PAREN #first + | ANY_VALUE LEFT_PAREN expression (IGNORE NULLS)? RIGHT_PAREN #any_value + | LAST LEFT_PAREN expression (IGNORE NULLS)? RIGHT_PAREN #last + | POSITION LEFT_PAREN substr=valueExpression IN str=valueExpression RIGHT_PAREN #position + | constant #constantDefault + | ASTERISK #star + | qualifiedName DOT ASTERISK #star + | LEFT_PAREN namedExpression (COMMA namedExpression)+ RIGHT_PAREN #rowConstructor + | LEFT_PAREN query RIGHT_PAREN #subqueryExpression + | functionName LEFT_PAREN (setQuantifier? argument+=expression (COMMA argument+=expression)*)? RIGHT_PAREN + (FILTER LEFT_PAREN WHERE where=booleanExpression RIGHT_PAREN)? + (nullsOption=(IGNORE | RESPECT) NULLS)? ( OVER windowSpec)? #functionCall + | identifier ARROW expression #lambda + | LEFT_PAREN identifier (COMMA identifier)+ RIGHT_PAREN ARROW expression #lambda + | value=primaryExpression LEFT_BRACKET index=valueExpression RIGHT_BRACKET #subscript + | identifier #columnReference + | base=primaryExpression DOT fieldName=identifier #dereference + | LEFT_PAREN expression RIGHT_PAREN #parenthesizedExpression + | EXTRACT LEFT_PAREN field=identifier FROM source=valueExpression RIGHT_PAREN #extract + | (SUBSTR | SUBSTRING) LEFT_PAREN str=valueExpression (FROM | COMMA) pos=valueExpression + ((FOR | COMMA) len=valueExpression)? RIGHT_PAREN #substring + | TRIM LEFT_PAREN trimOption=(BOTH | LEADING | TRAILING)? (trimStr=valueExpression)? + FROM srcStr=valueExpression RIGHT_PAREN #trim + | OVERLAY LEFT_PAREN input=valueExpression PLACING replace=valueExpression + FROM position=valueExpression (FOR length=valueExpression)? RIGHT_PAREN #overlay + | name=(PERCENTILE_CONT | PERCENTILE_DISC) LEFT_PAREN percentage=valueExpression RIGHT_PAREN + WITHIN GROUP LEFT_PAREN ORDER BY sortItem RIGHT_PAREN + (FILTER LEFT_PAREN WHERE where=booleanExpression RIGHT_PAREN)? ( OVER windowSpec)? #percentile + ; + +constant + : NULL #nullLiteral + | COLON identifier #parameterLiteral + | interval #intervalLiteral + | identifier stringLit #typeConstructor + | number #numericLiteral + | booleanValue #booleanLiteral + | stringLit+ #stringLiteral + ; + +comparisonOperator + : EQ | NEQ | NEQJ | LT | LTE | GT | GTE | NSEQ + ; + +arithmeticOperator + : PLUS | MINUS | ASTERISK | SLASH | PERCENT | DIV | TILDE | AMPERSAND | PIPE | CONCAT_PIPE | HAT + ; + +predicateOperator + : OR | AND | IN | NOT + ; + +booleanValue + : TRUE | FALSE + ; + +interval + : INTERVAL (errorCapturingMultiUnitsInterval | errorCapturingUnitToUnitInterval) + ; + +errorCapturingMultiUnitsInterval + : body=multiUnitsInterval unitToUnitInterval? + ; + +multiUnitsInterval + : (intervalValue unit+=unitInMultiUnits)+ + ; + +errorCapturingUnitToUnitInterval + : body=unitToUnitInterval (error1=multiUnitsInterval | error2=unitToUnitInterval)? + ; + +unitToUnitInterval + : value=intervalValue from=unitInUnitToUnit TO to=unitInUnitToUnit + ; + +intervalValue + : (PLUS | MINUS)? + (INTEGER_VALUE | DECIMAL_VALUE | stringLit) + ; + +unitInMultiUnits + : NANOSECOND | NANOSECONDS | MICROSECOND | MICROSECONDS | MILLISECOND | MILLISECONDS + | SECOND | SECONDS | MINUTE | MINUTES | HOUR | HOURS | DAY | DAYS | WEEK | WEEKS + | MONTH | MONTHS | YEAR | YEARS + ; + +unitInUnitToUnit + : SECOND | MINUTE | HOUR | DAY | MONTH | YEAR + ; + +colPosition + : position=FIRST | position=AFTER afterCol=errorCapturingIdentifier + ; + +dataType + : complex=ARRAY LT dataType GT #complexDataType + | complex=MAP LT dataType COMMA dataType GT #complexDataType + | complex=STRUCT (LT complexColTypeList? GT | NEQ) #complexDataType + | INTERVAL from=(YEAR | MONTH) (TO to=MONTH)? #yearMonthIntervalDataType + | INTERVAL from=(DAY | HOUR | MINUTE | SECOND) + (TO to=(HOUR | MINUTE | SECOND))? #dayTimeIntervalDataType + | identifier (LEFT_PAREN INTEGER_VALUE + (COMMA INTEGER_VALUE)* RIGHT_PAREN)? #primitiveDataType + ; + +qualifiedColTypeWithPositionList + : qualifiedColTypeWithPosition (COMMA qualifiedColTypeWithPosition)* + ; + +qualifiedColTypeWithPosition + : name=multipartIdentifier dataType colDefinitionDescriptorWithPosition* + ; + +colDefinitionDescriptorWithPosition + : NOT NULL + | defaultExpression + | commentSpec + | colPosition + ; + +defaultExpression + : DEFAULT expression + ; + +colTypeList + : colType (COMMA colType)* + ; + +colType + : colName=errorCapturingIdentifier dataType (NOT NULL)? commentSpec? + ; + +createOrReplaceTableColTypeList + : createOrReplaceTableColType (COMMA createOrReplaceTableColType)* + ; + +createOrReplaceTableColType + : colName=errorCapturingIdentifier dataType colDefinitionOption* + ; + +colDefinitionOption + : NOT NULL + | defaultExpression + | generationExpression + | commentSpec + ; + +generationExpression + : GENERATED ALWAYS AS LEFT_PAREN expression RIGHT_PAREN + ; + +complexColTypeList + : complexColType (COMMA complexColType)* + ; + +complexColType + : identifier COLON? dataType (NOT NULL)? commentSpec? + ; + +whenClause + : WHEN condition=expression THEN result=expression + ; + +windowClause + : WINDOW namedWindow (COMMA namedWindow)* + ; + +namedWindow + : name=errorCapturingIdentifier AS windowSpec + ; + +windowSpec + : name=errorCapturingIdentifier #windowRef + | LEFT_PAREN name=errorCapturingIdentifier RIGHT_PAREN #windowRef + | LEFT_PAREN + ( CLUSTER BY partition+=expression (COMMA partition+=expression)* + | ((PARTITION | DISTRIBUTE) BY partition+=expression (COMMA partition+=expression)*)? + ((ORDER | SORT) BY sortItem (COMMA sortItem)*)?) + windowFrame? + RIGHT_PAREN #windowDef + ; + +windowFrame + : frameType=RANGE start=frameBound + | frameType=ROWS start=frameBound + | frameType=RANGE BETWEEN start=frameBound AND end=frameBound + | frameType=ROWS BETWEEN start=frameBound AND end=frameBound + ; + +frameBound + : UNBOUNDED boundType=(PRECEDING | FOLLOWING) + | boundType=CURRENT ROW + | expression boundType=(PRECEDING | FOLLOWING) + ; + +qualifiedNameList + : qualifiedName (COMMA qualifiedName)* + ; + +functionName + : qualifiedName + | FILTER + | LEFT + | RIGHT + ; + +qualifiedName + : identifier (DOT identifier)* + ; + +// this rule is used for explicitly capturing wrong identifiers such as test-table, which should actually be `test-table` +// replace identifier with errorCapturingIdentifier where the immediate follow symbol is not an expression, otherwise +// valid expressions such as "a-b" can be recognized as an identifier +errorCapturingIdentifier + : identifier errorCapturingIdentifierExtra + ; + +// extra left-factoring grammar +errorCapturingIdentifierExtra + : (MINUS identifier)+ #errorIdent + | #realIdent + ; + +identifier + : strictIdentifier + | strictNonReserved + ; + +strictIdentifier + : IDENTIFIER #unquotedIdentifier + | quotedIdentifier #quotedIdentifierAlternative + | ansiNonReserved #unquotedIdentifier + | nonReserved #unquotedIdentifier + ; + +quotedIdentifier + : BACKQUOTED_IDENTIFIER + | DOUBLEQUOTED_STRING + ; + +backQuotedIdentifier + : BACKQUOTED_IDENTIFIER + ; + +number + : MINUS? EXPONENT_VALUE #exponentLiteral + | MINUS? DECIMAL_VALUE #decimalLiteral + | MINUS? (EXPONENT_VALUE | DECIMAL_VALUE) #legacyDecimalLiteral + | MINUS? INTEGER_VALUE #integerLiteral + | MINUS? BIGINT_LITERAL #bigIntLiteral + | MINUS? SMALLINT_LITERAL #smallIntLiteral + | MINUS? TINYINT_LITERAL #tinyIntLiteral + | MINUS? DOUBLE_LITERAL #doubleLiteral + | MINUS? FLOAT_LITERAL #floatLiteral + | MINUS? BIGDECIMAL_LITERAL #bigDecimalLiteral + ; + +alterColumnAction + : TYPE dataType + | commentSpec + | colPosition + | setOrDrop=(SET | DROP) NOT NULL + | SET defaultExpression + | dropDefault=DROP DEFAULT + ; + +stringLit + : STRING + | DOUBLEQUOTED_STRING + ; + +comment + : stringLit + | NULL + ; + +version + : INTEGER_VALUE + | stringLit + ; + +// When `SQL_standard_keyword_behavior=true`, there are 2 kinds of keywords in Spark SQL. +// - Reserved keywords: +// Keywords that are reserved and can't be used as identifiers for table, view, column, +// function, alias, etc. +// - Non-reserved keywords: +// Keywords that have a special meaning only in particular contexts and can be used as +// identifiers in other contexts. For example, `EXPLAIN SELECT ...` is a command, but EXPLAIN +// can be used as identifiers in other places. +// You can find the full keywords list by searching "Start of the keywords list" in this file. +// The non-reserved keywords are listed below. Keywords not in this list are reserved keywords. +ansiNonReserved +//--ANSI-NON-RESERVED-START + : ADD + | AFTER + | ALTER + | ALWAYS + | ANALYZE + | ANTI + | ANY_VALUE + | ARCHIVE + | ARRAY + | ASC + | AT + | BETWEEN + | BUCKET + | BUCKETS + | BY + | CACHE + | CASCADE + | CATALOG + | CATALOGS + | CHANGE + | CLEAR + | CLUSTER + | CLUSTERED + | CODEGEN + | COLLECTION + | COLUMNS + | COMMENT + | COMMIT + | COMPACT + | COMPACTIONS + | COMPUTE + | CONCATENATE + | COST + | CUBE + | CURRENT + | DATA + | DATABASE + | DATABASES + | DATEADD + | DATEDIFF + | DAY + | DAYS + | DAYOFYEAR + | DBPROPERTIES + | DEFAULT + | DEFINED + | DELETE + | DELIMITED + | DESC + | DESCRIBE + | DFS + | DIRECTORIES + | DIRECTORY + | DISTRIBUTE + | DIV + | DROP + | ESCAPED + | EXCHANGE + | EXCLUDE + | EXISTS + | EXPLAIN + | EXPORT + | EXTENDED + | EXTERNAL + | EXTRACT + | FIELDS + | FILEFORMAT + | FIRST + | FOLLOWING + | FORMAT + | FORMATTED + | FUNCTION + | FUNCTIONS + | GENERATED + | GLOBAL + | GROUPING + | HOUR + | HOURS + | IF + | IGNORE + | IMPORT + | INCLUDE + | INDEX + | INDEXES + | INPATH + | INPUTFORMAT + | INSERT + | INTERVAL + | ITEMS + | KEYS + | LAST + | LAZY + | LIKE + | ILIKE + | LIMIT + | LINES + | LIST + | LOAD + | LOCAL + | LOCATION + | LOCK + | LOCKS + | LOGICAL + | MACRO + | MAP + | MATCHED + | MERGE + | MICROSECOND + | MICROSECONDS + | MILLISECOND + | MILLISECONDS + | MINUTE + | MINUTES + | MONTH + | MONTHS + | MSCK + | NAMESPACE + | NAMESPACES + | NANOSECOND + | NANOSECONDS + | NO + | NULLS + | OF + | OPTION + | OPTIONS + | OUT + | OUTPUTFORMAT + | OVER + | OVERLAY + | OVERWRITE + | PARTITION + | PARTITIONED + | PARTITIONS + | PERCENTLIT + | PIVOT + | PLACING + | POSITION + | PRECEDING + | PRINCIPALS + | PROPERTIES + | PURGE + | QUARTER + | QUERY + | RANGE + | RECORDREADER + | RECORDWRITER + | RECOVER + | REDUCE + | REFRESH + | RENAME + | REPAIR + | REPEATABLE + | REPLACE + | RESET + | RESPECT + | RESTRICT + | REVOKE + | RLIKE + | ROLE + | ROLES + | ROLLBACK + | ROLLUP + | ROW + | ROWS + | SCHEMA + | SCHEMAS + | SECOND + | SECONDS + | SEMI + | SEPARATED + | SERDE + | SERDEPROPERTIES + | SET + | SETMINUS + | SETS + | SHOW + | SKEWED + | SORT + | SORTED + | SOURCE + | START + | STATISTICS + | STORED + | STRATIFY + | STRUCT + | SUBSTR + | SUBSTRING + | SYNC + | SYSTEM_TIME + | SYSTEM_VERSION + | TABLES + | TABLESAMPLE + | TARGET + | TBLPROPERTIES + | TEMPORARY + | TERMINATED + | TIMESTAMP + | TIMESTAMPADD + | TIMESTAMPDIFF + | TOUCH + | TRANSACTION + | TRANSACTIONS + | TRANSFORM + | TRIM + | TRUE + | TRUNCATE + | TRY_CAST + | TYPE + | UNARCHIVE + | UNBOUNDED + | UNCACHE + | UNLOCK + | UNPIVOT + | UNSET + | UPDATE + | USE + | VALUES + | VERSION + | VIEW + | VIEWS + | WEEK + | WEEKS + | WINDOW + | YEAR + | YEARS + | ZONE +//--ANSI-NON-RESERVED-END + ; + +// When `SQL_standard_keyword_behavior=false`, there are 2 kinds of keywords in Spark SQL. +// - Non-reserved keywords: +// Same definition as the one when `SQL_standard_keyword_behavior=true`. +// - Strict-non-reserved keywords: +// A strict version of non-reserved keywords, which can not be used as table alias. +// You can find the full keywords list by searching "Start of the keywords list" in this file. +// The strict-non-reserved keywords are listed in `strictNonReserved`. +// The non-reserved keywords are listed in `nonReserved`. +// These 2 together contain all the keywords. +strictNonReserved + : ANTI + | CROSS + | EXCEPT + | FULL + | INNER + | INTERSECT + | JOIN + | LATERAL + | LEFT + | NATURAL + | ON + | RIGHT + | SEMI + | SETMINUS + | UNION + | USING + ; + +nonReserved +//--DEFAULT-NON-RESERVED-START + : ADD + | AFTER + | ALL + | ALTER + | ALWAYS + | ANALYZE + | AND + | ANY + | ANY_VALUE + | ARCHIVE + | ARRAY + | AS + | ASC + | AT + | AUTHORIZATION + | BETWEEN + | BOTH + | BUCKET + | BUCKETS + | BY + | CACHE + | CASCADE + | CASE + | CAST + | CATALOG + | CATALOGS + | CHANGE + | CHECK + | CLEAR + | CLUSTER + | CLUSTERED + | CODEGEN + | COLLATE + | COLLECTION + | COLUMN + | COLUMNS + | COMMENT + | COMMIT + | COMPACT + | COMPACTIONS + | COMPUTE + | CONCATENATE + | CONSTRAINT + | COST + | CREATE + | CUBE + | CURRENT + | CURRENT_DATE + | CURRENT_TIME + | CURRENT_TIMESTAMP + | CURRENT_USER + | DATA + | DATABASE + | DATABASES + | DATEADD + | DATEDIFF + | DAY + | DAYS + | DAYOFYEAR + | DBPROPERTIES + | DEFAULT + | DEFINED + | DELETE + | DELIMITED + | DESC + | DESCRIBE + | DFS + | DIRECTORIES + | DIRECTORY + | DISTINCT + | DISTRIBUTE + | DIV + | DROP + | ELSE + | END + | ESCAPE + | ESCAPED + | EXCHANGE + | EXCLUDE + | EXISTS + | EXPLAIN + | EXPORT + | EXTENDED + | EXTERNAL + | EXTRACT + | FALSE + | FETCH + | FILTER + | FIELDS + | FILEFORMAT + | FIRST + | FOLLOWING + | FOR + | FOREIGN + | FORMAT + | FORMATTED + | FROM + | FUNCTION + | FUNCTIONS + | GENERATED + | GLOBAL + | GRANT + | GROUP + | GROUPING + | HAVING + | HOUR + | HOURS + | IF + | IGNORE + | IMPORT + | IN + | INCLUDE + | INDEX + | INDEXES + | INPATH + | INPUTFORMAT + | INSERT + | INTERVAL + | INTO + | IS + | ITEMS + | KEYS + | LAST + | LAZY + | LEADING + | LIKE + | ILIKE + | LIMIT + | LINES + | LIST + | LOAD + | LOCAL + | LOCATION + | LOCK + | LOCKS + | LOGICAL + | MACRO + | MAP + | MATCHED + | MERGE + | MICROSECOND + | MICROSECONDS + | MILLISECOND + | MILLISECONDS + | MINUTE + | MINUTES + | MONTH + | MONTHS + | MSCK + | NAMESPACE + | NAMESPACES + | NANOSECOND + | NANOSECONDS + | NO + | NOT + | NULL + | NULLS + | OF + | OFFSET + | ONLY + | OPTION + | OPTIONS + | OR + | ORDER + | OUT + | OUTER + | OUTPUTFORMAT + | OVER + | OVERLAPS + | OVERLAY + | OVERWRITE + | PARTITION + | PARTITIONED + | PARTITIONS + | PERCENTILE_CONT + | PERCENTILE_DISC + | PERCENTLIT + | PIVOT + | PLACING + | POSITION + | PRECEDING + | PRIMARY + | PRINCIPALS + | PROPERTIES + | PURGE + | QUARTER + | QUERY + | RANGE + | RECORDREADER + | RECORDWRITER + | RECOVER + | REDUCE + | REFERENCES + | REFRESH + | RENAME + | REPAIR + | REPEATABLE + | REPLACE + | RESET + | RESPECT + | RESTRICT + | REVOKE + | RLIKE + | ROLE + | ROLES + | ROLLBACK + | ROLLUP + | ROW + | ROWS + | SCHEMA + | SCHEMAS + | SECOND + | SECONDS + | SELECT + | SEPARATED + | SERDE + | SERDEPROPERTIES + | SESSION_USER + | SET + | SETS + | SHOW + | SKEWED + | SOME + | SORT + | SORTED + | SOURCE + | START + | STATISTICS + | STORED + | STRATIFY + | STRUCT + | SUBSTR + | SUBSTRING + | SYNC + | SYSTEM_TIME + | SYSTEM_VERSION + | TABLE + | TABLES + | TABLESAMPLE + | TARGET + | TBLPROPERTIES + | TEMPORARY + | TERMINATED + | THEN + | TIME + | TIMESTAMP + | TIMESTAMPADD + | TIMESTAMPDIFF + | TO + | TOUCH + | TRAILING + | TRANSACTION + | TRANSACTIONS + | TRANSFORM + | TRIM + | TRUE + | TRUNCATE + | TRY_CAST + | TYPE + | UNARCHIVE + | UNBOUNDED + | UNCACHE + | UNIQUE + | UNKNOWN + | UNLOCK + | UNPIVOT + | UNSET + | UPDATE + | USE + | USER + | VALUES + | VERSION + | VIEW + | VIEWS + | WEEK + | WEEKS + | WHEN + | WHERE + | WINDOW + | WITH + | WITHIN + | YEAR + | YEARS + | ZONE +//--DEFAULT-NON-RESERVED-END + ; diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/grammar/__init__.py b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseLexer.interp b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseLexer.interp new file mode 100644 index 000000000..91c10b857 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseLexer.interp @@ -0,0 +1,1059 @@ +token literal names: +null +';' +'(' +')' +',' +'.' +'[' +']' +'ADD' +'AFTER' +'ALL' +'ALTER' +'ALWAYS' +'ANALYZE' +'AND' +'ANTI' +'ANY' +'ANY_VALUE' +'ARCHIVE' +'ARRAY' +'AS' +'ASC' +'AT' +'AUTHORIZATION' +'BETWEEN' +'BOTH' +'BUCKET' +'BUCKETS' +'BY' +'CACHE' +'CASCADE' +'CASE' +'CAST' +'CATALOG' +'CATALOGS' +'CHANGE' +'CHECK' +'CLEAR' +'CLUSTER' +'CLUSTERED' +'CODEGEN' +'COLLATE' +'COLLECTION' +'COLUMN' +'COLUMNS' +'COMMENT' +'COMMIT' +'COMPACT' +'COMPACTIONS' +'COMPUTE' +'CONCATENATE' +'CONSTRAINT' +'COST' +'CREATE' +'CROSS' +'CUBE' +'CURRENT' +'CURRENT_DATE' +'CURRENT_TIME' +'CURRENT_TIMESTAMP' +'CURRENT_USER' +'DAY' +'DAYS' +'DAYOFYEAR' +'DATA' +'DATABASE' +'DATABASES' +'DATEADD' +'DATEDIFF' +'DBPROPERTIES' +'DEFAULT' +'DEFINED' +'DELETE' +'DELIMITED' +'DESC' +'DESCRIBE' +'DFS' +'DIRECTORIES' +'DIRECTORY' +'DISTINCT' +'DISTRIBUTE' +'DIV' +'DROP' +'ELSE' +'END' +'ESCAPE' +'ESCAPED' +'EXCEPT' +'EXCHANGE' +'EXCLUDE' +'EXISTS' +'EXPLAIN' +'EXPORT' +'EXTENDED' +'EXTERNAL' +'EXTRACT' +'FALSE' +'FETCH' +'FIELDS' +'FILTER' +'FILEFORMAT' +'FIRST' +'FOLLOWING' +'FOR' +'FOREIGN' +'FORMAT' +'FORMATTED' +'FROM' +'FULL' +'FUNCTION' +'FUNCTIONS' +'GENERATED' +'GLOBAL' +'GRANT' +'GROUP' +'GROUPING' +'HAVING' +'HOUR' +'HOURS' +'IF' +'IGNORE' +'IMPORT' +'IN' +'INCLUDE' +'INDEX' +'INDEXES' +'INNER' +'INPATH' +'INPUTFORMAT' +'INSERT' +'INTERSECT' +'INTERVAL' +'INTO' +'IS' +'ITEMS' +'JOIN' +'KEYS' +'LAST' +'LATERAL' +'LAZY' +'LEADING' +'LEFT' +'LIKE' +'ILIKE' +'LIMIT' +'LINES' +'LIST' +'LOAD' +'LOCAL' +'LOCATION' +'LOCK' +'LOCKS' +'LOGICAL' +'MACRO' +'MAP' +'MATCHED' +'MERGE' +'MICROSECOND' +'MICROSECONDS' +'MILLISECOND' +'MILLISECONDS' +'MINUTE' +'MINUTES' +'MONTH' +'MONTHS' +'MSCK' +'NAMESPACE' +'NAMESPACES' +'NANOSECOND' +'NANOSECONDS' +'NATURAL' +'NO' +null +'NULL' +'NULLS' +'OF' +'OFFSET' +'ON' +'ONLY' +'OPTION' +'OPTIONS' +'OR' +'ORDER' +'OUT' +'OUTER' +'OUTPUTFORMAT' +'OVER' +'OVERLAPS' +'OVERLAY' +'OVERWRITE' +'PARTITION' +'PARTITIONED' +'PARTITIONS' +'PERCENTILE_CONT' +'PERCENTILE_DISC' +'PERCENT' +'PIVOT' +'PLACING' +'POSITION' +'PRECEDING' +'PRIMARY' +'PRINCIPALS' +'PROPERTIES' +'PURGE' +'QUARTER' +'QUERY' +'RANGE' +'RECORDREADER' +'RECORDWRITER' +'RECOVER' +'REDUCE' +'REFERENCES' +'REFRESH' +'RENAME' +'REPAIR' +'REPEATABLE' +'REPLACE' +'RESET' +'RESPECT' +'RESTRICT' +'REVOKE' +'RIGHT' +null +'ROLE' +'ROLES' +'ROLLBACK' +'ROLLUP' +'ROW' +'ROWS' +'SECOND' +'SECONDS' +'SCHEMA' +'SCHEMAS' +'SELECT' +'SEMI' +'SEPARATED' +'SERDE' +'SERDEPROPERTIES' +'SESSION_USER' +'SET' +'MINUS' +'SETS' +'SHOW' +'SKEWED' +'SOME' +'SORT' +'SORTED' +'SOURCE' +'START' +'STATISTICS' +'STORED' +'STRATIFY' +'STRUCT' +'SUBSTR' +'SUBSTRING' +'SYNC' +'SYSTEM_TIME' +'SYSTEM_VERSION' +'TABLE' +'TABLES' +'TABLESAMPLE' +'TARGET' +'TBLPROPERTIES' +null +'TERMINATED' +'THEN' +'TIME' +'TIMESTAMP' +'TIMESTAMPADD' +'TIMESTAMPDIFF' +'TO' +'TOUCH' +'TRAILING' +'TRANSACTION' +'TRANSACTIONS' +'TRANSFORM' +'TRIM' +'TRUE' +'TRUNCATE' +'TRY_CAST' +'TYPE' +'UNARCHIVE' +'UNBOUNDED' +'UNCACHE' +'UNION' +'UNIQUE' +'UNKNOWN' +'UNLOCK' +'UNPIVOT' +'UNSET' +'UPDATE' +'USE' +'USER' +'USING' +'VALUES' +'VERSION' +'VIEW' +'VIEWS' +'WEEK' +'WEEKS' +'WHEN' +'WHERE' +'WINDOW' +'WITH' +'WITHIN' +'YEAR' +'YEARS' +'ZONE' +null +'<=>' +'<>' +'!=' +'<' +null +'>' +null +'+' +'-' +'*' +'/' +'%' +'~' +'&' +'|' +'||' +'^' +':' +'->' +'/*+' +'*/' +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null + +token symbolic names: +null +SEMICOLON +LEFT_PAREN +RIGHT_PAREN +COMMA +DOT +LEFT_BRACKET +RIGHT_BRACKET +ADD +AFTER +ALL +ALTER +ALWAYS +ANALYZE +AND +ANTI +ANY +ANY_VALUE +ARCHIVE +ARRAY +AS +ASC +AT +AUTHORIZATION +BETWEEN +BOTH +BUCKET +BUCKETS +BY +CACHE +CASCADE +CASE +CAST +CATALOG +CATALOGS +CHANGE +CHECK +CLEAR +CLUSTER +CLUSTERED +CODEGEN +COLLATE +COLLECTION +COLUMN +COLUMNS +COMMENT +COMMIT +COMPACT +COMPACTIONS +COMPUTE +CONCATENATE +CONSTRAINT +COST +CREATE +CROSS +CUBE +CURRENT +CURRENT_DATE +CURRENT_TIME +CURRENT_TIMESTAMP +CURRENT_USER +DAY +DAYS +DAYOFYEAR +DATA +DATABASE +DATABASES +DATEADD +DATEDIFF +DBPROPERTIES +DEFAULT +DEFINED +DELETE +DELIMITED +DESC +DESCRIBE +DFS +DIRECTORIES +DIRECTORY +DISTINCT +DISTRIBUTE +DIV +DROP +ELSE +END +ESCAPE +ESCAPED +EXCEPT +EXCHANGE +EXCLUDE +EXISTS +EXPLAIN +EXPORT +EXTENDED +EXTERNAL +EXTRACT +FALSE +FETCH +FIELDS +FILTER +FILEFORMAT +FIRST +FOLLOWING +FOR +FOREIGN +FORMAT +FORMATTED +FROM +FULL +FUNCTION +FUNCTIONS +GENERATED +GLOBAL +GRANT +GROUP +GROUPING +HAVING +HOUR +HOURS +IF +IGNORE +IMPORT +IN +INCLUDE +INDEX +INDEXES +INNER +INPATH +INPUTFORMAT +INSERT +INTERSECT +INTERVAL +INTO +IS +ITEMS +JOIN +KEYS +LAST +LATERAL +LAZY +LEADING +LEFT +LIKE +ILIKE +LIMIT +LINES +LIST +LOAD +LOCAL +LOCATION +LOCK +LOCKS +LOGICAL +MACRO +MAP +MATCHED +MERGE +MICROSECOND +MICROSECONDS +MILLISECOND +MILLISECONDS +MINUTE +MINUTES +MONTH +MONTHS +MSCK +NAMESPACE +NAMESPACES +NANOSECOND +NANOSECONDS +NATURAL +NO +NOT +NULL +NULLS +OF +OFFSET +ON +ONLY +OPTION +OPTIONS +OR +ORDER +OUT +OUTER +OUTPUTFORMAT +OVER +OVERLAPS +OVERLAY +OVERWRITE +PARTITION +PARTITIONED +PARTITIONS +PERCENTILE_CONT +PERCENTILE_DISC +PERCENTLIT +PIVOT +PLACING +POSITION +PRECEDING +PRIMARY +PRINCIPALS +PROPERTIES +PURGE +QUARTER +QUERY +RANGE +RECORDREADER +RECORDWRITER +RECOVER +REDUCE +REFERENCES +REFRESH +RENAME +REPAIR +REPEATABLE +REPLACE +RESET +RESPECT +RESTRICT +REVOKE +RIGHT +RLIKE +ROLE +ROLES +ROLLBACK +ROLLUP +ROW +ROWS +SECOND +SECONDS +SCHEMA +SCHEMAS +SELECT +SEMI +SEPARATED +SERDE +SERDEPROPERTIES +SESSION_USER +SET +SETMINUS +SETS +SHOW +SKEWED +SOME +SORT +SORTED +SOURCE +START +STATISTICS +STORED +STRATIFY +STRUCT +SUBSTR +SUBSTRING +SYNC +SYSTEM_TIME +SYSTEM_VERSION +TABLE +TABLES +TABLESAMPLE +TARGET +TBLPROPERTIES +TEMPORARY +TERMINATED +THEN +TIME +TIMESTAMP +TIMESTAMPADD +TIMESTAMPDIFF +TO +TOUCH +TRAILING +TRANSACTION +TRANSACTIONS +TRANSFORM +TRIM +TRUE +TRUNCATE +TRY_CAST +TYPE +UNARCHIVE +UNBOUNDED +UNCACHE +UNION +UNIQUE +UNKNOWN +UNLOCK +UNPIVOT +UNSET +UPDATE +USE +USER +USING +VALUES +VERSION +VIEW +VIEWS +WEEK +WEEKS +WHEN +WHERE +WINDOW +WITH +WITHIN +YEAR +YEARS +ZONE +EQ +NSEQ +NEQ +NEQJ +LT +LTE +GT +GTE +PLUS +MINUS +ASTERISK +SLASH +PERCENT +TILDE +AMPERSAND +PIPE +CONCAT_PIPE +HAT +COLON +ARROW +HENT_START +HENT_END +STRING +DOUBLEQUOTED_STRING +BIGINT_LITERAL +SMALLINT_LITERAL +TINYINT_LITERAL +INTEGER_VALUE +EXPONENT_VALUE +DECIMAL_VALUE +FLOAT_LITERAL +DOUBLE_LITERAL +BIGDECIMAL_LITERAL +IDENTIFIER +BACKQUOTED_IDENTIFIER +SIMPLE_COMMENT +BRACKETED_COMMENT +WS +UNRECOGNIZED + +rule names: +SEMICOLON +LEFT_PAREN +RIGHT_PAREN +COMMA +DOT +LEFT_BRACKET +RIGHT_BRACKET +ADD +AFTER +ALL +ALTER +ALWAYS +ANALYZE +AND +ANTI +ANY +ANY_VALUE +ARCHIVE +ARRAY +AS +ASC +AT +AUTHORIZATION +BETWEEN +BOTH +BUCKET +BUCKETS +BY +CACHE +CASCADE +CASE +CAST +CATALOG +CATALOGS +CHANGE +CHECK +CLEAR +CLUSTER +CLUSTERED +CODEGEN +COLLATE +COLLECTION +COLUMN +COLUMNS +COMMENT +COMMIT +COMPACT +COMPACTIONS +COMPUTE +CONCATENATE +CONSTRAINT +COST +CREATE +CROSS +CUBE +CURRENT +CURRENT_DATE +CURRENT_TIME +CURRENT_TIMESTAMP +CURRENT_USER +DAY +DAYS +DAYOFYEAR +DATA +DATABASE +DATABASES +DATEADD +DATEDIFF +DBPROPERTIES +DEFAULT +DEFINED +DELETE +DELIMITED +DESC +DESCRIBE +DFS +DIRECTORIES +DIRECTORY +DISTINCT +DISTRIBUTE +DIV +DROP +ELSE +END +ESCAPE +ESCAPED +EXCEPT +EXCHANGE +EXCLUDE +EXISTS +EXPLAIN +EXPORT +EXTENDED +EXTERNAL +EXTRACT +FALSE +FETCH +FIELDS +FILTER +FILEFORMAT +FIRST +FOLLOWING +FOR +FOREIGN +FORMAT +FORMATTED +FROM +FULL +FUNCTION +FUNCTIONS +GENERATED +GLOBAL +GRANT +GROUP +GROUPING +HAVING +HOUR +HOURS +IF +IGNORE +IMPORT +IN +INCLUDE +INDEX +INDEXES +INNER +INPATH +INPUTFORMAT +INSERT +INTERSECT +INTERVAL +INTO +IS +ITEMS +JOIN +KEYS +LAST +LATERAL +LAZY +LEADING +LEFT +LIKE +ILIKE +LIMIT +LINES +LIST +LOAD +LOCAL +LOCATION +LOCK +LOCKS +LOGICAL +MACRO +MAP +MATCHED +MERGE +MICROSECOND +MICROSECONDS +MILLISECOND +MILLISECONDS +MINUTE +MINUTES +MONTH +MONTHS +MSCK +NAMESPACE +NAMESPACES +NANOSECOND +NANOSECONDS +NATURAL +NO +NOT +NULL +NULLS +OF +OFFSET +ON +ONLY +OPTION +OPTIONS +OR +ORDER +OUT +OUTER +OUTPUTFORMAT +OVER +OVERLAPS +OVERLAY +OVERWRITE +PARTITION +PARTITIONED +PARTITIONS +PERCENTILE_CONT +PERCENTILE_DISC +PERCENTLIT +PIVOT +PLACING +POSITION +PRECEDING +PRIMARY +PRINCIPALS +PROPERTIES +PURGE +QUARTER +QUERY +RANGE +RECORDREADER +RECORDWRITER +RECOVER +REDUCE +REFERENCES +REFRESH +RENAME +REPAIR +REPEATABLE +REPLACE +RESET +RESPECT +RESTRICT +REVOKE +RIGHT +RLIKE +ROLE +ROLES +ROLLBACK +ROLLUP +ROW +ROWS +SECOND +SECONDS +SCHEMA +SCHEMAS +SELECT +SEMI +SEPARATED +SERDE +SERDEPROPERTIES +SESSION_USER +SET +SETMINUS +SETS +SHOW +SKEWED +SOME +SORT +SORTED +SOURCE +START +STATISTICS +STORED +STRATIFY +STRUCT +SUBSTR +SUBSTRING +SYNC +SYSTEM_TIME +SYSTEM_VERSION +TABLE +TABLES +TABLESAMPLE +TARGET +TBLPROPERTIES +TEMPORARY +TERMINATED +THEN +TIME +TIMESTAMP +TIMESTAMPADD +TIMESTAMPDIFF +TO +TOUCH +TRAILING +TRANSACTION +TRANSACTIONS +TRANSFORM +TRIM +TRUE +TRUNCATE +TRY_CAST +TYPE +UNARCHIVE +UNBOUNDED +UNCACHE +UNION +UNIQUE +UNKNOWN +UNLOCK +UNPIVOT +UNSET +UPDATE +USE +USER +USING +VALUES +VERSION +VIEW +VIEWS +WEEK +WEEKS +WHEN +WHERE +WINDOW +WITH +WITHIN +YEAR +YEARS +ZONE +EQ +NSEQ +NEQ +NEQJ +LT +LTE +GT +GTE +PLUS +MINUS +ASTERISK +SLASH +PERCENT +TILDE +AMPERSAND +PIPE +CONCAT_PIPE +HAT +COLON +ARROW +HENT_START +HENT_END +STRING +DOUBLEQUOTED_STRING +BIGINT_LITERAL +SMALLINT_LITERAL +TINYINT_LITERAL +INTEGER_VALUE +EXPONENT_VALUE +DECIMAL_VALUE +FLOAT_LITERAL +DOUBLE_LITERAL +BIGDECIMAL_LITERAL +IDENTIFIER +BACKQUOTED_IDENTIFIER +DECIMAL_DIGITS +EXPONENT +DIGIT +LETTER +SIMPLE_COMMENT +BRACKETED_COMMENT +WS +UNRECOGNIZED + +channel names: +DEFAULT_TOKEN_CHANNEL +HIDDEN + +mode names: +DEFAULT_MODE + +atn: +[4, 0, 346, 3299, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 2, 83, 7, 83, 2, 84, 7, 84, 2, 85, 7, 85, 2, 86, 7, 86, 2, 87, 7, 87, 2, 88, 7, 88, 2, 89, 7, 89, 2, 90, 7, 90, 2, 91, 7, 91, 2, 92, 7, 92, 2, 93, 7, 93, 2, 94, 7, 94, 2, 95, 7, 95, 2, 96, 7, 96, 2, 97, 7, 97, 2, 98, 7, 98, 2, 99, 7, 99, 2, 100, 7, 100, 2, 101, 7, 101, 2, 102, 7, 102, 2, 103, 7, 103, 2, 104, 7, 104, 2, 105, 7, 105, 2, 106, 7, 106, 2, 107, 7, 107, 2, 108, 7, 108, 2, 109, 7, 109, 2, 110, 7, 110, 2, 111, 7, 111, 2, 112, 7, 112, 2, 113, 7, 113, 2, 114, 7, 114, 2, 115, 7, 115, 2, 116, 7, 116, 2, 117, 7, 117, 2, 118, 7, 118, 2, 119, 7, 119, 2, 120, 7, 120, 2, 121, 7, 121, 2, 122, 7, 122, 2, 123, 7, 123, 2, 124, 7, 124, 2, 125, 7, 125, 2, 126, 7, 126, 2, 127, 7, 127, 2, 128, 7, 128, 2, 129, 7, 129, 2, 130, 7, 130, 2, 131, 7, 131, 2, 132, 7, 132, 2, 133, 7, 133, 2, 134, 7, 134, 2, 135, 7, 135, 2, 136, 7, 136, 2, 137, 7, 137, 2, 138, 7, 138, 2, 139, 7, 139, 2, 140, 7, 140, 2, 141, 7, 141, 2, 142, 7, 142, 2, 143, 7, 143, 2, 144, 7, 144, 2, 145, 7, 145, 2, 146, 7, 146, 2, 147, 7, 147, 2, 148, 7, 148, 2, 149, 7, 149, 2, 150, 7, 150, 2, 151, 7, 151, 2, 152, 7, 152, 2, 153, 7, 153, 2, 154, 7, 154, 2, 155, 7, 155, 2, 156, 7, 156, 2, 157, 7, 157, 2, 158, 7, 158, 2, 159, 7, 159, 2, 160, 7, 160, 2, 161, 7, 161, 2, 162, 7, 162, 2, 163, 7, 163, 2, 164, 7, 164, 2, 165, 7, 165, 2, 166, 7, 166, 2, 167, 7, 167, 2, 168, 7, 168, 2, 169, 7, 169, 2, 170, 7, 170, 2, 171, 7, 171, 2, 172, 7, 172, 2, 173, 7, 173, 2, 174, 7, 174, 2, 175, 7, 175, 2, 176, 7, 176, 2, 177, 7, 177, 2, 178, 7, 178, 2, 179, 7, 179, 2, 180, 7, 180, 2, 181, 7, 181, 2, 182, 7, 182, 2, 183, 7, 183, 2, 184, 7, 184, 2, 185, 7, 185, 2, 186, 7, 186, 2, 187, 7, 187, 2, 188, 7, 188, 2, 189, 7, 189, 2, 190, 7, 190, 2, 191, 7, 191, 2, 192, 7, 192, 2, 193, 7, 193, 2, 194, 7, 194, 2, 195, 7, 195, 2, 196, 7, 196, 2, 197, 7, 197, 2, 198, 7, 198, 2, 199, 7, 199, 2, 200, 7, 200, 2, 201, 7, 201, 2, 202, 7, 202, 2, 203, 7, 203, 2, 204, 7, 204, 2, 205, 7, 205, 2, 206, 7, 206, 2, 207, 7, 207, 2, 208, 7, 208, 2, 209, 7, 209, 2, 210, 7, 210, 2, 211, 7, 211, 2, 212, 7, 212, 2, 213, 7, 213, 2, 214, 7, 214, 2, 215, 7, 215, 2, 216, 7, 216, 2, 217, 7, 217, 2, 218, 7, 218, 2, 219, 7, 219, 2, 220, 7, 220, 2, 221, 7, 221, 2, 222, 7, 222, 2, 223, 7, 223, 2, 224, 7, 224, 2, 225, 7, 225, 2, 226, 7, 226, 2, 227, 7, 227, 2, 228, 7, 228, 2, 229, 7, 229, 2, 230, 7, 230, 2, 231, 7, 231, 2, 232, 7, 232, 2, 233, 7, 233, 2, 234, 7, 234, 2, 235, 7, 235, 2, 236, 7, 236, 2, 237, 7, 237, 2, 238, 7, 238, 2, 239, 7, 239, 2, 240, 7, 240, 2, 241, 7, 241, 2, 242, 7, 242, 2, 243, 7, 243, 2, 244, 7, 244, 2, 245, 7, 245, 2, 246, 7, 246, 2, 247, 7, 247, 2, 248, 7, 248, 2, 249, 7, 249, 2, 250, 7, 250, 2, 251, 7, 251, 2, 252, 7, 252, 2, 253, 7, 253, 2, 254, 7, 254, 2, 255, 7, 255, 2, 256, 7, 256, 2, 257, 7, 257, 2, 258, 7, 258, 2, 259, 7, 259, 2, 260, 7, 260, 2, 261, 7, 261, 2, 262, 7, 262, 2, 263, 7, 263, 2, 264, 7, 264, 2, 265, 7, 265, 2, 266, 7, 266, 2, 267, 7, 267, 2, 268, 7, 268, 2, 269, 7, 269, 2, 270, 7, 270, 2, 271, 7, 271, 2, 272, 7, 272, 2, 273, 7, 273, 2, 274, 7, 274, 2, 275, 7, 275, 2, 276, 7, 276, 2, 277, 7, 277, 2, 278, 7, 278, 2, 279, 7, 279, 2, 280, 7, 280, 2, 281, 7, 281, 2, 282, 7, 282, 2, 283, 7, 283, 2, 284, 7, 284, 2, 285, 7, 285, 2, 286, 7, 286, 2, 287, 7, 287, 2, 288, 7, 288, 2, 289, 7, 289, 2, 290, 7, 290, 2, 291, 7, 291, 2, 292, 7, 292, 2, 293, 7, 293, 2, 294, 7, 294, 2, 295, 7, 295, 2, 296, 7, 296, 2, 297, 7, 297, 2, 298, 7, 298, 2, 299, 7, 299, 2, 300, 7, 300, 2, 301, 7, 301, 2, 302, 7, 302, 2, 303, 7, 303, 2, 304, 7, 304, 2, 305, 7, 305, 2, 306, 7, 306, 2, 307, 7, 307, 2, 308, 7, 308, 2, 309, 7, 309, 2, 310, 7, 310, 2, 311, 7, 311, 2, 312, 7, 312, 2, 313, 7, 313, 2, 314, 7, 314, 2, 315, 7, 315, 2, 316, 7, 316, 2, 317, 7, 317, 2, 318, 7, 318, 2, 319, 7, 319, 2, 320, 7, 320, 2, 321, 7, 321, 2, 322, 7, 322, 2, 323, 7, 323, 2, 324, 7, 324, 2, 325, 7, 325, 2, 326, 7, 326, 2, 327, 7, 327, 2, 328, 7, 328, 2, 329, 7, 329, 2, 330, 7, 330, 2, 331, 7, 331, 2, 332, 7, 332, 2, 333, 7, 333, 2, 334, 7, 334, 2, 335, 7, 335, 2, 336, 7, 336, 2, 337, 7, 337, 2, 338, 7, 338, 2, 339, 7, 339, 2, 340, 7, 340, 2, 341, 7, 341, 2, 342, 7, 342, 2, 343, 7, 343, 2, 344, 7, 344, 2, 345, 7, 345, 2, 346, 7, 346, 2, 347, 7, 347, 2, 348, 7, 348, 2, 349, 7, 349, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 75, 1, 75, 1, 75, 1, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 1, 83, 1, 83, 1, 83, 1, 83, 1, 84, 1, 84, 1, 84, 1, 84, 1, 84, 1, 84, 1, 84, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 91, 1, 91, 1, 91, 1, 91, 1, 91, 1, 91, 1, 91, 1, 92, 1, 92, 1, 92, 1, 92, 1, 92, 1, 92, 1, 92, 1, 92, 1, 92, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 97, 1, 97, 1, 97, 1, 97, 1, 97, 1, 97, 1, 97, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 100, 1, 100, 1, 100, 1, 100, 1, 100, 1, 100, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 102, 1, 102, 1, 102, 1, 102, 1, 103, 1, 103, 1, 103, 1, 103, 1, 103, 1, 103, 1, 103, 1, 103, 1, 104, 1, 104, 1, 104, 1, 104, 1, 104, 1, 104, 1, 104, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 106, 1, 106, 1, 106, 1, 106, 1, 106, 1, 107, 1, 107, 1, 107, 1, 107, 1, 107, 1, 108, 1, 108, 1, 108, 1, 108, 1, 108, 1, 108, 1, 108, 1, 108, 1, 108, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 115, 1, 115, 1, 115, 1, 115, 1, 115, 1, 115, 1, 115, 1, 116, 1, 116, 1, 116, 1, 116, 1, 116, 1, 117, 1, 117, 1, 117, 1, 117, 1, 117, 1, 117, 1, 118, 1, 118, 1, 118, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 121, 1, 121, 1, 121, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 132, 1, 132, 1, 132, 1, 133, 1, 133, 1, 133, 1, 133, 1, 133, 1, 133, 1, 134, 1, 134, 1, 134, 1, 134, 1, 134, 1, 135, 1, 135, 1, 135, 1, 135, 1, 135, 1, 136, 1, 136, 1, 136, 1, 136, 1, 136, 1, 137, 1, 137, 1, 137, 1, 137, 1, 137, 1, 137, 1, 137, 1, 137, 1, 138, 1, 138, 1, 138, 1, 138, 1, 138, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 140, 1, 140, 1, 140, 1, 140, 1, 140, 1, 141, 1, 141, 1, 141, 1, 141, 1, 141, 1, 142, 1, 142, 1, 142, 1, 142, 1, 142, 1, 142, 1, 143, 1, 143, 1, 143, 1, 143, 1, 143, 1, 143, 1, 144, 1, 144, 1, 144, 1, 144, 1, 144, 1, 144, 1, 145, 1, 145, 1, 145, 1, 145, 1, 145, 1, 146, 1, 146, 1, 146, 1, 146, 1, 146, 1, 147, 1, 147, 1, 147, 1, 147, 1, 147, 1, 147, 1, 148, 1, 148, 1, 148, 1, 148, 1, 148, 1, 148, 1, 148, 1, 148, 1, 148, 1, 149, 1, 149, 1, 149, 1, 149, 1, 149, 1, 150, 1, 150, 1, 150, 1, 150, 1, 150, 1, 150, 1, 151, 1, 151, 1, 151, 1, 151, 1, 151, 1, 151, 1, 151, 1, 151, 1, 152, 1, 152, 1, 152, 1, 152, 1, 152, 1, 152, 1, 153, 1, 153, 1, 153, 1, 153, 1, 154, 1, 154, 1, 154, 1, 154, 1, 154, 1, 154, 1, 154, 1, 154, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 1, 158, 1, 158, 1, 158, 1, 158, 1, 158, 1, 158, 1, 158, 1, 158, 1, 158, 1, 158, 1, 158, 1, 158, 1, 159, 1, 159, 1, 159, 1, 159, 1, 159, 1, 159, 1, 159, 1, 159, 1, 159, 1, 159, 1, 159, 1, 159, 1, 159, 1, 160, 1, 160, 1, 160, 1, 160, 1, 160, 1, 160, 1, 160, 1, 161, 1, 161, 1, 161, 1, 161, 1, 161, 1, 161, 1, 161, 1, 161, 1, 162, 1, 162, 1, 162, 1, 162, 1, 162, 1, 162, 1, 163, 1, 163, 1, 163, 1, 163, 1, 163, 1, 163, 1, 163, 1, 164, 1, 164, 1, 164, 1, 164, 1, 164, 1, 165, 1, 165, 1, 165, 1, 165, 1, 165, 1, 165, 1, 165, 1, 165, 1, 165, 1, 165, 1, 166, 1, 166, 1, 166, 1, 166, 1, 166, 1, 166, 1, 166, 1, 166, 1, 166, 1, 166, 1, 166, 1, 167, 1, 167, 1, 167, 1, 167, 1, 167, 1, 167, 1, 167, 1, 167, 1, 167, 1, 167, 1, 167, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 169, 1, 169, 1, 169, 1, 169, 1, 169, 1, 169, 1, 169, 1, 169, 1, 170, 1, 170, 1, 170, 1, 171, 1, 171, 1, 171, 1, 171, 3, 171, 1936, 8, 171, 1, 172, 1, 172, 1, 172, 1, 172, 1, 172, 1, 173, 1, 173, 1, 173, 1, 173, 1, 173, 1, 173, 1, 174, 1, 174, 1, 174, 1, 175, 1, 175, 1, 175, 1, 175, 1, 175, 1, 175, 1, 175, 1, 176, 1, 176, 1, 176, 1, 177, 1, 177, 1, 177, 1, 177, 1, 177, 1, 178, 1, 178, 1, 178, 1, 178, 1, 178, 1, 178, 1, 178, 1, 179, 1, 179, 1, 179, 1, 179, 1, 179, 1, 179, 1, 179, 1, 179, 1, 180, 1, 180, 1, 180, 1, 181, 1, 181, 1, 181, 1, 181, 1, 181, 1, 181, 1, 182, 1, 182, 1, 182, 1, 182, 1, 183, 1, 183, 1, 183, 1, 183, 1, 183, 1, 183, 1, 184, 1, 184, 1, 184, 1, 184, 1, 184, 1, 184, 1, 184, 1, 184, 1, 184, 1, 184, 1, 184, 1, 184, 1, 184, 1, 185, 1, 185, 1, 185, 1, 185, 1, 185, 1, 186, 1, 186, 1, 186, 1, 186, 1, 186, 1, 186, 1, 186, 1, 186, 1, 186, 1, 187, 1, 187, 1, 187, 1, 187, 1, 187, 1, 187, 1, 187, 1, 187, 1, 188, 1, 188, 1, 188, 1, 188, 1, 188, 1, 188, 1, 188, 1, 188, 1, 188, 1, 188, 1, 189, 1, 189, 1, 189, 1, 189, 1, 189, 1, 189, 1, 189, 1, 189, 1, 189, 1, 189, 1, 190, 1, 190, 1, 190, 1, 190, 1, 190, 1, 190, 1, 190, 1, 190, 1, 190, 1, 190, 1, 190, 1, 190, 1, 191, 1, 191, 1, 191, 1, 191, 1, 191, 1, 191, 1, 191, 1, 191, 1, 191, 1, 191, 1, 191, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 192, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 193, 1, 194, 1, 194, 1, 194, 1, 194, 1, 194, 1, 194, 1, 194, 1, 194, 1, 195, 1, 195, 1, 195, 1, 195, 1, 195, 1, 195, 1, 196, 1, 196, 1, 196, 1, 196, 1, 196, 1, 196, 1, 196, 1, 196, 1, 197, 1, 197, 1, 197, 1, 197, 1, 197, 1, 197, 1, 197, 1, 197, 1, 197, 1, 198, 1, 198, 1, 198, 1, 198, 1, 198, 1, 198, 1, 198, 1, 198, 1, 198, 1, 198, 1, 199, 1, 199, 1, 199, 1, 199, 1, 199, 1, 199, 1, 199, 1, 199, 1, 200, 1, 200, 1, 200, 1, 200, 1, 200, 1, 200, 1, 200, 1, 200, 1, 200, 1, 200, 1, 200, 1, 201, 1, 201, 1, 201, 1, 201, 1, 201, 1, 201, 1, 201, 1, 201, 1, 201, 1, 201, 1, 201, 1, 202, 1, 202, 1, 202, 1, 202, 1, 202, 1, 202, 1, 203, 1, 203, 1, 203, 1, 203, 1, 203, 1, 203, 1, 203, 1, 203, 1, 204, 1, 204, 1, 204, 1, 204, 1, 204, 1, 204, 1, 205, 1, 205, 1, 205, 1, 205, 1, 205, 1, 205, 1, 206, 1, 206, 1, 206, 1, 206, 1, 206, 1, 206, 1, 206, 1, 206, 1, 206, 1, 206, 1, 206, 1, 206, 1, 206, 1, 207, 1, 207, 1, 207, 1, 207, 1, 207, 1, 207, 1, 207, 1, 207, 1, 207, 1, 207, 1, 207, 1, 207, 1, 207, 1, 208, 1, 208, 1, 208, 1, 208, 1, 208, 1, 208, 1, 208, 1, 208, 1, 209, 1, 209, 1, 209, 1, 209, 1, 209, 1, 209, 1, 209, 1, 210, 1, 210, 1, 210, 1, 210, 1, 210, 1, 210, 1, 210, 1, 210, 1, 210, 1, 210, 1, 210, 1, 211, 1, 211, 1, 211, 1, 211, 1, 211, 1, 211, 1, 211, 1, 211, 1, 212, 1, 212, 1, 212, 1, 212, 1, 212, 1, 212, 1, 212, 1, 213, 1, 213, 1, 213, 1, 213, 1, 213, 1, 213, 1, 213, 1, 214, 1, 214, 1, 214, 1, 214, 1, 214, 1, 214, 1, 214, 1, 214, 1, 214, 1, 214, 1, 214, 1, 215, 1, 215, 1, 215, 1, 215, 1, 215, 1, 215, 1, 215, 1, 215, 1, 216, 1, 216, 1, 216, 1, 216, 1, 216, 1, 216, 1, 217, 1, 217, 1, 217, 1, 217, 1, 217, 1, 217, 1, 217, 1, 217, 1, 218, 1, 218, 1, 218, 1, 218, 1, 218, 1, 218, 1, 218, 1, 218, 1, 218, 1, 219, 1, 219, 1, 219, 1, 219, 1, 219, 1, 219, 1, 219, 1, 220, 1, 220, 1, 220, 1, 220, 1, 220, 1, 220, 1, 221, 1, 221, 1, 221, 1, 221, 1, 221, 1, 221, 1, 221, 1, 221, 1, 221, 1, 221, 1, 221, 3, 221, 2348, 8, 221, 1, 222, 1, 222, 1, 222, 1, 222, 1, 222, 1, 223, 1, 223, 1, 223, 1, 223, 1, 223, 1, 223, 1, 224, 1, 224, 1, 224, 1, 224, 1, 224, 1, 224, 1, 224, 1, 224, 1, 224, 1, 225, 1, 225, 1, 225, 1, 225, 1, 225, 1, 225, 1, 225, 1, 226, 1, 226, 1, 226, 1, 226, 1, 227, 1, 227, 1, 227, 1, 227, 1, 227, 1, 228, 1, 228, 1, 228, 1, 228, 1, 228, 1, 228, 1, 228, 1, 229, 1, 229, 1, 229, 1, 229, 1, 229, 1, 229, 1, 229, 1, 229, 1, 230, 1, 230, 1, 230, 1, 230, 1, 230, 1, 230, 1, 230, 1, 231, 1, 231, 1, 231, 1, 231, 1, 231, 1, 231, 1, 231, 1, 231, 1, 232, 1, 232, 1, 232, 1, 232, 1, 232, 1, 232, 1, 232, 1, 233, 1, 233, 1, 233, 1, 233, 1, 233, 1, 234, 1, 234, 1, 234, 1, 234, 1, 234, 1, 234, 1, 234, 1, 234, 1, 234, 1, 234, 1, 235, 1, 235, 1, 235, 1, 235, 1, 235, 1, 235, 1, 236, 1, 236, 1, 236, 1, 236, 1, 236, 1, 236, 1, 236, 1, 236, 1, 236, 1, 236, 1, 236, 1, 236, 1, 236, 1, 236, 1, 236, 1, 236, 1, 237, 1, 237, 1, 237, 1, 237, 1, 237, 1, 237, 1, 237, 1, 237, 1, 237, 1, 237, 1, 237, 1, 237, 1, 237, 1, 238, 1, 238, 1, 238, 1, 238, 1, 239, 1, 239, 1, 239, 1, 239, 1, 239, 1, 239, 1, 240, 1, 240, 1, 240, 1, 240, 1, 240, 1, 241, 1, 241, 1, 241, 1, 241, 1, 241, 1, 242, 1, 242, 1, 242, 1, 242, 1, 242, 1, 242, 1, 242, 1, 243, 1, 243, 1, 243, 1, 243, 1, 243, 1, 244, 1, 244, 1, 244, 1, 244, 1, 244, 1, 245, 1, 245, 1, 245, 1, 245, 1, 245, 1, 245, 1, 245, 1, 246, 1, 246, 1, 246, 1, 246, 1, 246, 1, 246, 1, 246, 1, 247, 1, 247, 1, 247, 1, 247, 1, 247, 1, 247, 1, 248, 1, 248, 1, 248, 1, 248, 1, 248, 1, 248, 1, 248, 1, 248, 1, 248, 1, 248, 1, 248, 1, 249, 1, 249, 1, 249, 1, 249, 1, 249, 1, 249, 1, 249, 1, 250, 1, 250, 1, 250, 1, 250, 1, 250, 1, 250, 1, 250, 1, 250, 1, 250, 1, 251, 1, 251, 1, 251, 1, 251, 1, 251, 1, 251, 1, 251, 1, 252, 1, 252, 1, 252, 1, 252, 1, 252, 1, 252, 1, 252, 1, 253, 1, 253, 1, 253, 1, 253, 1, 253, 1, 253, 1, 253, 1, 253, 1, 253, 1, 253, 1, 254, 1, 254, 1, 254, 1, 254, 1, 254, 1, 255, 1, 255, 1, 255, 1, 255, 1, 255, 1, 255, 1, 255, 1, 255, 1, 255, 1, 255, 1, 255, 1, 255, 1, 256, 1, 256, 1, 256, 1, 256, 1, 256, 1, 256, 1, 256, 1, 256, 1, 256, 1, 256, 1, 256, 1, 256, 1, 256, 1, 256, 1, 256, 1, 257, 1, 257, 1, 257, 1, 257, 1, 257, 1, 257, 1, 258, 1, 258, 1, 258, 1, 258, 1, 258, 1, 258, 1, 258, 1, 259, 1, 259, 1, 259, 1, 259, 1, 259, 1, 259, 1, 259, 1, 259, 1, 259, 1, 259, 1, 259, 1, 259, 1, 260, 1, 260, 1, 260, 1, 260, 1, 260, 1, 260, 1, 260, 1, 261, 1, 261, 1, 261, 1, 261, 1, 261, 1, 261, 1, 261, 1, 261, 1, 261, 1, 261, 1, 261, 1, 261, 1, 261, 1, 261, 1, 262, 1, 262, 1, 262, 1, 262, 1, 262, 1, 262, 1, 262, 1, 262, 1, 262, 1, 262, 1, 262, 1, 262, 1, 262, 3, 262, 2672, 8, 262, 1, 263, 1, 263, 1, 263, 1, 263, 1, 263, 1, 263, 1, 263, 1, 263, 1, 263, 1, 263, 1, 263, 1, 264, 1, 264, 1, 264, 1, 264, 1, 264, 1, 265, 1, 265, 1, 265, 1, 265, 1, 265, 1, 266, 1, 266, 1, 266, 1, 266, 1, 266, 1, 266, 1, 266, 1, 266, 1, 266, 1, 266, 1, 267, 1, 267, 1, 267, 1, 267, 1, 267, 1, 267, 1, 267, 1, 267, 1, 267, 1, 267, 1, 267, 1, 267, 1, 267, 1, 268, 1, 268, 1, 268, 1, 268, 1, 268, 1, 268, 1, 268, 1, 268, 1, 268, 1, 268, 1, 268, 1, 268, 1, 268, 1, 268, 1, 269, 1, 269, 1, 269, 1, 270, 1, 270, 1, 270, 1, 270, 1, 270, 1, 270, 1, 271, 1, 271, 1, 271, 1, 271, 1, 271, 1, 271, 1, 271, 1, 271, 1, 271, 1, 272, 1, 272, 1, 272, 1, 272, 1, 272, 1, 272, 1, 272, 1, 272, 1, 272, 1, 272, 1, 272, 1, 272, 1, 273, 1, 273, 1, 273, 1, 273, 1, 273, 1, 273, 1, 273, 1, 273, 1, 273, 1, 273, 1, 273, 1, 273, 1, 273, 1, 274, 1, 274, 1, 274, 1, 274, 1, 274, 1, 274, 1, 274, 1, 274, 1, 274, 1, 274, 1, 275, 1, 275, 1, 275, 1, 275, 1, 275, 1, 276, 1, 276, 1, 276, 1, 276, 1, 276, 1, 277, 1, 277, 1, 277, 1, 277, 1, 277, 1, 277, 1, 277, 1, 277, 1, 277, 1, 278, 1, 278, 1, 278, 1, 278, 1, 278, 1, 278, 1, 278, 1, 278, 1, 278, 1, 279, 1, 279, 1, 279, 1, 279, 1, 279, 1, 280, 1, 280, 1, 280, 1, 280, 1, 280, 1, 280, 1, 280, 1, 280, 1, 280, 1, 280, 1, 281, 1, 281, 1, 281, 1, 281, 1, 281, 1, 281, 1, 281, 1, 281, 1, 281, 1, 281, 1, 282, 1, 282, 1, 282, 1, 282, 1, 282, 1, 282, 1, 282, 1, 282, 1, 283, 1, 283, 1, 283, 1, 283, 1, 283, 1, 283, 1, 284, 1, 284, 1, 284, 1, 284, 1, 284, 1, 284, 1, 284, 1, 285, 1, 285, 1, 285, 1, 285, 1, 285, 1, 285, 1, 285, 1, 285, 1, 286, 1, 286, 1, 286, 1, 286, 1, 286, 1, 286, 1, 286, 1, 287, 1, 287, 1, 287, 1, 287, 1, 287, 1, 287, 1, 287, 1, 287, 1, 288, 1, 288, 1, 288, 1, 288, 1, 288, 1, 288, 1, 289, 1, 289, 1, 289, 1, 289, 1, 289, 1, 289, 1, 289, 1, 290, 1, 290, 1, 290, 1, 290, 1, 291, 1, 291, 1, 291, 1, 291, 1, 291, 1, 292, 1, 292, 1, 292, 1, 292, 1, 292, 1, 292, 1, 293, 1, 293, 1, 293, 1, 293, 1, 293, 1, 293, 1, 293, 1, 294, 1, 294, 1, 294, 1, 294, 1, 294, 1, 294, 1, 294, 1, 294, 1, 295, 1, 295, 1, 295, 1, 295, 1, 295, 1, 296, 1, 296, 1, 296, 1, 296, 1, 296, 1, 296, 1, 297, 1, 297, 1, 297, 1, 297, 1, 297, 1, 298, 1, 298, 1, 298, 1, 298, 1, 298, 1, 298, 1, 299, 1, 299, 1, 299, 1, 299, 1, 299, 1, 300, 1, 300, 1, 300, 1, 300, 1, 300, 1, 300, 1, 301, 1, 301, 1, 301, 1, 301, 1, 301, 1, 301, 1, 301, 1, 302, 1, 302, 1, 302, 1, 302, 1, 302, 1, 303, 1, 303, 1, 303, 1, 303, 1, 303, 1, 303, 1, 303, 1, 304, 1, 304, 1, 304, 1, 304, 1, 304, 1, 305, 1, 305, 1, 305, 1, 305, 1, 305, 1, 305, 1, 306, 1, 306, 1, 306, 1, 306, 1, 306, 1, 307, 1, 307, 1, 307, 3, 307, 2996, 8, 307, 1, 308, 1, 308, 1, 308, 1, 308, 1, 309, 1, 309, 1, 309, 1, 310, 1, 310, 1, 310, 1, 311, 1, 311, 1, 312, 1, 312, 1, 312, 1, 312, 3, 312, 3014, 8, 312, 1, 313, 1, 313, 1, 314, 1, 314, 1, 314, 1, 314, 3, 314, 3022, 8, 314, 1, 315, 1, 315, 1, 316, 1, 316, 1, 317, 1, 317, 1, 318, 1, 318, 1, 319, 1, 319, 1, 320, 1, 320, 1, 321, 1, 321, 1, 322, 1, 322, 1, 323, 1, 323, 1, 323, 1, 324, 1, 324, 1, 325, 1, 325, 1, 326, 1, 326, 1, 326, 1, 327, 1, 327, 1, 327, 1, 327, 1, 328, 1, 328, 1, 328, 1, 329, 1, 329, 1, 329, 1, 329, 5, 329, 3061, 8, 329, 10, 329, 12, 329, 3064, 9, 329, 1, 329, 1, 329, 1, 329, 1, 329, 1, 329, 5, 329, 3071, 8, 329, 10, 329, 12, 329, 3074, 9, 329, 1, 329, 1, 329, 1, 329, 1, 329, 1, 329, 5, 329, 3081, 8, 329, 10, 329, 12, 329, 3084, 9, 329, 1, 329, 3, 329, 3087, 8, 329, 1, 330, 1, 330, 1, 330, 1, 330, 5, 330, 3093, 8, 330, 10, 330, 12, 330, 3096, 9, 330, 1, 330, 1, 330, 1, 331, 4, 331, 3101, 8, 331, 11, 331, 12, 331, 3102, 1, 331, 1, 331, 1, 332, 4, 332, 3108, 8, 332, 11, 332, 12, 332, 3109, 1, 332, 1, 332, 1, 333, 4, 333, 3115, 8, 333, 11, 333, 12, 333, 3116, 1, 333, 1, 333, 1, 334, 4, 334, 3122, 8, 334, 11, 334, 12, 334, 3123, 1, 335, 4, 335, 3127, 8, 335, 11, 335, 12, 335, 3128, 1, 335, 1, 335, 1, 335, 1, 335, 1, 335, 1, 335, 3, 335, 3137, 8, 335, 1, 336, 1, 336, 1, 336, 1, 337, 4, 337, 3143, 8, 337, 11, 337, 12, 337, 3144, 1, 337, 3, 337, 3148, 8, 337, 1, 337, 1, 337, 1, 337, 1, 337, 3, 337, 3154, 8, 337, 1, 337, 1, 337, 1, 337, 3, 337, 3159, 8, 337, 1, 338, 4, 338, 3162, 8, 338, 11, 338, 12, 338, 3163, 1, 338, 3, 338, 3167, 8, 338, 1, 338, 1, 338, 1, 338, 1, 338, 3, 338, 3173, 8, 338, 1, 338, 1, 338, 1, 338, 3, 338, 3178, 8, 338, 1, 339, 4, 339, 3181, 8, 339, 11, 339, 12, 339, 3182, 1, 339, 3, 339, 3186, 8, 339, 1, 339, 1, 339, 1, 339, 1, 339, 1, 339, 3, 339, 3193, 8, 339, 1, 339, 1, 339, 1, 339, 1, 339, 1, 339, 3, 339, 3200, 8, 339, 1, 340, 1, 340, 1, 340, 4, 340, 3205, 8, 340, 11, 340, 12, 340, 3206, 1, 341, 1, 341, 1, 341, 1, 341, 5, 341, 3213, 8, 341, 10, 341, 12, 341, 3216, 9, 341, 1, 341, 1, 341, 1, 342, 4, 342, 3221, 8, 342, 11, 342, 12, 342, 3222, 1, 342, 1, 342, 5, 342, 3227, 8, 342, 10, 342, 12, 342, 3230, 9, 342, 1, 342, 1, 342, 4, 342, 3234, 8, 342, 11, 342, 12, 342, 3235, 3, 342, 3238, 8, 342, 1, 343, 1, 343, 3, 343, 3242, 8, 343, 1, 343, 4, 343, 3245, 8, 343, 11, 343, 12, 343, 3246, 1, 344, 1, 344, 1, 345, 1, 345, 1, 346, 1, 346, 1, 346, 1, 346, 1, 346, 1, 346, 5, 346, 3259, 8, 346, 10, 346, 12, 346, 3262, 9, 346, 1, 346, 3, 346, 3265, 8, 346, 1, 346, 3, 346, 3268, 8, 346, 1, 346, 1, 346, 1, 347, 1, 347, 1, 347, 1, 347, 1, 347, 1, 347, 5, 347, 3278, 8, 347, 10, 347, 12, 347, 3281, 9, 347, 1, 347, 1, 347, 1, 347, 1, 347, 3, 347, 3287, 8, 347, 1, 347, 1, 347, 1, 348, 4, 348, 3292, 8, 348, 11, 348, 12, 348, 3293, 1, 348, 1, 348, 1, 349, 1, 349, 1, 3279, 0, 350, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55, 28, 57, 29, 59, 30, 61, 31, 63, 32, 65, 33, 67, 34, 69, 35, 71, 36, 73, 37, 75, 38, 77, 39, 79, 40, 81, 41, 83, 42, 85, 43, 87, 44, 89, 45, 91, 46, 93, 47, 95, 48, 97, 49, 99, 50, 101, 51, 103, 52, 105, 53, 107, 54, 109, 55, 111, 56, 113, 57, 115, 58, 117, 59, 119, 60, 121, 61, 123, 62, 125, 63, 127, 64, 129, 65, 131, 66, 133, 67, 135, 68, 137, 69, 139, 70, 141, 71, 143, 72, 145, 73, 147, 74, 149, 75, 151, 76, 153, 77, 155, 78, 157, 79, 159, 80, 161, 81, 163, 82, 165, 83, 167, 84, 169, 85, 171, 86, 173, 87, 175, 88, 177, 89, 179, 90, 181, 91, 183, 92, 185, 93, 187, 94, 189, 95, 191, 96, 193, 97, 195, 98, 197, 99, 199, 100, 201, 101, 203, 102, 205, 103, 207, 104, 209, 105, 211, 106, 213, 107, 215, 108, 217, 109, 219, 110, 221, 111, 223, 112, 225, 113, 227, 114, 229, 115, 231, 116, 233, 117, 235, 118, 237, 119, 239, 120, 241, 121, 243, 122, 245, 123, 247, 124, 249, 125, 251, 126, 253, 127, 255, 128, 257, 129, 259, 130, 261, 131, 263, 132, 265, 133, 267, 134, 269, 135, 271, 136, 273, 137, 275, 138, 277, 139, 279, 140, 281, 141, 283, 142, 285, 143, 287, 144, 289, 145, 291, 146, 293, 147, 295, 148, 297, 149, 299, 150, 301, 151, 303, 152, 305, 153, 307, 154, 309, 155, 311, 156, 313, 157, 315, 158, 317, 159, 319, 160, 321, 161, 323, 162, 325, 163, 327, 164, 329, 165, 331, 166, 333, 167, 335, 168, 337, 169, 339, 170, 341, 171, 343, 172, 345, 173, 347, 174, 349, 175, 351, 176, 353, 177, 355, 178, 357, 179, 359, 180, 361, 181, 363, 182, 365, 183, 367, 184, 369, 185, 371, 186, 373, 187, 375, 188, 377, 189, 379, 190, 381, 191, 383, 192, 385, 193, 387, 194, 389, 195, 391, 196, 393, 197, 395, 198, 397, 199, 399, 200, 401, 201, 403, 202, 405, 203, 407, 204, 409, 205, 411, 206, 413, 207, 415, 208, 417, 209, 419, 210, 421, 211, 423, 212, 425, 213, 427, 214, 429, 215, 431, 216, 433, 217, 435, 218, 437, 219, 439, 220, 441, 221, 443, 222, 445, 223, 447, 224, 449, 225, 451, 226, 453, 227, 455, 228, 457, 229, 459, 230, 461, 231, 463, 232, 465, 233, 467, 234, 469, 235, 471, 236, 473, 237, 475, 238, 477, 239, 479, 240, 481, 241, 483, 242, 485, 243, 487, 244, 489, 245, 491, 246, 493, 247, 495, 248, 497, 249, 499, 250, 501, 251, 503, 252, 505, 253, 507, 254, 509, 255, 511, 256, 513, 257, 515, 258, 517, 259, 519, 260, 521, 261, 523, 262, 525, 263, 527, 264, 529, 265, 531, 266, 533, 267, 535, 268, 537, 269, 539, 270, 541, 271, 543, 272, 545, 273, 547, 274, 549, 275, 551, 276, 553, 277, 555, 278, 557, 279, 559, 280, 561, 281, 563, 282, 565, 283, 567, 284, 569, 285, 571, 286, 573, 287, 575, 288, 577, 289, 579, 290, 581, 291, 583, 292, 585, 293, 587, 294, 589, 295, 591, 296, 593, 297, 595, 298, 597, 299, 599, 300, 601, 301, 603, 302, 605, 303, 607, 304, 609, 305, 611, 306, 613, 307, 615, 308, 617, 309, 619, 310, 621, 311, 623, 312, 625, 313, 627, 314, 629, 315, 631, 316, 633, 317, 635, 318, 637, 319, 639, 320, 641, 321, 643, 322, 645, 323, 647, 324, 649, 325, 651, 326, 653, 327, 655, 328, 657, 329, 659, 330, 661, 331, 663, 332, 665, 333, 667, 334, 669, 335, 671, 336, 673, 337, 675, 338, 677, 339, 679, 340, 681, 341, 683, 342, 685, 0, 687, 0, 689, 0, 691, 0, 693, 343, 695, 344, 697, 345, 699, 346, 1, 0, 10, 2, 0, 39, 39, 92, 92, 1, 0, 39, 39, 1, 0, 34, 34, 2, 0, 34, 34, 92, 92, 1, 0, 96, 96, 2, 0, 43, 43, 45, 45, 1, 0, 48, 57, 1, 0, 65, 90, 2, 0, 10, 10, 13, 13, 3, 0, 9, 10, 13, 13, 32, 32, 3345, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0, 55, 1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 63, 1, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0, 0, 71, 1, 0, 0, 0, 0, 73, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 0, 77, 1, 0, 0, 0, 0, 79, 1, 0, 0, 0, 0, 81, 1, 0, 0, 0, 0, 83, 1, 0, 0, 0, 0, 85, 1, 0, 0, 0, 0, 87, 1, 0, 0, 0, 0, 89, 1, 0, 0, 0, 0, 91, 1, 0, 0, 0, 0, 93, 1, 0, 0, 0, 0, 95, 1, 0, 0, 0, 0, 97, 1, 0, 0, 0, 0, 99, 1, 0, 0, 0, 0, 101, 1, 0, 0, 0, 0, 103, 1, 0, 0, 0, 0, 105, 1, 0, 0, 0, 0, 107, 1, 0, 0, 0, 0, 109, 1, 0, 0, 0, 0, 111, 1, 0, 0, 0, 0, 113, 1, 0, 0, 0, 0, 115, 1, 0, 0, 0, 0, 117, 1, 0, 0, 0, 0, 119, 1, 0, 0, 0, 0, 121, 1, 0, 0, 0, 0, 123, 1, 0, 0, 0, 0, 125, 1, 0, 0, 0, 0, 127, 1, 0, 0, 0, 0, 129, 1, 0, 0, 0, 0, 131, 1, 0, 0, 0, 0, 133, 1, 0, 0, 0, 0, 135, 1, 0, 0, 0, 0, 137, 1, 0, 0, 0, 0, 139, 1, 0, 0, 0, 0, 141, 1, 0, 0, 0, 0, 143, 1, 0, 0, 0, 0, 145, 1, 0, 0, 0, 0, 147, 1, 0, 0, 0, 0, 149, 1, 0, 0, 0, 0, 151, 1, 0, 0, 0, 0, 153, 1, 0, 0, 0, 0, 155, 1, 0, 0, 0, 0, 157, 1, 0, 0, 0, 0, 159, 1, 0, 0, 0, 0, 161, 1, 0, 0, 0, 0, 163, 1, 0, 0, 0, 0, 165, 1, 0, 0, 0, 0, 167, 1, 0, 0, 0, 0, 169, 1, 0, 0, 0, 0, 171, 1, 0, 0, 0, 0, 173, 1, 0, 0, 0, 0, 175, 1, 0, 0, 0, 0, 177, 1, 0, 0, 0, 0, 179, 1, 0, 0, 0, 0, 181, 1, 0, 0, 0, 0, 183, 1, 0, 0, 0, 0, 185, 1, 0, 0, 0, 0, 187, 1, 0, 0, 0, 0, 189, 1, 0, 0, 0, 0, 191, 1, 0, 0, 0, 0, 193, 1, 0, 0, 0, 0, 195, 1, 0, 0, 0, 0, 197, 1, 0, 0, 0, 0, 199, 1, 0, 0, 0, 0, 201, 1, 0, 0, 0, 0, 203, 1, 0, 0, 0, 0, 205, 1, 0, 0, 0, 0, 207, 1, 0, 0, 0, 0, 209, 1, 0, 0, 0, 0, 211, 1, 0, 0, 0, 0, 213, 1, 0, 0, 0, 0, 215, 1, 0, 0, 0, 0, 217, 1, 0, 0, 0, 0, 219, 1, 0, 0, 0, 0, 221, 1, 0, 0, 0, 0, 223, 1, 0, 0, 0, 0, 225, 1, 0, 0, 0, 0, 227, 1, 0, 0, 0, 0, 229, 1, 0, 0, 0, 0, 231, 1, 0, 0, 0, 0, 233, 1, 0, 0, 0, 0, 235, 1, 0, 0, 0, 0, 237, 1, 0, 0, 0, 0, 239, 1, 0, 0, 0, 0, 241, 1, 0, 0, 0, 0, 243, 1, 0, 0, 0, 0, 245, 1, 0, 0, 0, 0, 247, 1, 0, 0, 0, 0, 249, 1, 0, 0, 0, 0, 251, 1, 0, 0, 0, 0, 253, 1, 0, 0, 0, 0, 255, 1, 0, 0, 0, 0, 257, 1, 0, 0, 0, 0, 259, 1, 0, 0, 0, 0, 261, 1, 0, 0, 0, 0, 263, 1, 0, 0, 0, 0, 265, 1, 0, 0, 0, 0, 267, 1, 0, 0, 0, 0, 269, 1, 0, 0, 0, 0, 271, 1, 0, 0, 0, 0, 273, 1, 0, 0, 0, 0, 275, 1, 0, 0, 0, 0, 277, 1, 0, 0, 0, 0, 279, 1, 0, 0, 0, 0, 281, 1, 0, 0, 0, 0, 283, 1, 0, 0, 0, 0, 285, 1, 0, 0, 0, 0, 287, 1, 0, 0, 0, 0, 289, 1, 0, 0, 0, 0, 291, 1, 0, 0, 0, 0, 293, 1, 0, 0, 0, 0, 295, 1, 0, 0, 0, 0, 297, 1, 0, 0, 0, 0, 299, 1, 0, 0, 0, 0, 301, 1, 0, 0, 0, 0, 303, 1, 0, 0, 0, 0, 305, 1, 0, 0, 0, 0, 307, 1, 0, 0, 0, 0, 309, 1, 0, 0, 0, 0, 311, 1, 0, 0, 0, 0, 313, 1, 0, 0, 0, 0, 315, 1, 0, 0, 0, 0, 317, 1, 0, 0, 0, 0, 319, 1, 0, 0, 0, 0, 321, 1, 0, 0, 0, 0, 323, 1, 0, 0, 0, 0, 325, 1, 0, 0, 0, 0, 327, 1, 0, 0, 0, 0, 329, 1, 0, 0, 0, 0, 331, 1, 0, 0, 0, 0, 333, 1, 0, 0, 0, 0, 335, 1, 0, 0, 0, 0, 337, 1, 0, 0, 0, 0, 339, 1, 0, 0, 0, 0, 341, 1, 0, 0, 0, 0, 343, 1, 0, 0, 0, 0, 345, 1, 0, 0, 0, 0, 347, 1, 0, 0, 0, 0, 349, 1, 0, 0, 0, 0, 351, 1, 0, 0, 0, 0, 353, 1, 0, 0, 0, 0, 355, 1, 0, 0, 0, 0, 357, 1, 0, 0, 0, 0, 359, 1, 0, 0, 0, 0, 361, 1, 0, 0, 0, 0, 363, 1, 0, 0, 0, 0, 365, 1, 0, 0, 0, 0, 367, 1, 0, 0, 0, 0, 369, 1, 0, 0, 0, 0, 371, 1, 0, 0, 0, 0, 373, 1, 0, 0, 0, 0, 375, 1, 0, 0, 0, 0, 377, 1, 0, 0, 0, 0, 379, 1, 0, 0, 0, 0, 381, 1, 0, 0, 0, 0, 383, 1, 0, 0, 0, 0, 385, 1, 0, 0, 0, 0, 387, 1, 0, 0, 0, 0, 389, 1, 0, 0, 0, 0, 391, 1, 0, 0, 0, 0, 393, 1, 0, 0, 0, 0, 395, 1, 0, 0, 0, 0, 397, 1, 0, 0, 0, 0, 399, 1, 0, 0, 0, 0, 401, 1, 0, 0, 0, 0, 403, 1, 0, 0, 0, 0, 405, 1, 0, 0, 0, 0, 407, 1, 0, 0, 0, 0, 409, 1, 0, 0, 0, 0, 411, 1, 0, 0, 0, 0, 413, 1, 0, 0, 0, 0, 415, 1, 0, 0, 0, 0, 417, 1, 0, 0, 0, 0, 419, 1, 0, 0, 0, 0, 421, 1, 0, 0, 0, 0, 423, 1, 0, 0, 0, 0, 425, 1, 0, 0, 0, 0, 427, 1, 0, 0, 0, 0, 429, 1, 0, 0, 0, 0, 431, 1, 0, 0, 0, 0, 433, 1, 0, 0, 0, 0, 435, 1, 0, 0, 0, 0, 437, 1, 0, 0, 0, 0, 439, 1, 0, 0, 0, 0, 441, 1, 0, 0, 0, 0, 443, 1, 0, 0, 0, 0, 445, 1, 0, 0, 0, 0, 447, 1, 0, 0, 0, 0, 449, 1, 0, 0, 0, 0, 451, 1, 0, 0, 0, 0, 453, 1, 0, 0, 0, 0, 455, 1, 0, 0, 0, 0, 457, 1, 0, 0, 0, 0, 459, 1, 0, 0, 0, 0, 461, 1, 0, 0, 0, 0, 463, 1, 0, 0, 0, 0, 465, 1, 0, 0, 0, 0, 467, 1, 0, 0, 0, 0, 469, 1, 0, 0, 0, 0, 471, 1, 0, 0, 0, 0, 473, 1, 0, 0, 0, 0, 475, 1, 0, 0, 0, 0, 477, 1, 0, 0, 0, 0, 479, 1, 0, 0, 0, 0, 481, 1, 0, 0, 0, 0, 483, 1, 0, 0, 0, 0, 485, 1, 0, 0, 0, 0, 487, 1, 0, 0, 0, 0, 489, 1, 0, 0, 0, 0, 491, 1, 0, 0, 0, 0, 493, 1, 0, 0, 0, 0, 495, 1, 0, 0, 0, 0, 497, 1, 0, 0, 0, 0, 499, 1, 0, 0, 0, 0, 501, 1, 0, 0, 0, 0, 503, 1, 0, 0, 0, 0, 505, 1, 0, 0, 0, 0, 507, 1, 0, 0, 0, 0, 509, 1, 0, 0, 0, 0, 511, 1, 0, 0, 0, 0, 513, 1, 0, 0, 0, 0, 515, 1, 0, 0, 0, 0, 517, 1, 0, 0, 0, 0, 519, 1, 0, 0, 0, 0, 521, 1, 0, 0, 0, 0, 523, 1, 0, 0, 0, 0, 525, 1, 0, 0, 0, 0, 527, 1, 0, 0, 0, 0, 529, 1, 0, 0, 0, 0, 531, 1, 0, 0, 0, 0, 533, 1, 0, 0, 0, 0, 535, 1, 0, 0, 0, 0, 537, 1, 0, 0, 0, 0, 539, 1, 0, 0, 0, 0, 541, 1, 0, 0, 0, 0, 543, 1, 0, 0, 0, 0, 545, 1, 0, 0, 0, 0, 547, 1, 0, 0, 0, 0, 549, 1, 0, 0, 0, 0, 551, 1, 0, 0, 0, 0, 553, 1, 0, 0, 0, 0, 555, 1, 0, 0, 0, 0, 557, 1, 0, 0, 0, 0, 559, 1, 0, 0, 0, 0, 561, 1, 0, 0, 0, 0, 563, 1, 0, 0, 0, 0, 565, 1, 0, 0, 0, 0, 567, 1, 0, 0, 0, 0, 569, 1, 0, 0, 0, 0, 571, 1, 0, 0, 0, 0, 573, 1, 0, 0, 0, 0, 575, 1, 0, 0, 0, 0, 577, 1, 0, 0, 0, 0, 579, 1, 0, 0, 0, 0, 581, 1, 0, 0, 0, 0, 583, 1, 0, 0, 0, 0, 585, 1, 0, 0, 0, 0, 587, 1, 0, 0, 0, 0, 589, 1, 0, 0, 0, 0, 591, 1, 0, 0, 0, 0, 593, 1, 0, 0, 0, 0, 595, 1, 0, 0, 0, 0, 597, 1, 0, 0, 0, 0, 599, 1, 0, 0, 0, 0, 601, 1, 0, 0, 0, 0, 603, 1, 0, 0, 0, 0, 605, 1, 0, 0, 0, 0, 607, 1, 0, 0, 0, 0, 609, 1, 0, 0, 0, 0, 611, 1, 0, 0, 0, 0, 613, 1, 0, 0, 0, 0, 615, 1, 0, 0, 0, 0, 617, 1, 0, 0, 0, 0, 619, 1, 0, 0, 0, 0, 621, 1, 0, 0, 0, 0, 623, 1, 0, 0, 0, 0, 625, 1, 0, 0, 0, 0, 627, 1, 0, 0, 0, 0, 629, 1, 0, 0, 0, 0, 631, 1, 0, 0, 0, 0, 633, 1, 0, 0, 0, 0, 635, 1, 0, 0, 0, 0, 637, 1, 0, 0, 0, 0, 639, 1, 0, 0, 0, 0, 641, 1, 0, 0, 0, 0, 643, 1, 0, 0, 0, 0, 645, 1, 0, 0, 0, 0, 647, 1, 0, 0, 0, 0, 649, 1, 0, 0, 0, 0, 651, 1, 0, 0, 0, 0, 653, 1, 0, 0, 0, 0, 655, 1, 0, 0, 0, 0, 657, 1, 0, 0, 0, 0, 659, 1, 0, 0, 0, 0, 661, 1, 0, 0, 0, 0, 663, 1, 0, 0, 0, 0, 665, 1, 0, 0, 0, 0, 667, 1, 0, 0, 0, 0, 669, 1, 0, 0, 0, 0, 671, 1, 0, 0, 0, 0, 673, 1, 0, 0, 0, 0, 675, 1, 0, 0, 0, 0, 677, 1, 0, 0, 0, 0, 679, 1, 0, 0, 0, 0, 681, 1, 0, 0, 0, 0, 683, 1, 0, 0, 0, 0, 693, 1, 0, 0, 0, 0, 695, 1, 0, 0, 0, 0, 697, 1, 0, 0, 0, 0, 699, 1, 0, 0, 0, 1, 701, 1, 0, 0, 0, 3, 703, 1, 0, 0, 0, 5, 705, 1, 0, 0, 0, 7, 707, 1, 0, 0, 0, 9, 709, 1, 0, 0, 0, 11, 711, 1, 0, 0, 0, 13, 713, 1, 0, 0, 0, 15, 715, 1, 0, 0, 0, 17, 719, 1, 0, 0, 0, 19, 725, 1, 0, 0, 0, 21, 729, 1, 0, 0, 0, 23, 735, 1, 0, 0, 0, 25, 742, 1, 0, 0, 0, 27, 750, 1, 0, 0, 0, 29, 754, 1, 0, 0, 0, 31, 759, 1, 0, 0, 0, 33, 763, 1, 0, 0, 0, 35, 773, 1, 0, 0, 0, 37, 781, 1, 0, 0, 0, 39, 787, 1, 0, 0, 0, 41, 790, 1, 0, 0, 0, 43, 794, 1, 0, 0, 0, 45, 797, 1, 0, 0, 0, 47, 811, 1, 0, 0, 0, 49, 819, 1, 0, 0, 0, 51, 824, 1, 0, 0, 0, 53, 831, 1, 0, 0, 0, 55, 839, 1, 0, 0, 0, 57, 842, 1, 0, 0, 0, 59, 848, 1, 0, 0, 0, 61, 856, 1, 0, 0, 0, 63, 861, 1, 0, 0, 0, 65, 866, 1, 0, 0, 0, 67, 874, 1, 0, 0, 0, 69, 883, 1, 0, 0, 0, 71, 890, 1, 0, 0, 0, 73, 896, 1, 0, 0, 0, 75, 902, 1, 0, 0, 0, 77, 910, 1, 0, 0, 0, 79, 920, 1, 0, 0, 0, 81, 928, 1, 0, 0, 0, 83, 936, 1, 0, 0, 0, 85, 947, 1, 0, 0, 0, 87, 954, 1, 0, 0, 0, 89, 962, 1, 0, 0, 0, 91, 970, 1, 0, 0, 0, 93, 977, 1, 0, 0, 0, 95, 985, 1, 0, 0, 0, 97, 997, 1, 0, 0, 0, 99, 1005, 1, 0, 0, 0, 101, 1017, 1, 0, 0, 0, 103, 1028, 1, 0, 0, 0, 105, 1033, 1, 0, 0, 0, 107, 1040, 1, 0, 0, 0, 109, 1046, 1, 0, 0, 0, 111, 1051, 1, 0, 0, 0, 113, 1059, 1, 0, 0, 0, 115, 1072, 1, 0, 0, 0, 117, 1085, 1, 0, 0, 0, 119, 1103, 1, 0, 0, 0, 121, 1116, 1, 0, 0, 0, 123, 1120, 1, 0, 0, 0, 125, 1125, 1, 0, 0, 0, 127, 1135, 1, 0, 0, 0, 129, 1140, 1, 0, 0, 0, 131, 1149, 1, 0, 0, 0, 133, 1159, 1, 0, 0, 0, 135, 1167, 1, 0, 0, 0, 137, 1176, 1, 0, 0, 0, 139, 1189, 1, 0, 0, 0, 141, 1197, 1, 0, 0, 0, 143, 1205, 1, 0, 0, 0, 145, 1212, 1, 0, 0, 0, 147, 1222, 1, 0, 0, 0, 149, 1227, 1, 0, 0, 0, 151, 1236, 1, 0, 0, 0, 153, 1240, 1, 0, 0, 0, 155, 1252, 1, 0, 0, 0, 157, 1262, 1, 0, 0, 0, 159, 1271, 1, 0, 0, 0, 161, 1282, 1, 0, 0, 0, 163, 1286, 1, 0, 0, 0, 165, 1291, 1, 0, 0, 0, 167, 1296, 1, 0, 0, 0, 169, 1300, 1, 0, 0, 0, 171, 1307, 1, 0, 0, 0, 173, 1315, 1, 0, 0, 0, 175, 1322, 1, 0, 0, 0, 177, 1331, 1, 0, 0, 0, 179, 1339, 1, 0, 0, 0, 181, 1346, 1, 0, 0, 0, 183, 1354, 1, 0, 0, 0, 185, 1361, 1, 0, 0, 0, 187, 1370, 1, 0, 0, 0, 189, 1379, 1, 0, 0, 0, 191, 1387, 1, 0, 0, 0, 193, 1393, 1, 0, 0, 0, 195, 1399, 1, 0, 0, 0, 197, 1406, 1, 0, 0, 0, 199, 1413, 1, 0, 0, 0, 201, 1424, 1, 0, 0, 0, 203, 1430, 1, 0, 0, 0, 205, 1440, 1, 0, 0, 0, 207, 1444, 1, 0, 0, 0, 209, 1452, 1, 0, 0, 0, 211, 1459, 1, 0, 0, 0, 213, 1469, 1, 0, 0, 0, 215, 1474, 1, 0, 0, 0, 217, 1479, 1, 0, 0, 0, 219, 1488, 1, 0, 0, 0, 221, 1498, 1, 0, 0, 0, 223, 1508, 1, 0, 0, 0, 225, 1515, 1, 0, 0, 0, 227, 1521, 1, 0, 0, 0, 229, 1527, 1, 0, 0, 0, 231, 1536, 1, 0, 0, 0, 233, 1543, 1, 0, 0, 0, 235, 1548, 1, 0, 0, 0, 237, 1554, 1, 0, 0, 0, 239, 1557, 1, 0, 0, 0, 241, 1564, 1, 0, 0, 0, 243, 1571, 1, 0, 0, 0, 245, 1574, 1, 0, 0, 0, 247, 1582, 1, 0, 0, 0, 249, 1588, 1, 0, 0, 0, 251, 1596, 1, 0, 0, 0, 253, 1602, 1, 0, 0, 0, 255, 1609, 1, 0, 0, 0, 257, 1621, 1, 0, 0, 0, 259, 1628, 1, 0, 0, 0, 261, 1638, 1, 0, 0, 0, 263, 1647, 1, 0, 0, 0, 265, 1652, 1, 0, 0, 0, 267, 1655, 1, 0, 0, 0, 269, 1661, 1, 0, 0, 0, 271, 1666, 1, 0, 0, 0, 273, 1671, 1, 0, 0, 0, 275, 1676, 1, 0, 0, 0, 277, 1684, 1, 0, 0, 0, 279, 1689, 1, 0, 0, 0, 281, 1697, 1, 0, 0, 0, 283, 1702, 1, 0, 0, 0, 285, 1707, 1, 0, 0, 0, 287, 1713, 1, 0, 0, 0, 289, 1719, 1, 0, 0, 0, 291, 1725, 1, 0, 0, 0, 293, 1730, 1, 0, 0, 0, 295, 1735, 1, 0, 0, 0, 297, 1741, 1, 0, 0, 0, 299, 1750, 1, 0, 0, 0, 301, 1755, 1, 0, 0, 0, 303, 1761, 1, 0, 0, 0, 305, 1769, 1, 0, 0, 0, 307, 1775, 1, 0, 0, 0, 309, 1779, 1, 0, 0, 0, 311, 1787, 1, 0, 0, 0, 313, 1793, 1, 0, 0, 0, 315, 1805, 1, 0, 0, 0, 317, 1818, 1, 0, 0, 0, 319, 1830, 1, 0, 0, 0, 321, 1843, 1, 0, 0, 0, 323, 1850, 1, 0, 0, 0, 325, 1858, 1, 0, 0, 0, 327, 1864, 1, 0, 0, 0, 329, 1871, 1, 0, 0, 0, 331, 1876, 1, 0, 0, 0, 333, 1886, 1, 0, 0, 0, 335, 1897, 1, 0, 0, 0, 337, 1908, 1, 0, 0, 0, 339, 1920, 1, 0, 0, 0, 341, 1928, 1, 0, 0, 0, 343, 1935, 1, 0, 0, 0, 345, 1937, 1, 0, 0, 0, 347, 1942, 1, 0, 0, 0, 349, 1948, 1, 0, 0, 0, 351, 1951, 1, 0, 0, 0, 353, 1958, 1, 0, 0, 0, 355, 1961, 1, 0, 0, 0, 357, 1966, 1, 0, 0, 0, 359, 1973, 1, 0, 0, 0, 361, 1981, 1, 0, 0, 0, 363, 1984, 1, 0, 0, 0, 365, 1990, 1, 0, 0, 0, 367, 1994, 1, 0, 0, 0, 369, 2000, 1, 0, 0, 0, 371, 2013, 1, 0, 0, 0, 373, 2018, 1, 0, 0, 0, 375, 2027, 1, 0, 0, 0, 377, 2035, 1, 0, 0, 0, 379, 2045, 1, 0, 0, 0, 381, 2055, 1, 0, 0, 0, 383, 2067, 1, 0, 0, 0, 385, 2078, 1, 0, 0, 0, 387, 2094, 1, 0, 0, 0, 389, 2110, 1, 0, 0, 0, 391, 2118, 1, 0, 0, 0, 393, 2124, 1, 0, 0, 0, 395, 2132, 1, 0, 0, 0, 397, 2141, 1, 0, 0, 0, 399, 2151, 1, 0, 0, 0, 401, 2159, 1, 0, 0, 0, 403, 2170, 1, 0, 0, 0, 405, 2181, 1, 0, 0, 0, 407, 2187, 1, 0, 0, 0, 409, 2195, 1, 0, 0, 0, 411, 2201, 1, 0, 0, 0, 413, 2207, 1, 0, 0, 0, 415, 2220, 1, 0, 0, 0, 417, 2233, 1, 0, 0, 0, 419, 2241, 1, 0, 0, 0, 421, 2248, 1, 0, 0, 0, 423, 2259, 1, 0, 0, 0, 425, 2267, 1, 0, 0, 0, 427, 2274, 1, 0, 0, 0, 429, 2281, 1, 0, 0, 0, 431, 2292, 1, 0, 0, 0, 433, 2300, 1, 0, 0, 0, 435, 2306, 1, 0, 0, 0, 437, 2314, 1, 0, 0, 0, 439, 2323, 1, 0, 0, 0, 441, 2330, 1, 0, 0, 0, 443, 2347, 1, 0, 0, 0, 445, 2349, 1, 0, 0, 0, 447, 2354, 1, 0, 0, 0, 449, 2360, 1, 0, 0, 0, 451, 2369, 1, 0, 0, 0, 453, 2376, 1, 0, 0, 0, 455, 2380, 1, 0, 0, 0, 457, 2385, 1, 0, 0, 0, 459, 2392, 1, 0, 0, 0, 461, 2400, 1, 0, 0, 0, 463, 2407, 1, 0, 0, 0, 465, 2415, 1, 0, 0, 0, 467, 2422, 1, 0, 0, 0, 469, 2427, 1, 0, 0, 0, 471, 2437, 1, 0, 0, 0, 473, 2443, 1, 0, 0, 0, 475, 2459, 1, 0, 0, 0, 477, 2472, 1, 0, 0, 0, 479, 2476, 1, 0, 0, 0, 481, 2482, 1, 0, 0, 0, 483, 2487, 1, 0, 0, 0, 485, 2492, 1, 0, 0, 0, 487, 2499, 1, 0, 0, 0, 489, 2504, 1, 0, 0, 0, 491, 2509, 1, 0, 0, 0, 493, 2516, 1, 0, 0, 0, 495, 2523, 1, 0, 0, 0, 497, 2529, 1, 0, 0, 0, 499, 2540, 1, 0, 0, 0, 501, 2547, 1, 0, 0, 0, 503, 2556, 1, 0, 0, 0, 505, 2563, 1, 0, 0, 0, 507, 2570, 1, 0, 0, 0, 509, 2580, 1, 0, 0, 0, 511, 2585, 1, 0, 0, 0, 513, 2597, 1, 0, 0, 0, 515, 2612, 1, 0, 0, 0, 517, 2618, 1, 0, 0, 0, 519, 2625, 1, 0, 0, 0, 521, 2637, 1, 0, 0, 0, 523, 2644, 1, 0, 0, 0, 525, 2671, 1, 0, 0, 0, 527, 2673, 1, 0, 0, 0, 529, 2684, 1, 0, 0, 0, 531, 2689, 1, 0, 0, 0, 533, 2694, 1, 0, 0, 0, 535, 2704, 1, 0, 0, 0, 537, 2717, 1, 0, 0, 0, 539, 2731, 1, 0, 0, 0, 541, 2734, 1, 0, 0, 0, 543, 2740, 1, 0, 0, 0, 545, 2749, 1, 0, 0, 0, 547, 2761, 1, 0, 0, 0, 549, 2774, 1, 0, 0, 0, 551, 2784, 1, 0, 0, 0, 553, 2789, 1, 0, 0, 0, 555, 2794, 1, 0, 0, 0, 557, 2803, 1, 0, 0, 0, 559, 2812, 1, 0, 0, 0, 561, 2817, 1, 0, 0, 0, 563, 2827, 1, 0, 0, 0, 565, 2837, 1, 0, 0, 0, 567, 2845, 1, 0, 0, 0, 569, 2851, 1, 0, 0, 0, 571, 2858, 1, 0, 0, 0, 573, 2866, 1, 0, 0, 0, 575, 2873, 1, 0, 0, 0, 577, 2881, 1, 0, 0, 0, 579, 2887, 1, 0, 0, 0, 581, 2894, 1, 0, 0, 0, 583, 2898, 1, 0, 0, 0, 585, 2903, 1, 0, 0, 0, 587, 2909, 1, 0, 0, 0, 589, 2916, 1, 0, 0, 0, 591, 2924, 1, 0, 0, 0, 593, 2929, 1, 0, 0, 0, 595, 2935, 1, 0, 0, 0, 597, 2940, 1, 0, 0, 0, 599, 2946, 1, 0, 0, 0, 601, 2951, 1, 0, 0, 0, 603, 2957, 1, 0, 0, 0, 605, 2964, 1, 0, 0, 0, 607, 2969, 1, 0, 0, 0, 609, 2976, 1, 0, 0, 0, 611, 2981, 1, 0, 0, 0, 613, 2987, 1, 0, 0, 0, 615, 2995, 1, 0, 0, 0, 617, 2997, 1, 0, 0, 0, 619, 3001, 1, 0, 0, 0, 621, 3004, 1, 0, 0, 0, 623, 3007, 1, 0, 0, 0, 625, 3013, 1, 0, 0, 0, 627, 3015, 1, 0, 0, 0, 629, 3021, 1, 0, 0, 0, 631, 3023, 1, 0, 0, 0, 633, 3025, 1, 0, 0, 0, 635, 3027, 1, 0, 0, 0, 637, 3029, 1, 0, 0, 0, 639, 3031, 1, 0, 0, 0, 641, 3033, 1, 0, 0, 0, 643, 3035, 1, 0, 0, 0, 645, 3037, 1, 0, 0, 0, 647, 3039, 1, 0, 0, 0, 649, 3042, 1, 0, 0, 0, 651, 3044, 1, 0, 0, 0, 653, 3046, 1, 0, 0, 0, 655, 3049, 1, 0, 0, 0, 657, 3053, 1, 0, 0, 0, 659, 3086, 1, 0, 0, 0, 661, 3088, 1, 0, 0, 0, 663, 3100, 1, 0, 0, 0, 665, 3107, 1, 0, 0, 0, 667, 3114, 1, 0, 0, 0, 669, 3121, 1, 0, 0, 0, 671, 3136, 1, 0, 0, 0, 673, 3138, 1, 0, 0, 0, 675, 3158, 1, 0, 0, 0, 677, 3177, 1, 0, 0, 0, 679, 3199, 1, 0, 0, 0, 681, 3204, 1, 0, 0, 0, 683, 3208, 1, 0, 0, 0, 685, 3237, 1, 0, 0, 0, 687, 3239, 1, 0, 0, 0, 689, 3248, 1, 0, 0, 0, 691, 3250, 1, 0, 0, 0, 693, 3252, 1, 0, 0, 0, 695, 3271, 1, 0, 0, 0, 697, 3291, 1, 0, 0, 0, 699, 3297, 1, 0, 0, 0, 701, 702, 5, 59, 0, 0, 702, 2, 1, 0, 0, 0, 703, 704, 5, 40, 0, 0, 704, 4, 1, 0, 0, 0, 705, 706, 5, 41, 0, 0, 706, 6, 1, 0, 0, 0, 707, 708, 5, 44, 0, 0, 708, 8, 1, 0, 0, 0, 709, 710, 5, 46, 0, 0, 710, 10, 1, 0, 0, 0, 711, 712, 5, 91, 0, 0, 712, 12, 1, 0, 0, 0, 713, 714, 5, 93, 0, 0, 714, 14, 1, 0, 0, 0, 715, 716, 5, 65, 0, 0, 716, 717, 5, 68, 0, 0, 717, 718, 5, 68, 0, 0, 718, 16, 1, 0, 0, 0, 719, 720, 5, 65, 0, 0, 720, 721, 5, 70, 0, 0, 721, 722, 5, 84, 0, 0, 722, 723, 5, 69, 0, 0, 723, 724, 5, 82, 0, 0, 724, 18, 1, 0, 0, 0, 725, 726, 5, 65, 0, 0, 726, 727, 5, 76, 0, 0, 727, 728, 5, 76, 0, 0, 728, 20, 1, 0, 0, 0, 729, 730, 5, 65, 0, 0, 730, 731, 5, 76, 0, 0, 731, 732, 5, 84, 0, 0, 732, 733, 5, 69, 0, 0, 733, 734, 5, 82, 0, 0, 734, 22, 1, 0, 0, 0, 735, 736, 5, 65, 0, 0, 736, 737, 5, 76, 0, 0, 737, 738, 5, 87, 0, 0, 738, 739, 5, 65, 0, 0, 739, 740, 5, 89, 0, 0, 740, 741, 5, 83, 0, 0, 741, 24, 1, 0, 0, 0, 742, 743, 5, 65, 0, 0, 743, 744, 5, 78, 0, 0, 744, 745, 5, 65, 0, 0, 745, 746, 5, 76, 0, 0, 746, 747, 5, 89, 0, 0, 747, 748, 5, 90, 0, 0, 748, 749, 5, 69, 0, 0, 749, 26, 1, 0, 0, 0, 750, 751, 5, 65, 0, 0, 751, 752, 5, 78, 0, 0, 752, 753, 5, 68, 0, 0, 753, 28, 1, 0, 0, 0, 754, 755, 5, 65, 0, 0, 755, 756, 5, 78, 0, 0, 756, 757, 5, 84, 0, 0, 757, 758, 5, 73, 0, 0, 758, 30, 1, 0, 0, 0, 759, 760, 5, 65, 0, 0, 760, 761, 5, 78, 0, 0, 761, 762, 5, 89, 0, 0, 762, 32, 1, 0, 0, 0, 763, 764, 5, 65, 0, 0, 764, 765, 5, 78, 0, 0, 765, 766, 5, 89, 0, 0, 766, 767, 5, 95, 0, 0, 767, 768, 5, 86, 0, 0, 768, 769, 5, 65, 0, 0, 769, 770, 5, 76, 0, 0, 770, 771, 5, 85, 0, 0, 771, 772, 5, 69, 0, 0, 772, 34, 1, 0, 0, 0, 773, 774, 5, 65, 0, 0, 774, 775, 5, 82, 0, 0, 775, 776, 5, 67, 0, 0, 776, 777, 5, 72, 0, 0, 777, 778, 5, 73, 0, 0, 778, 779, 5, 86, 0, 0, 779, 780, 5, 69, 0, 0, 780, 36, 1, 0, 0, 0, 781, 782, 5, 65, 0, 0, 782, 783, 5, 82, 0, 0, 783, 784, 5, 82, 0, 0, 784, 785, 5, 65, 0, 0, 785, 786, 5, 89, 0, 0, 786, 38, 1, 0, 0, 0, 787, 788, 5, 65, 0, 0, 788, 789, 5, 83, 0, 0, 789, 40, 1, 0, 0, 0, 790, 791, 5, 65, 0, 0, 791, 792, 5, 83, 0, 0, 792, 793, 5, 67, 0, 0, 793, 42, 1, 0, 0, 0, 794, 795, 5, 65, 0, 0, 795, 796, 5, 84, 0, 0, 796, 44, 1, 0, 0, 0, 797, 798, 5, 65, 0, 0, 798, 799, 5, 85, 0, 0, 799, 800, 5, 84, 0, 0, 800, 801, 5, 72, 0, 0, 801, 802, 5, 79, 0, 0, 802, 803, 5, 82, 0, 0, 803, 804, 5, 73, 0, 0, 804, 805, 5, 90, 0, 0, 805, 806, 5, 65, 0, 0, 806, 807, 5, 84, 0, 0, 807, 808, 5, 73, 0, 0, 808, 809, 5, 79, 0, 0, 809, 810, 5, 78, 0, 0, 810, 46, 1, 0, 0, 0, 811, 812, 5, 66, 0, 0, 812, 813, 5, 69, 0, 0, 813, 814, 5, 84, 0, 0, 814, 815, 5, 87, 0, 0, 815, 816, 5, 69, 0, 0, 816, 817, 5, 69, 0, 0, 817, 818, 5, 78, 0, 0, 818, 48, 1, 0, 0, 0, 819, 820, 5, 66, 0, 0, 820, 821, 5, 79, 0, 0, 821, 822, 5, 84, 0, 0, 822, 823, 5, 72, 0, 0, 823, 50, 1, 0, 0, 0, 824, 825, 5, 66, 0, 0, 825, 826, 5, 85, 0, 0, 826, 827, 5, 67, 0, 0, 827, 828, 5, 75, 0, 0, 828, 829, 5, 69, 0, 0, 829, 830, 5, 84, 0, 0, 830, 52, 1, 0, 0, 0, 831, 832, 5, 66, 0, 0, 832, 833, 5, 85, 0, 0, 833, 834, 5, 67, 0, 0, 834, 835, 5, 75, 0, 0, 835, 836, 5, 69, 0, 0, 836, 837, 5, 84, 0, 0, 837, 838, 5, 83, 0, 0, 838, 54, 1, 0, 0, 0, 839, 840, 5, 66, 0, 0, 840, 841, 5, 89, 0, 0, 841, 56, 1, 0, 0, 0, 842, 843, 5, 67, 0, 0, 843, 844, 5, 65, 0, 0, 844, 845, 5, 67, 0, 0, 845, 846, 5, 72, 0, 0, 846, 847, 5, 69, 0, 0, 847, 58, 1, 0, 0, 0, 848, 849, 5, 67, 0, 0, 849, 850, 5, 65, 0, 0, 850, 851, 5, 83, 0, 0, 851, 852, 5, 67, 0, 0, 852, 853, 5, 65, 0, 0, 853, 854, 5, 68, 0, 0, 854, 855, 5, 69, 0, 0, 855, 60, 1, 0, 0, 0, 856, 857, 5, 67, 0, 0, 857, 858, 5, 65, 0, 0, 858, 859, 5, 83, 0, 0, 859, 860, 5, 69, 0, 0, 860, 62, 1, 0, 0, 0, 861, 862, 5, 67, 0, 0, 862, 863, 5, 65, 0, 0, 863, 864, 5, 83, 0, 0, 864, 865, 5, 84, 0, 0, 865, 64, 1, 0, 0, 0, 866, 867, 5, 67, 0, 0, 867, 868, 5, 65, 0, 0, 868, 869, 5, 84, 0, 0, 869, 870, 5, 65, 0, 0, 870, 871, 5, 76, 0, 0, 871, 872, 5, 79, 0, 0, 872, 873, 5, 71, 0, 0, 873, 66, 1, 0, 0, 0, 874, 875, 5, 67, 0, 0, 875, 876, 5, 65, 0, 0, 876, 877, 5, 84, 0, 0, 877, 878, 5, 65, 0, 0, 878, 879, 5, 76, 0, 0, 879, 880, 5, 79, 0, 0, 880, 881, 5, 71, 0, 0, 881, 882, 5, 83, 0, 0, 882, 68, 1, 0, 0, 0, 883, 884, 5, 67, 0, 0, 884, 885, 5, 72, 0, 0, 885, 886, 5, 65, 0, 0, 886, 887, 5, 78, 0, 0, 887, 888, 5, 71, 0, 0, 888, 889, 5, 69, 0, 0, 889, 70, 1, 0, 0, 0, 890, 891, 5, 67, 0, 0, 891, 892, 5, 72, 0, 0, 892, 893, 5, 69, 0, 0, 893, 894, 5, 67, 0, 0, 894, 895, 5, 75, 0, 0, 895, 72, 1, 0, 0, 0, 896, 897, 5, 67, 0, 0, 897, 898, 5, 76, 0, 0, 898, 899, 5, 69, 0, 0, 899, 900, 5, 65, 0, 0, 900, 901, 5, 82, 0, 0, 901, 74, 1, 0, 0, 0, 902, 903, 5, 67, 0, 0, 903, 904, 5, 76, 0, 0, 904, 905, 5, 85, 0, 0, 905, 906, 5, 83, 0, 0, 906, 907, 5, 84, 0, 0, 907, 908, 5, 69, 0, 0, 908, 909, 5, 82, 0, 0, 909, 76, 1, 0, 0, 0, 910, 911, 5, 67, 0, 0, 911, 912, 5, 76, 0, 0, 912, 913, 5, 85, 0, 0, 913, 914, 5, 83, 0, 0, 914, 915, 5, 84, 0, 0, 915, 916, 5, 69, 0, 0, 916, 917, 5, 82, 0, 0, 917, 918, 5, 69, 0, 0, 918, 919, 5, 68, 0, 0, 919, 78, 1, 0, 0, 0, 920, 921, 5, 67, 0, 0, 921, 922, 5, 79, 0, 0, 922, 923, 5, 68, 0, 0, 923, 924, 5, 69, 0, 0, 924, 925, 5, 71, 0, 0, 925, 926, 5, 69, 0, 0, 926, 927, 5, 78, 0, 0, 927, 80, 1, 0, 0, 0, 928, 929, 5, 67, 0, 0, 929, 930, 5, 79, 0, 0, 930, 931, 5, 76, 0, 0, 931, 932, 5, 76, 0, 0, 932, 933, 5, 65, 0, 0, 933, 934, 5, 84, 0, 0, 934, 935, 5, 69, 0, 0, 935, 82, 1, 0, 0, 0, 936, 937, 5, 67, 0, 0, 937, 938, 5, 79, 0, 0, 938, 939, 5, 76, 0, 0, 939, 940, 5, 76, 0, 0, 940, 941, 5, 69, 0, 0, 941, 942, 5, 67, 0, 0, 942, 943, 5, 84, 0, 0, 943, 944, 5, 73, 0, 0, 944, 945, 5, 79, 0, 0, 945, 946, 5, 78, 0, 0, 946, 84, 1, 0, 0, 0, 947, 948, 5, 67, 0, 0, 948, 949, 5, 79, 0, 0, 949, 950, 5, 76, 0, 0, 950, 951, 5, 85, 0, 0, 951, 952, 5, 77, 0, 0, 952, 953, 5, 78, 0, 0, 953, 86, 1, 0, 0, 0, 954, 955, 5, 67, 0, 0, 955, 956, 5, 79, 0, 0, 956, 957, 5, 76, 0, 0, 957, 958, 5, 85, 0, 0, 958, 959, 5, 77, 0, 0, 959, 960, 5, 78, 0, 0, 960, 961, 5, 83, 0, 0, 961, 88, 1, 0, 0, 0, 962, 963, 5, 67, 0, 0, 963, 964, 5, 79, 0, 0, 964, 965, 5, 77, 0, 0, 965, 966, 5, 77, 0, 0, 966, 967, 5, 69, 0, 0, 967, 968, 5, 78, 0, 0, 968, 969, 5, 84, 0, 0, 969, 90, 1, 0, 0, 0, 970, 971, 5, 67, 0, 0, 971, 972, 5, 79, 0, 0, 972, 973, 5, 77, 0, 0, 973, 974, 5, 77, 0, 0, 974, 975, 5, 73, 0, 0, 975, 976, 5, 84, 0, 0, 976, 92, 1, 0, 0, 0, 977, 978, 5, 67, 0, 0, 978, 979, 5, 79, 0, 0, 979, 980, 5, 77, 0, 0, 980, 981, 5, 80, 0, 0, 981, 982, 5, 65, 0, 0, 982, 983, 5, 67, 0, 0, 983, 984, 5, 84, 0, 0, 984, 94, 1, 0, 0, 0, 985, 986, 5, 67, 0, 0, 986, 987, 5, 79, 0, 0, 987, 988, 5, 77, 0, 0, 988, 989, 5, 80, 0, 0, 989, 990, 5, 65, 0, 0, 990, 991, 5, 67, 0, 0, 991, 992, 5, 84, 0, 0, 992, 993, 5, 73, 0, 0, 993, 994, 5, 79, 0, 0, 994, 995, 5, 78, 0, 0, 995, 996, 5, 83, 0, 0, 996, 96, 1, 0, 0, 0, 997, 998, 5, 67, 0, 0, 998, 999, 5, 79, 0, 0, 999, 1000, 5, 77, 0, 0, 1000, 1001, 5, 80, 0, 0, 1001, 1002, 5, 85, 0, 0, 1002, 1003, 5, 84, 0, 0, 1003, 1004, 5, 69, 0, 0, 1004, 98, 1, 0, 0, 0, 1005, 1006, 5, 67, 0, 0, 1006, 1007, 5, 79, 0, 0, 1007, 1008, 5, 78, 0, 0, 1008, 1009, 5, 67, 0, 0, 1009, 1010, 5, 65, 0, 0, 1010, 1011, 5, 84, 0, 0, 1011, 1012, 5, 69, 0, 0, 1012, 1013, 5, 78, 0, 0, 1013, 1014, 5, 65, 0, 0, 1014, 1015, 5, 84, 0, 0, 1015, 1016, 5, 69, 0, 0, 1016, 100, 1, 0, 0, 0, 1017, 1018, 5, 67, 0, 0, 1018, 1019, 5, 79, 0, 0, 1019, 1020, 5, 78, 0, 0, 1020, 1021, 5, 83, 0, 0, 1021, 1022, 5, 84, 0, 0, 1022, 1023, 5, 82, 0, 0, 1023, 1024, 5, 65, 0, 0, 1024, 1025, 5, 73, 0, 0, 1025, 1026, 5, 78, 0, 0, 1026, 1027, 5, 84, 0, 0, 1027, 102, 1, 0, 0, 0, 1028, 1029, 5, 67, 0, 0, 1029, 1030, 5, 79, 0, 0, 1030, 1031, 5, 83, 0, 0, 1031, 1032, 5, 84, 0, 0, 1032, 104, 1, 0, 0, 0, 1033, 1034, 5, 67, 0, 0, 1034, 1035, 5, 82, 0, 0, 1035, 1036, 5, 69, 0, 0, 1036, 1037, 5, 65, 0, 0, 1037, 1038, 5, 84, 0, 0, 1038, 1039, 5, 69, 0, 0, 1039, 106, 1, 0, 0, 0, 1040, 1041, 5, 67, 0, 0, 1041, 1042, 5, 82, 0, 0, 1042, 1043, 5, 79, 0, 0, 1043, 1044, 5, 83, 0, 0, 1044, 1045, 5, 83, 0, 0, 1045, 108, 1, 0, 0, 0, 1046, 1047, 5, 67, 0, 0, 1047, 1048, 5, 85, 0, 0, 1048, 1049, 5, 66, 0, 0, 1049, 1050, 5, 69, 0, 0, 1050, 110, 1, 0, 0, 0, 1051, 1052, 5, 67, 0, 0, 1052, 1053, 5, 85, 0, 0, 1053, 1054, 5, 82, 0, 0, 1054, 1055, 5, 82, 0, 0, 1055, 1056, 5, 69, 0, 0, 1056, 1057, 5, 78, 0, 0, 1057, 1058, 5, 84, 0, 0, 1058, 112, 1, 0, 0, 0, 1059, 1060, 5, 67, 0, 0, 1060, 1061, 5, 85, 0, 0, 1061, 1062, 5, 82, 0, 0, 1062, 1063, 5, 82, 0, 0, 1063, 1064, 5, 69, 0, 0, 1064, 1065, 5, 78, 0, 0, 1065, 1066, 5, 84, 0, 0, 1066, 1067, 5, 95, 0, 0, 1067, 1068, 5, 68, 0, 0, 1068, 1069, 5, 65, 0, 0, 1069, 1070, 5, 84, 0, 0, 1070, 1071, 5, 69, 0, 0, 1071, 114, 1, 0, 0, 0, 1072, 1073, 5, 67, 0, 0, 1073, 1074, 5, 85, 0, 0, 1074, 1075, 5, 82, 0, 0, 1075, 1076, 5, 82, 0, 0, 1076, 1077, 5, 69, 0, 0, 1077, 1078, 5, 78, 0, 0, 1078, 1079, 5, 84, 0, 0, 1079, 1080, 5, 95, 0, 0, 1080, 1081, 5, 84, 0, 0, 1081, 1082, 5, 73, 0, 0, 1082, 1083, 5, 77, 0, 0, 1083, 1084, 5, 69, 0, 0, 1084, 116, 1, 0, 0, 0, 1085, 1086, 5, 67, 0, 0, 1086, 1087, 5, 85, 0, 0, 1087, 1088, 5, 82, 0, 0, 1088, 1089, 5, 82, 0, 0, 1089, 1090, 5, 69, 0, 0, 1090, 1091, 5, 78, 0, 0, 1091, 1092, 5, 84, 0, 0, 1092, 1093, 5, 95, 0, 0, 1093, 1094, 5, 84, 0, 0, 1094, 1095, 5, 73, 0, 0, 1095, 1096, 5, 77, 0, 0, 1096, 1097, 5, 69, 0, 0, 1097, 1098, 5, 83, 0, 0, 1098, 1099, 5, 84, 0, 0, 1099, 1100, 5, 65, 0, 0, 1100, 1101, 5, 77, 0, 0, 1101, 1102, 5, 80, 0, 0, 1102, 118, 1, 0, 0, 0, 1103, 1104, 5, 67, 0, 0, 1104, 1105, 5, 85, 0, 0, 1105, 1106, 5, 82, 0, 0, 1106, 1107, 5, 82, 0, 0, 1107, 1108, 5, 69, 0, 0, 1108, 1109, 5, 78, 0, 0, 1109, 1110, 5, 84, 0, 0, 1110, 1111, 5, 95, 0, 0, 1111, 1112, 5, 85, 0, 0, 1112, 1113, 5, 83, 0, 0, 1113, 1114, 5, 69, 0, 0, 1114, 1115, 5, 82, 0, 0, 1115, 120, 1, 0, 0, 0, 1116, 1117, 5, 68, 0, 0, 1117, 1118, 5, 65, 0, 0, 1118, 1119, 5, 89, 0, 0, 1119, 122, 1, 0, 0, 0, 1120, 1121, 5, 68, 0, 0, 1121, 1122, 5, 65, 0, 0, 1122, 1123, 5, 89, 0, 0, 1123, 1124, 5, 83, 0, 0, 1124, 124, 1, 0, 0, 0, 1125, 1126, 5, 68, 0, 0, 1126, 1127, 5, 65, 0, 0, 1127, 1128, 5, 89, 0, 0, 1128, 1129, 5, 79, 0, 0, 1129, 1130, 5, 70, 0, 0, 1130, 1131, 5, 89, 0, 0, 1131, 1132, 5, 69, 0, 0, 1132, 1133, 5, 65, 0, 0, 1133, 1134, 5, 82, 0, 0, 1134, 126, 1, 0, 0, 0, 1135, 1136, 5, 68, 0, 0, 1136, 1137, 5, 65, 0, 0, 1137, 1138, 5, 84, 0, 0, 1138, 1139, 5, 65, 0, 0, 1139, 128, 1, 0, 0, 0, 1140, 1141, 5, 68, 0, 0, 1141, 1142, 5, 65, 0, 0, 1142, 1143, 5, 84, 0, 0, 1143, 1144, 5, 65, 0, 0, 1144, 1145, 5, 66, 0, 0, 1145, 1146, 5, 65, 0, 0, 1146, 1147, 5, 83, 0, 0, 1147, 1148, 5, 69, 0, 0, 1148, 130, 1, 0, 0, 0, 1149, 1150, 5, 68, 0, 0, 1150, 1151, 5, 65, 0, 0, 1151, 1152, 5, 84, 0, 0, 1152, 1153, 5, 65, 0, 0, 1153, 1154, 5, 66, 0, 0, 1154, 1155, 5, 65, 0, 0, 1155, 1156, 5, 83, 0, 0, 1156, 1157, 5, 69, 0, 0, 1157, 1158, 5, 83, 0, 0, 1158, 132, 1, 0, 0, 0, 1159, 1160, 5, 68, 0, 0, 1160, 1161, 5, 65, 0, 0, 1161, 1162, 5, 84, 0, 0, 1162, 1163, 5, 69, 0, 0, 1163, 1164, 5, 65, 0, 0, 1164, 1165, 5, 68, 0, 0, 1165, 1166, 5, 68, 0, 0, 1166, 134, 1, 0, 0, 0, 1167, 1168, 5, 68, 0, 0, 1168, 1169, 5, 65, 0, 0, 1169, 1170, 5, 84, 0, 0, 1170, 1171, 5, 69, 0, 0, 1171, 1172, 5, 68, 0, 0, 1172, 1173, 5, 73, 0, 0, 1173, 1174, 5, 70, 0, 0, 1174, 1175, 5, 70, 0, 0, 1175, 136, 1, 0, 0, 0, 1176, 1177, 5, 68, 0, 0, 1177, 1178, 5, 66, 0, 0, 1178, 1179, 5, 80, 0, 0, 1179, 1180, 5, 82, 0, 0, 1180, 1181, 5, 79, 0, 0, 1181, 1182, 5, 80, 0, 0, 1182, 1183, 5, 69, 0, 0, 1183, 1184, 5, 82, 0, 0, 1184, 1185, 5, 84, 0, 0, 1185, 1186, 5, 73, 0, 0, 1186, 1187, 5, 69, 0, 0, 1187, 1188, 5, 83, 0, 0, 1188, 138, 1, 0, 0, 0, 1189, 1190, 5, 68, 0, 0, 1190, 1191, 5, 69, 0, 0, 1191, 1192, 5, 70, 0, 0, 1192, 1193, 5, 65, 0, 0, 1193, 1194, 5, 85, 0, 0, 1194, 1195, 5, 76, 0, 0, 1195, 1196, 5, 84, 0, 0, 1196, 140, 1, 0, 0, 0, 1197, 1198, 5, 68, 0, 0, 1198, 1199, 5, 69, 0, 0, 1199, 1200, 5, 70, 0, 0, 1200, 1201, 5, 73, 0, 0, 1201, 1202, 5, 78, 0, 0, 1202, 1203, 5, 69, 0, 0, 1203, 1204, 5, 68, 0, 0, 1204, 142, 1, 0, 0, 0, 1205, 1206, 5, 68, 0, 0, 1206, 1207, 5, 69, 0, 0, 1207, 1208, 5, 76, 0, 0, 1208, 1209, 5, 69, 0, 0, 1209, 1210, 5, 84, 0, 0, 1210, 1211, 5, 69, 0, 0, 1211, 144, 1, 0, 0, 0, 1212, 1213, 5, 68, 0, 0, 1213, 1214, 5, 69, 0, 0, 1214, 1215, 5, 76, 0, 0, 1215, 1216, 5, 73, 0, 0, 1216, 1217, 5, 77, 0, 0, 1217, 1218, 5, 73, 0, 0, 1218, 1219, 5, 84, 0, 0, 1219, 1220, 5, 69, 0, 0, 1220, 1221, 5, 68, 0, 0, 1221, 146, 1, 0, 0, 0, 1222, 1223, 5, 68, 0, 0, 1223, 1224, 5, 69, 0, 0, 1224, 1225, 5, 83, 0, 0, 1225, 1226, 5, 67, 0, 0, 1226, 148, 1, 0, 0, 0, 1227, 1228, 5, 68, 0, 0, 1228, 1229, 5, 69, 0, 0, 1229, 1230, 5, 83, 0, 0, 1230, 1231, 5, 67, 0, 0, 1231, 1232, 5, 82, 0, 0, 1232, 1233, 5, 73, 0, 0, 1233, 1234, 5, 66, 0, 0, 1234, 1235, 5, 69, 0, 0, 1235, 150, 1, 0, 0, 0, 1236, 1237, 5, 68, 0, 0, 1237, 1238, 5, 70, 0, 0, 1238, 1239, 5, 83, 0, 0, 1239, 152, 1, 0, 0, 0, 1240, 1241, 5, 68, 0, 0, 1241, 1242, 5, 73, 0, 0, 1242, 1243, 5, 82, 0, 0, 1243, 1244, 5, 69, 0, 0, 1244, 1245, 5, 67, 0, 0, 1245, 1246, 5, 84, 0, 0, 1246, 1247, 5, 79, 0, 0, 1247, 1248, 5, 82, 0, 0, 1248, 1249, 5, 73, 0, 0, 1249, 1250, 5, 69, 0, 0, 1250, 1251, 5, 83, 0, 0, 1251, 154, 1, 0, 0, 0, 1252, 1253, 5, 68, 0, 0, 1253, 1254, 5, 73, 0, 0, 1254, 1255, 5, 82, 0, 0, 1255, 1256, 5, 69, 0, 0, 1256, 1257, 5, 67, 0, 0, 1257, 1258, 5, 84, 0, 0, 1258, 1259, 5, 79, 0, 0, 1259, 1260, 5, 82, 0, 0, 1260, 1261, 5, 89, 0, 0, 1261, 156, 1, 0, 0, 0, 1262, 1263, 5, 68, 0, 0, 1263, 1264, 5, 73, 0, 0, 1264, 1265, 5, 83, 0, 0, 1265, 1266, 5, 84, 0, 0, 1266, 1267, 5, 73, 0, 0, 1267, 1268, 5, 78, 0, 0, 1268, 1269, 5, 67, 0, 0, 1269, 1270, 5, 84, 0, 0, 1270, 158, 1, 0, 0, 0, 1271, 1272, 5, 68, 0, 0, 1272, 1273, 5, 73, 0, 0, 1273, 1274, 5, 83, 0, 0, 1274, 1275, 5, 84, 0, 0, 1275, 1276, 5, 82, 0, 0, 1276, 1277, 5, 73, 0, 0, 1277, 1278, 5, 66, 0, 0, 1278, 1279, 5, 85, 0, 0, 1279, 1280, 5, 84, 0, 0, 1280, 1281, 5, 69, 0, 0, 1281, 160, 1, 0, 0, 0, 1282, 1283, 5, 68, 0, 0, 1283, 1284, 5, 73, 0, 0, 1284, 1285, 5, 86, 0, 0, 1285, 162, 1, 0, 0, 0, 1286, 1287, 5, 68, 0, 0, 1287, 1288, 5, 82, 0, 0, 1288, 1289, 5, 79, 0, 0, 1289, 1290, 5, 80, 0, 0, 1290, 164, 1, 0, 0, 0, 1291, 1292, 5, 69, 0, 0, 1292, 1293, 5, 76, 0, 0, 1293, 1294, 5, 83, 0, 0, 1294, 1295, 5, 69, 0, 0, 1295, 166, 1, 0, 0, 0, 1296, 1297, 5, 69, 0, 0, 1297, 1298, 5, 78, 0, 0, 1298, 1299, 5, 68, 0, 0, 1299, 168, 1, 0, 0, 0, 1300, 1301, 5, 69, 0, 0, 1301, 1302, 5, 83, 0, 0, 1302, 1303, 5, 67, 0, 0, 1303, 1304, 5, 65, 0, 0, 1304, 1305, 5, 80, 0, 0, 1305, 1306, 5, 69, 0, 0, 1306, 170, 1, 0, 0, 0, 1307, 1308, 5, 69, 0, 0, 1308, 1309, 5, 83, 0, 0, 1309, 1310, 5, 67, 0, 0, 1310, 1311, 5, 65, 0, 0, 1311, 1312, 5, 80, 0, 0, 1312, 1313, 5, 69, 0, 0, 1313, 1314, 5, 68, 0, 0, 1314, 172, 1, 0, 0, 0, 1315, 1316, 5, 69, 0, 0, 1316, 1317, 5, 88, 0, 0, 1317, 1318, 5, 67, 0, 0, 1318, 1319, 5, 69, 0, 0, 1319, 1320, 5, 80, 0, 0, 1320, 1321, 5, 84, 0, 0, 1321, 174, 1, 0, 0, 0, 1322, 1323, 5, 69, 0, 0, 1323, 1324, 5, 88, 0, 0, 1324, 1325, 5, 67, 0, 0, 1325, 1326, 5, 72, 0, 0, 1326, 1327, 5, 65, 0, 0, 1327, 1328, 5, 78, 0, 0, 1328, 1329, 5, 71, 0, 0, 1329, 1330, 5, 69, 0, 0, 1330, 176, 1, 0, 0, 0, 1331, 1332, 5, 69, 0, 0, 1332, 1333, 5, 88, 0, 0, 1333, 1334, 5, 67, 0, 0, 1334, 1335, 5, 76, 0, 0, 1335, 1336, 5, 85, 0, 0, 1336, 1337, 5, 68, 0, 0, 1337, 1338, 5, 69, 0, 0, 1338, 178, 1, 0, 0, 0, 1339, 1340, 5, 69, 0, 0, 1340, 1341, 5, 88, 0, 0, 1341, 1342, 5, 73, 0, 0, 1342, 1343, 5, 83, 0, 0, 1343, 1344, 5, 84, 0, 0, 1344, 1345, 5, 83, 0, 0, 1345, 180, 1, 0, 0, 0, 1346, 1347, 5, 69, 0, 0, 1347, 1348, 5, 88, 0, 0, 1348, 1349, 5, 80, 0, 0, 1349, 1350, 5, 76, 0, 0, 1350, 1351, 5, 65, 0, 0, 1351, 1352, 5, 73, 0, 0, 1352, 1353, 5, 78, 0, 0, 1353, 182, 1, 0, 0, 0, 1354, 1355, 5, 69, 0, 0, 1355, 1356, 5, 88, 0, 0, 1356, 1357, 5, 80, 0, 0, 1357, 1358, 5, 79, 0, 0, 1358, 1359, 5, 82, 0, 0, 1359, 1360, 5, 84, 0, 0, 1360, 184, 1, 0, 0, 0, 1361, 1362, 5, 69, 0, 0, 1362, 1363, 5, 88, 0, 0, 1363, 1364, 5, 84, 0, 0, 1364, 1365, 5, 69, 0, 0, 1365, 1366, 5, 78, 0, 0, 1366, 1367, 5, 68, 0, 0, 1367, 1368, 5, 69, 0, 0, 1368, 1369, 5, 68, 0, 0, 1369, 186, 1, 0, 0, 0, 1370, 1371, 5, 69, 0, 0, 1371, 1372, 5, 88, 0, 0, 1372, 1373, 5, 84, 0, 0, 1373, 1374, 5, 69, 0, 0, 1374, 1375, 5, 82, 0, 0, 1375, 1376, 5, 78, 0, 0, 1376, 1377, 5, 65, 0, 0, 1377, 1378, 5, 76, 0, 0, 1378, 188, 1, 0, 0, 0, 1379, 1380, 5, 69, 0, 0, 1380, 1381, 5, 88, 0, 0, 1381, 1382, 5, 84, 0, 0, 1382, 1383, 5, 82, 0, 0, 1383, 1384, 5, 65, 0, 0, 1384, 1385, 5, 67, 0, 0, 1385, 1386, 5, 84, 0, 0, 1386, 190, 1, 0, 0, 0, 1387, 1388, 5, 70, 0, 0, 1388, 1389, 5, 65, 0, 0, 1389, 1390, 5, 76, 0, 0, 1390, 1391, 5, 83, 0, 0, 1391, 1392, 5, 69, 0, 0, 1392, 192, 1, 0, 0, 0, 1393, 1394, 5, 70, 0, 0, 1394, 1395, 5, 69, 0, 0, 1395, 1396, 5, 84, 0, 0, 1396, 1397, 5, 67, 0, 0, 1397, 1398, 5, 72, 0, 0, 1398, 194, 1, 0, 0, 0, 1399, 1400, 5, 70, 0, 0, 1400, 1401, 5, 73, 0, 0, 1401, 1402, 5, 69, 0, 0, 1402, 1403, 5, 76, 0, 0, 1403, 1404, 5, 68, 0, 0, 1404, 1405, 5, 83, 0, 0, 1405, 196, 1, 0, 0, 0, 1406, 1407, 5, 70, 0, 0, 1407, 1408, 5, 73, 0, 0, 1408, 1409, 5, 76, 0, 0, 1409, 1410, 5, 84, 0, 0, 1410, 1411, 5, 69, 0, 0, 1411, 1412, 5, 82, 0, 0, 1412, 198, 1, 0, 0, 0, 1413, 1414, 5, 70, 0, 0, 1414, 1415, 5, 73, 0, 0, 1415, 1416, 5, 76, 0, 0, 1416, 1417, 5, 69, 0, 0, 1417, 1418, 5, 70, 0, 0, 1418, 1419, 5, 79, 0, 0, 1419, 1420, 5, 82, 0, 0, 1420, 1421, 5, 77, 0, 0, 1421, 1422, 5, 65, 0, 0, 1422, 1423, 5, 84, 0, 0, 1423, 200, 1, 0, 0, 0, 1424, 1425, 5, 70, 0, 0, 1425, 1426, 5, 73, 0, 0, 1426, 1427, 5, 82, 0, 0, 1427, 1428, 5, 83, 0, 0, 1428, 1429, 5, 84, 0, 0, 1429, 202, 1, 0, 0, 0, 1430, 1431, 5, 70, 0, 0, 1431, 1432, 5, 79, 0, 0, 1432, 1433, 5, 76, 0, 0, 1433, 1434, 5, 76, 0, 0, 1434, 1435, 5, 79, 0, 0, 1435, 1436, 5, 87, 0, 0, 1436, 1437, 5, 73, 0, 0, 1437, 1438, 5, 78, 0, 0, 1438, 1439, 5, 71, 0, 0, 1439, 204, 1, 0, 0, 0, 1440, 1441, 5, 70, 0, 0, 1441, 1442, 5, 79, 0, 0, 1442, 1443, 5, 82, 0, 0, 1443, 206, 1, 0, 0, 0, 1444, 1445, 5, 70, 0, 0, 1445, 1446, 5, 79, 0, 0, 1446, 1447, 5, 82, 0, 0, 1447, 1448, 5, 69, 0, 0, 1448, 1449, 5, 73, 0, 0, 1449, 1450, 5, 71, 0, 0, 1450, 1451, 5, 78, 0, 0, 1451, 208, 1, 0, 0, 0, 1452, 1453, 5, 70, 0, 0, 1453, 1454, 5, 79, 0, 0, 1454, 1455, 5, 82, 0, 0, 1455, 1456, 5, 77, 0, 0, 1456, 1457, 5, 65, 0, 0, 1457, 1458, 5, 84, 0, 0, 1458, 210, 1, 0, 0, 0, 1459, 1460, 5, 70, 0, 0, 1460, 1461, 5, 79, 0, 0, 1461, 1462, 5, 82, 0, 0, 1462, 1463, 5, 77, 0, 0, 1463, 1464, 5, 65, 0, 0, 1464, 1465, 5, 84, 0, 0, 1465, 1466, 5, 84, 0, 0, 1466, 1467, 5, 69, 0, 0, 1467, 1468, 5, 68, 0, 0, 1468, 212, 1, 0, 0, 0, 1469, 1470, 5, 70, 0, 0, 1470, 1471, 5, 82, 0, 0, 1471, 1472, 5, 79, 0, 0, 1472, 1473, 5, 77, 0, 0, 1473, 214, 1, 0, 0, 0, 1474, 1475, 5, 70, 0, 0, 1475, 1476, 5, 85, 0, 0, 1476, 1477, 5, 76, 0, 0, 1477, 1478, 5, 76, 0, 0, 1478, 216, 1, 0, 0, 0, 1479, 1480, 5, 70, 0, 0, 1480, 1481, 5, 85, 0, 0, 1481, 1482, 5, 78, 0, 0, 1482, 1483, 5, 67, 0, 0, 1483, 1484, 5, 84, 0, 0, 1484, 1485, 5, 73, 0, 0, 1485, 1486, 5, 79, 0, 0, 1486, 1487, 5, 78, 0, 0, 1487, 218, 1, 0, 0, 0, 1488, 1489, 5, 70, 0, 0, 1489, 1490, 5, 85, 0, 0, 1490, 1491, 5, 78, 0, 0, 1491, 1492, 5, 67, 0, 0, 1492, 1493, 5, 84, 0, 0, 1493, 1494, 5, 73, 0, 0, 1494, 1495, 5, 79, 0, 0, 1495, 1496, 5, 78, 0, 0, 1496, 1497, 5, 83, 0, 0, 1497, 220, 1, 0, 0, 0, 1498, 1499, 5, 71, 0, 0, 1499, 1500, 5, 69, 0, 0, 1500, 1501, 5, 78, 0, 0, 1501, 1502, 5, 69, 0, 0, 1502, 1503, 5, 82, 0, 0, 1503, 1504, 5, 65, 0, 0, 1504, 1505, 5, 84, 0, 0, 1505, 1506, 5, 69, 0, 0, 1506, 1507, 5, 68, 0, 0, 1507, 222, 1, 0, 0, 0, 1508, 1509, 5, 71, 0, 0, 1509, 1510, 5, 76, 0, 0, 1510, 1511, 5, 79, 0, 0, 1511, 1512, 5, 66, 0, 0, 1512, 1513, 5, 65, 0, 0, 1513, 1514, 5, 76, 0, 0, 1514, 224, 1, 0, 0, 0, 1515, 1516, 5, 71, 0, 0, 1516, 1517, 5, 82, 0, 0, 1517, 1518, 5, 65, 0, 0, 1518, 1519, 5, 78, 0, 0, 1519, 1520, 5, 84, 0, 0, 1520, 226, 1, 0, 0, 0, 1521, 1522, 5, 71, 0, 0, 1522, 1523, 5, 82, 0, 0, 1523, 1524, 5, 79, 0, 0, 1524, 1525, 5, 85, 0, 0, 1525, 1526, 5, 80, 0, 0, 1526, 228, 1, 0, 0, 0, 1527, 1528, 5, 71, 0, 0, 1528, 1529, 5, 82, 0, 0, 1529, 1530, 5, 79, 0, 0, 1530, 1531, 5, 85, 0, 0, 1531, 1532, 5, 80, 0, 0, 1532, 1533, 5, 73, 0, 0, 1533, 1534, 5, 78, 0, 0, 1534, 1535, 5, 71, 0, 0, 1535, 230, 1, 0, 0, 0, 1536, 1537, 5, 72, 0, 0, 1537, 1538, 5, 65, 0, 0, 1538, 1539, 5, 86, 0, 0, 1539, 1540, 5, 73, 0, 0, 1540, 1541, 5, 78, 0, 0, 1541, 1542, 5, 71, 0, 0, 1542, 232, 1, 0, 0, 0, 1543, 1544, 5, 72, 0, 0, 1544, 1545, 5, 79, 0, 0, 1545, 1546, 5, 85, 0, 0, 1546, 1547, 5, 82, 0, 0, 1547, 234, 1, 0, 0, 0, 1548, 1549, 5, 72, 0, 0, 1549, 1550, 5, 79, 0, 0, 1550, 1551, 5, 85, 0, 0, 1551, 1552, 5, 82, 0, 0, 1552, 1553, 5, 83, 0, 0, 1553, 236, 1, 0, 0, 0, 1554, 1555, 5, 73, 0, 0, 1555, 1556, 5, 70, 0, 0, 1556, 238, 1, 0, 0, 0, 1557, 1558, 5, 73, 0, 0, 1558, 1559, 5, 71, 0, 0, 1559, 1560, 5, 78, 0, 0, 1560, 1561, 5, 79, 0, 0, 1561, 1562, 5, 82, 0, 0, 1562, 1563, 5, 69, 0, 0, 1563, 240, 1, 0, 0, 0, 1564, 1565, 5, 73, 0, 0, 1565, 1566, 5, 77, 0, 0, 1566, 1567, 5, 80, 0, 0, 1567, 1568, 5, 79, 0, 0, 1568, 1569, 5, 82, 0, 0, 1569, 1570, 5, 84, 0, 0, 1570, 242, 1, 0, 0, 0, 1571, 1572, 5, 73, 0, 0, 1572, 1573, 5, 78, 0, 0, 1573, 244, 1, 0, 0, 0, 1574, 1575, 5, 73, 0, 0, 1575, 1576, 5, 78, 0, 0, 1576, 1577, 5, 67, 0, 0, 1577, 1578, 5, 76, 0, 0, 1578, 1579, 5, 85, 0, 0, 1579, 1580, 5, 68, 0, 0, 1580, 1581, 5, 69, 0, 0, 1581, 246, 1, 0, 0, 0, 1582, 1583, 5, 73, 0, 0, 1583, 1584, 5, 78, 0, 0, 1584, 1585, 5, 68, 0, 0, 1585, 1586, 5, 69, 0, 0, 1586, 1587, 5, 88, 0, 0, 1587, 248, 1, 0, 0, 0, 1588, 1589, 5, 73, 0, 0, 1589, 1590, 5, 78, 0, 0, 1590, 1591, 5, 68, 0, 0, 1591, 1592, 5, 69, 0, 0, 1592, 1593, 5, 88, 0, 0, 1593, 1594, 5, 69, 0, 0, 1594, 1595, 5, 83, 0, 0, 1595, 250, 1, 0, 0, 0, 1596, 1597, 5, 73, 0, 0, 1597, 1598, 5, 78, 0, 0, 1598, 1599, 5, 78, 0, 0, 1599, 1600, 5, 69, 0, 0, 1600, 1601, 5, 82, 0, 0, 1601, 252, 1, 0, 0, 0, 1602, 1603, 5, 73, 0, 0, 1603, 1604, 5, 78, 0, 0, 1604, 1605, 5, 80, 0, 0, 1605, 1606, 5, 65, 0, 0, 1606, 1607, 5, 84, 0, 0, 1607, 1608, 5, 72, 0, 0, 1608, 254, 1, 0, 0, 0, 1609, 1610, 5, 73, 0, 0, 1610, 1611, 5, 78, 0, 0, 1611, 1612, 5, 80, 0, 0, 1612, 1613, 5, 85, 0, 0, 1613, 1614, 5, 84, 0, 0, 1614, 1615, 5, 70, 0, 0, 1615, 1616, 5, 79, 0, 0, 1616, 1617, 5, 82, 0, 0, 1617, 1618, 5, 77, 0, 0, 1618, 1619, 5, 65, 0, 0, 1619, 1620, 5, 84, 0, 0, 1620, 256, 1, 0, 0, 0, 1621, 1622, 5, 73, 0, 0, 1622, 1623, 5, 78, 0, 0, 1623, 1624, 5, 83, 0, 0, 1624, 1625, 5, 69, 0, 0, 1625, 1626, 5, 82, 0, 0, 1626, 1627, 5, 84, 0, 0, 1627, 258, 1, 0, 0, 0, 1628, 1629, 5, 73, 0, 0, 1629, 1630, 5, 78, 0, 0, 1630, 1631, 5, 84, 0, 0, 1631, 1632, 5, 69, 0, 0, 1632, 1633, 5, 82, 0, 0, 1633, 1634, 5, 83, 0, 0, 1634, 1635, 5, 69, 0, 0, 1635, 1636, 5, 67, 0, 0, 1636, 1637, 5, 84, 0, 0, 1637, 260, 1, 0, 0, 0, 1638, 1639, 5, 73, 0, 0, 1639, 1640, 5, 78, 0, 0, 1640, 1641, 5, 84, 0, 0, 1641, 1642, 5, 69, 0, 0, 1642, 1643, 5, 82, 0, 0, 1643, 1644, 5, 86, 0, 0, 1644, 1645, 5, 65, 0, 0, 1645, 1646, 5, 76, 0, 0, 1646, 262, 1, 0, 0, 0, 1647, 1648, 5, 73, 0, 0, 1648, 1649, 5, 78, 0, 0, 1649, 1650, 5, 84, 0, 0, 1650, 1651, 5, 79, 0, 0, 1651, 264, 1, 0, 0, 0, 1652, 1653, 5, 73, 0, 0, 1653, 1654, 5, 83, 0, 0, 1654, 266, 1, 0, 0, 0, 1655, 1656, 5, 73, 0, 0, 1656, 1657, 5, 84, 0, 0, 1657, 1658, 5, 69, 0, 0, 1658, 1659, 5, 77, 0, 0, 1659, 1660, 5, 83, 0, 0, 1660, 268, 1, 0, 0, 0, 1661, 1662, 5, 74, 0, 0, 1662, 1663, 5, 79, 0, 0, 1663, 1664, 5, 73, 0, 0, 1664, 1665, 5, 78, 0, 0, 1665, 270, 1, 0, 0, 0, 1666, 1667, 5, 75, 0, 0, 1667, 1668, 5, 69, 0, 0, 1668, 1669, 5, 89, 0, 0, 1669, 1670, 5, 83, 0, 0, 1670, 272, 1, 0, 0, 0, 1671, 1672, 5, 76, 0, 0, 1672, 1673, 5, 65, 0, 0, 1673, 1674, 5, 83, 0, 0, 1674, 1675, 5, 84, 0, 0, 1675, 274, 1, 0, 0, 0, 1676, 1677, 5, 76, 0, 0, 1677, 1678, 5, 65, 0, 0, 1678, 1679, 5, 84, 0, 0, 1679, 1680, 5, 69, 0, 0, 1680, 1681, 5, 82, 0, 0, 1681, 1682, 5, 65, 0, 0, 1682, 1683, 5, 76, 0, 0, 1683, 276, 1, 0, 0, 0, 1684, 1685, 5, 76, 0, 0, 1685, 1686, 5, 65, 0, 0, 1686, 1687, 5, 90, 0, 0, 1687, 1688, 5, 89, 0, 0, 1688, 278, 1, 0, 0, 0, 1689, 1690, 5, 76, 0, 0, 1690, 1691, 5, 69, 0, 0, 1691, 1692, 5, 65, 0, 0, 1692, 1693, 5, 68, 0, 0, 1693, 1694, 5, 73, 0, 0, 1694, 1695, 5, 78, 0, 0, 1695, 1696, 5, 71, 0, 0, 1696, 280, 1, 0, 0, 0, 1697, 1698, 5, 76, 0, 0, 1698, 1699, 5, 69, 0, 0, 1699, 1700, 5, 70, 0, 0, 1700, 1701, 5, 84, 0, 0, 1701, 282, 1, 0, 0, 0, 1702, 1703, 5, 76, 0, 0, 1703, 1704, 5, 73, 0, 0, 1704, 1705, 5, 75, 0, 0, 1705, 1706, 5, 69, 0, 0, 1706, 284, 1, 0, 0, 0, 1707, 1708, 5, 73, 0, 0, 1708, 1709, 5, 76, 0, 0, 1709, 1710, 5, 73, 0, 0, 1710, 1711, 5, 75, 0, 0, 1711, 1712, 5, 69, 0, 0, 1712, 286, 1, 0, 0, 0, 1713, 1714, 5, 76, 0, 0, 1714, 1715, 5, 73, 0, 0, 1715, 1716, 5, 77, 0, 0, 1716, 1717, 5, 73, 0, 0, 1717, 1718, 5, 84, 0, 0, 1718, 288, 1, 0, 0, 0, 1719, 1720, 5, 76, 0, 0, 1720, 1721, 5, 73, 0, 0, 1721, 1722, 5, 78, 0, 0, 1722, 1723, 5, 69, 0, 0, 1723, 1724, 5, 83, 0, 0, 1724, 290, 1, 0, 0, 0, 1725, 1726, 5, 76, 0, 0, 1726, 1727, 5, 73, 0, 0, 1727, 1728, 5, 83, 0, 0, 1728, 1729, 5, 84, 0, 0, 1729, 292, 1, 0, 0, 0, 1730, 1731, 5, 76, 0, 0, 1731, 1732, 5, 79, 0, 0, 1732, 1733, 5, 65, 0, 0, 1733, 1734, 5, 68, 0, 0, 1734, 294, 1, 0, 0, 0, 1735, 1736, 5, 76, 0, 0, 1736, 1737, 5, 79, 0, 0, 1737, 1738, 5, 67, 0, 0, 1738, 1739, 5, 65, 0, 0, 1739, 1740, 5, 76, 0, 0, 1740, 296, 1, 0, 0, 0, 1741, 1742, 5, 76, 0, 0, 1742, 1743, 5, 79, 0, 0, 1743, 1744, 5, 67, 0, 0, 1744, 1745, 5, 65, 0, 0, 1745, 1746, 5, 84, 0, 0, 1746, 1747, 5, 73, 0, 0, 1747, 1748, 5, 79, 0, 0, 1748, 1749, 5, 78, 0, 0, 1749, 298, 1, 0, 0, 0, 1750, 1751, 5, 76, 0, 0, 1751, 1752, 5, 79, 0, 0, 1752, 1753, 5, 67, 0, 0, 1753, 1754, 5, 75, 0, 0, 1754, 300, 1, 0, 0, 0, 1755, 1756, 5, 76, 0, 0, 1756, 1757, 5, 79, 0, 0, 1757, 1758, 5, 67, 0, 0, 1758, 1759, 5, 75, 0, 0, 1759, 1760, 5, 83, 0, 0, 1760, 302, 1, 0, 0, 0, 1761, 1762, 5, 76, 0, 0, 1762, 1763, 5, 79, 0, 0, 1763, 1764, 5, 71, 0, 0, 1764, 1765, 5, 73, 0, 0, 1765, 1766, 5, 67, 0, 0, 1766, 1767, 5, 65, 0, 0, 1767, 1768, 5, 76, 0, 0, 1768, 304, 1, 0, 0, 0, 1769, 1770, 5, 77, 0, 0, 1770, 1771, 5, 65, 0, 0, 1771, 1772, 5, 67, 0, 0, 1772, 1773, 5, 82, 0, 0, 1773, 1774, 5, 79, 0, 0, 1774, 306, 1, 0, 0, 0, 1775, 1776, 5, 77, 0, 0, 1776, 1777, 5, 65, 0, 0, 1777, 1778, 5, 80, 0, 0, 1778, 308, 1, 0, 0, 0, 1779, 1780, 5, 77, 0, 0, 1780, 1781, 5, 65, 0, 0, 1781, 1782, 5, 84, 0, 0, 1782, 1783, 5, 67, 0, 0, 1783, 1784, 5, 72, 0, 0, 1784, 1785, 5, 69, 0, 0, 1785, 1786, 5, 68, 0, 0, 1786, 310, 1, 0, 0, 0, 1787, 1788, 5, 77, 0, 0, 1788, 1789, 5, 69, 0, 0, 1789, 1790, 5, 82, 0, 0, 1790, 1791, 5, 71, 0, 0, 1791, 1792, 5, 69, 0, 0, 1792, 312, 1, 0, 0, 0, 1793, 1794, 5, 77, 0, 0, 1794, 1795, 5, 73, 0, 0, 1795, 1796, 5, 67, 0, 0, 1796, 1797, 5, 82, 0, 0, 1797, 1798, 5, 79, 0, 0, 1798, 1799, 5, 83, 0, 0, 1799, 1800, 5, 69, 0, 0, 1800, 1801, 5, 67, 0, 0, 1801, 1802, 5, 79, 0, 0, 1802, 1803, 5, 78, 0, 0, 1803, 1804, 5, 68, 0, 0, 1804, 314, 1, 0, 0, 0, 1805, 1806, 5, 77, 0, 0, 1806, 1807, 5, 73, 0, 0, 1807, 1808, 5, 67, 0, 0, 1808, 1809, 5, 82, 0, 0, 1809, 1810, 5, 79, 0, 0, 1810, 1811, 5, 83, 0, 0, 1811, 1812, 5, 69, 0, 0, 1812, 1813, 5, 67, 0, 0, 1813, 1814, 5, 79, 0, 0, 1814, 1815, 5, 78, 0, 0, 1815, 1816, 5, 68, 0, 0, 1816, 1817, 5, 83, 0, 0, 1817, 316, 1, 0, 0, 0, 1818, 1819, 5, 77, 0, 0, 1819, 1820, 5, 73, 0, 0, 1820, 1821, 5, 76, 0, 0, 1821, 1822, 5, 76, 0, 0, 1822, 1823, 5, 73, 0, 0, 1823, 1824, 5, 83, 0, 0, 1824, 1825, 5, 69, 0, 0, 1825, 1826, 5, 67, 0, 0, 1826, 1827, 5, 79, 0, 0, 1827, 1828, 5, 78, 0, 0, 1828, 1829, 5, 68, 0, 0, 1829, 318, 1, 0, 0, 0, 1830, 1831, 5, 77, 0, 0, 1831, 1832, 5, 73, 0, 0, 1832, 1833, 5, 76, 0, 0, 1833, 1834, 5, 76, 0, 0, 1834, 1835, 5, 73, 0, 0, 1835, 1836, 5, 83, 0, 0, 1836, 1837, 5, 69, 0, 0, 1837, 1838, 5, 67, 0, 0, 1838, 1839, 5, 79, 0, 0, 1839, 1840, 5, 78, 0, 0, 1840, 1841, 5, 68, 0, 0, 1841, 1842, 5, 83, 0, 0, 1842, 320, 1, 0, 0, 0, 1843, 1844, 5, 77, 0, 0, 1844, 1845, 5, 73, 0, 0, 1845, 1846, 5, 78, 0, 0, 1846, 1847, 5, 85, 0, 0, 1847, 1848, 5, 84, 0, 0, 1848, 1849, 5, 69, 0, 0, 1849, 322, 1, 0, 0, 0, 1850, 1851, 5, 77, 0, 0, 1851, 1852, 5, 73, 0, 0, 1852, 1853, 5, 78, 0, 0, 1853, 1854, 5, 85, 0, 0, 1854, 1855, 5, 84, 0, 0, 1855, 1856, 5, 69, 0, 0, 1856, 1857, 5, 83, 0, 0, 1857, 324, 1, 0, 0, 0, 1858, 1859, 5, 77, 0, 0, 1859, 1860, 5, 79, 0, 0, 1860, 1861, 5, 78, 0, 0, 1861, 1862, 5, 84, 0, 0, 1862, 1863, 5, 72, 0, 0, 1863, 326, 1, 0, 0, 0, 1864, 1865, 5, 77, 0, 0, 1865, 1866, 5, 79, 0, 0, 1866, 1867, 5, 78, 0, 0, 1867, 1868, 5, 84, 0, 0, 1868, 1869, 5, 72, 0, 0, 1869, 1870, 5, 83, 0, 0, 1870, 328, 1, 0, 0, 0, 1871, 1872, 5, 77, 0, 0, 1872, 1873, 5, 83, 0, 0, 1873, 1874, 5, 67, 0, 0, 1874, 1875, 5, 75, 0, 0, 1875, 330, 1, 0, 0, 0, 1876, 1877, 5, 78, 0, 0, 1877, 1878, 5, 65, 0, 0, 1878, 1879, 5, 77, 0, 0, 1879, 1880, 5, 69, 0, 0, 1880, 1881, 5, 83, 0, 0, 1881, 1882, 5, 80, 0, 0, 1882, 1883, 5, 65, 0, 0, 1883, 1884, 5, 67, 0, 0, 1884, 1885, 5, 69, 0, 0, 1885, 332, 1, 0, 0, 0, 1886, 1887, 5, 78, 0, 0, 1887, 1888, 5, 65, 0, 0, 1888, 1889, 5, 77, 0, 0, 1889, 1890, 5, 69, 0, 0, 1890, 1891, 5, 83, 0, 0, 1891, 1892, 5, 80, 0, 0, 1892, 1893, 5, 65, 0, 0, 1893, 1894, 5, 67, 0, 0, 1894, 1895, 5, 69, 0, 0, 1895, 1896, 5, 83, 0, 0, 1896, 334, 1, 0, 0, 0, 1897, 1898, 5, 78, 0, 0, 1898, 1899, 5, 65, 0, 0, 1899, 1900, 5, 78, 0, 0, 1900, 1901, 5, 79, 0, 0, 1901, 1902, 5, 83, 0, 0, 1902, 1903, 5, 69, 0, 0, 1903, 1904, 5, 67, 0, 0, 1904, 1905, 5, 79, 0, 0, 1905, 1906, 5, 78, 0, 0, 1906, 1907, 5, 68, 0, 0, 1907, 336, 1, 0, 0, 0, 1908, 1909, 5, 78, 0, 0, 1909, 1910, 5, 65, 0, 0, 1910, 1911, 5, 78, 0, 0, 1911, 1912, 5, 79, 0, 0, 1912, 1913, 5, 83, 0, 0, 1913, 1914, 5, 69, 0, 0, 1914, 1915, 5, 67, 0, 0, 1915, 1916, 5, 79, 0, 0, 1916, 1917, 5, 78, 0, 0, 1917, 1918, 5, 68, 0, 0, 1918, 1919, 5, 83, 0, 0, 1919, 338, 1, 0, 0, 0, 1920, 1921, 5, 78, 0, 0, 1921, 1922, 5, 65, 0, 0, 1922, 1923, 5, 84, 0, 0, 1923, 1924, 5, 85, 0, 0, 1924, 1925, 5, 82, 0, 0, 1925, 1926, 5, 65, 0, 0, 1926, 1927, 5, 76, 0, 0, 1927, 340, 1, 0, 0, 0, 1928, 1929, 5, 78, 0, 0, 1929, 1930, 5, 79, 0, 0, 1930, 342, 1, 0, 0, 0, 1931, 1932, 5, 78, 0, 0, 1932, 1933, 5, 79, 0, 0, 1933, 1936, 5, 84, 0, 0, 1934, 1936, 5, 33, 0, 0, 1935, 1931, 1, 0, 0, 0, 1935, 1934, 1, 0, 0, 0, 1936, 344, 1, 0, 0, 0, 1937, 1938, 5, 78, 0, 0, 1938, 1939, 5, 85, 0, 0, 1939, 1940, 5, 76, 0, 0, 1940, 1941, 5, 76, 0, 0, 1941, 346, 1, 0, 0, 0, 1942, 1943, 5, 78, 0, 0, 1943, 1944, 5, 85, 0, 0, 1944, 1945, 5, 76, 0, 0, 1945, 1946, 5, 76, 0, 0, 1946, 1947, 5, 83, 0, 0, 1947, 348, 1, 0, 0, 0, 1948, 1949, 5, 79, 0, 0, 1949, 1950, 5, 70, 0, 0, 1950, 350, 1, 0, 0, 0, 1951, 1952, 5, 79, 0, 0, 1952, 1953, 5, 70, 0, 0, 1953, 1954, 5, 70, 0, 0, 1954, 1955, 5, 83, 0, 0, 1955, 1956, 5, 69, 0, 0, 1956, 1957, 5, 84, 0, 0, 1957, 352, 1, 0, 0, 0, 1958, 1959, 5, 79, 0, 0, 1959, 1960, 5, 78, 0, 0, 1960, 354, 1, 0, 0, 0, 1961, 1962, 5, 79, 0, 0, 1962, 1963, 5, 78, 0, 0, 1963, 1964, 5, 76, 0, 0, 1964, 1965, 5, 89, 0, 0, 1965, 356, 1, 0, 0, 0, 1966, 1967, 5, 79, 0, 0, 1967, 1968, 5, 80, 0, 0, 1968, 1969, 5, 84, 0, 0, 1969, 1970, 5, 73, 0, 0, 1970, 1971, 5, 79, 0, 0, 1971, 1972, 5, 78, 0, 0, 1972, 358, 1, 0, 0, 0, 1973, 1974, 5, 79, 0, 0, 1974, 1975, 5, 80, 0, 0, 1975, 1976, 5, 84, 0, 0, 1976, 1977, 5, 73, 0, 0, 1977, 1978, 5, 79, 0, 0, 1978, 1979, 5, 78, 0, 0, 1979, 1980, 5, 83, 0, 0, 1980, 360, 1, 0, 0, 0, 1981, 1982, 5, 79, 0, 0, 1982, 1983, 5, 82, 0, 0, 1983, 362, 1, 0, 0, 0, 1984, 1985, 5, 79, 0, 0, 1985, 1986, 5, 82, 0, 0, 1986, 1987, 5, 68, 0, 0, 1987, 1988, 5, 69, 0, 0, 1988, 1989, 5, 82, 0, 0, 1989, 364, 1, 0, 0, 0, 1990, 1991, 5, 79, 0, 0, 1991, 1992, 5, 85, 0, 0, 1992, 1993, 5, 84, 0, 0, 1993, 366, 1, 0, 0, 0, 1994, 1995, 5, 79, 0, 0, 1995, 1996, 5, 85, 0, 0, 1996, 1997, 5, 84, 0, 0, 1997, 1998, 5, 69, 0, 0, 1998, 1999, 5, 82, 0, 0, 1999, 368, 1, 0, 0, 0, 2000, 2001, 5, 79, 0, 0, 2001, 2002, 5, 85, 0, 0, 2002, 2003, 5, 84, 0, 0, 2003, 2004, 5, 80, 0, 0, 2004, 2005, 5, 85, 0, 0, 2005, 2006, 5, 84, 0, 0, 2006, 2007, 5, 70, 0, 0, 2007, 2008, 5, 79, 0, 0, 2008, 2009, 5, 82, 0, 0, 2009, 2010, 5, 77, 0, 0, 2010, 2011, 5, 65, 0, 0, 2011, 2012, 5, 84, 0, 0, 2012, 370, 1, 0, 0, 0, 2013, 2014, 5, 79, 0, 0, 2014, 2015, 5, 86, 0, 0, 2015, 2016, 5, 69, 0, 0, 2016, 2017, 5, 82, 0, 0, 2017, 372, 1, 0, 0, 0, 2018, 2019, 5, 79, 0, 0, 2019, 2020, 5, 86, 0, 0, 2020, 2021, 5, 69, 0, 0, 2021, 2022, 5, 82, 0, 0, 2022, 2023, 5, 76, 0, 0, 2023, 2024, 5, 65, 0, 0, 2024, 2025, 5, 80, 0, 0, 2025, 2026, 5, 83, 0, 0, 2026, 374, 1, 0, 0, 0, 2027, 2028, 5, 79, 0, 0, 2028, 2029, 5, 86, 0, 0, 2029, 2030, 5, 69, 0, 0, 2030, 2031, 5, 82, 0, 0, 2031, 2032, 5, 76, 0, 0, 2032, 2033, 5, 65, 0, 0, 2033, 2034, 5, 89, 0, 0, 2034, 376, 1, 0, 0, 0, 2035, 2036, 5, 79, 0, 0, 2036, 2037, 5, 86, 0, 0, 2037, 2038, 5, 69, 0, 0, 2038, 2039, 5, 82, 0, 0, 2039, 2040, 5, 87, 0, 0, 2040, 2041, 5, 82, 0, 0, 2041, 2042, 5, 73, 0, 0, 2042, 2043, 5, 84, 0, 0, 2043, 2044, 5, 69, 0, 0, 2044, 378, 1, 0, 0, 0, 2045, 2046, 5, 80, 0, 0, 2046, 2047, 5, 65, 0, 0, 2047, 2048, 5, 82, 0, 0, 2048, 2049, 5, 84, 0, 0, 2049, 2050, 5, 73, 0, 0, 2050, 2051, 5, 84, 0, 0, 2051, 2052, 5, 73, 0, 0, 2052, 2053, 5, 79, 0, 0, 2053, 2054, 5, 78, 0, 0, 2054, 380, 1, 0, 0, 0, 2055, 2056, 5, 80, 0, 0, 2056, 2057, 5, 65, 0, 0, 2057, 2058, 5, 82, 0, 0, 2058, 2059, 5, 84, 0, 0, 2059, 2060, 5, 73, 0, 0, 2060, 2061, 5, 84, 0, 0, 2061, 2062, 5, 73, 0, 0, 2062, 2063, 5, 79, 0, 0, 2063, 2064, 5, 78, 0, 0, 2064, 2065, 5, 69, 0, 0, 2065, 2066, 5, 68, 0, 0, 2066, 382, 1, 0, 0, 0, 2067, 2068, 5, 80, 0, 0, 2068, 2069, 5, 65, 0, 0, 2069, 2070, 5, 82, 0, 0, 2070, 2071, 5, 84, 0, 0, 2071, 2072, 5, 73, 0, 0, 2072, 2073, 5, 84, 0, 0, 2073, 2074, 5, 73, 0, 0, 2074, 2075, 5, 79, 0, 0, 2075, 2076, 5, 78, 0, 0, 2076, 2077, 5, 83, 0, 0, 2077, 384, 1, 0, 0, 0, 2078, 2079, 5, 80, 0, 0, 2079, 2080, 5, 69, 0, 0, 2080, 2081, 5, 82, 0, 0, 2081, 2082, 5, 67, 0, 0, 2082, 2083, 5, 69, 0, 0, 2083, 2084, 5, 78, 0, 0, 2084, 2085, 5, 84, 0, 0, 2085, 2086, 5, 73, 0, 0, 2086, 2087, 5, 76, 0, 0, 2087, 2088, 5, 69, 0, 0, 2088, 2089, 5, 95, 0, 0, 2089, 2090, 5, 67, 0, 0, 2090, 2091, 5, 79, 0, 0, 2091, 2092, 5, 78, 0, 0, 2092, 2093, 5, 84, 0, 0, 2093, 386, 1, 0, 0, 0, 2094, 2095, 5, 80, 0, 0, 2095, 2096, 5, 69, 0, 0, 2096, 2097, 5, 82, 0, 0, 2097, 2098, 5, 67, 0, 0, 2098, 2099, 5, 69, 0, 0, 2099, 2100, 5, 78, 0, 0, 2100, 2101, 5, 84, 0, 0, 2101, 2102, 5, 73, 0, 0, 2102, 2103, 5, 76, 0, 0, 2103, 2104, 5, 69, 0, 0, 2104, 2105, 5, 95, 0, 0, 2105, 2106, 5, 68, 0, 0, 2106, 2107, 5, 73, 0, 0, 2107, 2108, 5, 83, 0, 0, 2108, 2109, 5, 67, 0, 0, 2109, 388, 1, 0, 0, 0, 2110, 2111, 5, 80, 0, 0, 2111, 2112, 5, 69, 0, 0, 2112, 2113, 5, 82, 0, 0, 2113, 2114, 5, 67, 0, 0, 2114, 2115, 5, 69, 0, 0, 2115, 2116, 5, 78, 0, 0, 2116, 2117, 5, 84, 0, 0, 2117, 390, 1, 0, 0, 0, 2118, 2119, 5, 80, 0, 0, 2119, 2120, 5, 73, 0, 0, 2120, 2121, 5, 86, 0, 0, 2121, 2122, 5, 79, 0, 0, 2122, 2123, 5, 84, 0, 0, 2123, 392, 1, 0, 0, 0, 2124, 2125, 5, 80, 0, 0, 2125, 2126, 5, 76, 0, 0, 2126, 2127, 5, 65, 0, 0, 2127, 2128, 5, 67, 0, 0, 2128, 2129, 5, 73, 0, 0, 2129, 2130, 5, 78, 0, 0, 2130, 2131, 5, 71, 0, 0, 2131, 394, 1, 0, 0, 0, 2132, 2133, 5, 80, 0, 0, 2133, 2134, 5, 79, 0, 0, 2134, 2135, 5, 83, 0, 0, 2135, 2136, 5, 73, 0, 0, 2136, 2137, 5, 84, 0, 0, 2137, 2138, 5, 73, 0, 0, 2138, 2139, 5, 79, 0, 0, 2139, 2140, 5, 78, 0, 0, 2140, 396, 1, 0, 0, 0, 2141, 2142, 5, 80, 0, 0, 2142, 2143, 5, 82, 0, 0, 2143, 2144, 5, 69, 0, 0, 2144, 2145, 5, 67, 0, 0, 2145, 2146, 5, 69, 0, 0, 2146, 2147, 5, 68, 0, 0, 2147, 2148, 5, 73, 0, 0, 2148, 2149, 5, 78, 0, 0, 2149, 2150, 5, 71, 0, 0, 2150, 398, 1, 0, 0, 0, 2151, 2152, 5, 80, 0, 0, 2152, 2153, 5, 82, 0, 0, 2153, 2154, 5, 73, 0, 0, 2154, 2155, 5, 77, 0, 0, 2155, 2156, 5, 65, 0, 0, 2156, 2157, 5, 82, 0, 0, 2157, 2158, 5, 89, 0, 0, 2158, 400, 1, 0, 0, 0, 2159, 2160, 5, 80, 0, 0, 2160, 2161, 5, 82, 0, 0, 2161, 2162, 5, 73, 0, 0, 2162, 2163, 5, 78, 0, 0, 2163, 2164, 5, 67, 0, 0, 2164, 2165, 5, 73, 0, 0, 2165, 2166, 5, 80, 0, 0, 2166, 2167, 5, 65, 0, 0, 2167, 2168, 5, 76, 0, 0, 2168, 2169, 5, 83, 0, 0, 2169, 402, 1, 0, 0, 0, 2170, 2171, 5, 80, 0, 0, 2171, 2172, 5, 82, 0, 0, 2172, 2173, 5, 79, 0, 0, 2173, 2174, 5, 80, 0, 0, 2174, 2175, 5, 69, 0, 0, 2175, 2176, 5, 82, 0, 0, 2176, 2177, 5, 84, 0, 0, 2177, 2178, 5, 73, 0, 0, 2178, 2179, 5, 69, 0, 0, 2179, 2180, 5, 83, 0, 0, 2180, 404, 1, 0, 0, 0, 2181, 2182, 5, 80, 0, 0, 2182, 2183, 5, 85, 0, 0, 2183, 2184, 5, 82, 0, 0, 2184, 2185, 5, 71, 0, 0, 2185, 2186, 5, 69, 0, 0, 2186, 406, 1, 0, 0, 0, 2187, 2188, 5, 81, 0, 0, 2188, 2189, 5, 85, 0, 0, 2189, 2190, 5, 65, 0, 0, 2190, 2191, 5, 82, 0, 0, 2191, 2192, 5, 84, 0, 0, 2192, 2193, 5, 69, 0, 0, 2193, 2194, 5, 82, 0, 0, 2194, 408, 1, 0, 0, 0, 2195, 2196, 5, 81, 0, 0, 2196, 2197, 5, 85, 0, 0, 2197, 2198, 5, 69, 0, 0, 2198, 2199, 5, 82, 0, 0, 2199, 2200, 5, 89, 0, 0, 2200, 410, 1, 0, 0, 0, 2201, 2202, 5, 82, 0, 0, 2202, 2203, 5, 65, 0, 0, 2203, 2204, 5, 78, 0, 0, 2204, 2205, 5, 71, 0, 0, 2205, 2206, 5, 69, 0, 0, 2206, 412, 1, 0, 0, 0, 2207, 2208, 5, 82, 0, 0, 2208, 2209, 5, 69, 0, 0, 2209, 2210, 5, 67, 0, 0, 2210, 2211, 5, 79, 0, 0, 2211, 2212, 5, 82, 0, 0, 2212, 2213, 5, 68, 0, 0, 2213, 2214, 5, 82, 0, 0, 2214, 2215, 5, 69, 0, 0, 2215, 2216, 5, 65, 0, 0, 2216, 2217, 5, 68, 0, 0, 2217, 2218, 5, 69, 0, 0, 2218, 2219, 5, 82, 0, 0, 2219, 414, 1, 0, 0, 0, 2220, 2221, 5, 82, 0, 0, 2221, 2222, 5, 69, 0, 0, 2222, 2223, 5, 67, 0, 0, 2223, 2224, 5, 79, 0, 0, 2224, 2225, 5, 82, 0, 0, 2225, 2226, 5, 68, 0, 0, 2226, 2227, 5, 87, 0, 0, 2227, 2228, 5, 82, 0, 0, 2228, 2229, 5, 73, 0, 0, 2229, 2230, 5, 84, 0, 0, 2230, 2231, 5, 69, 0, 0, 2231, 2232, 5, 82, 0, 0, 2232, 416, 1, 0, 0, 0, 2233, 2234, 5, 82, 0, 0, 2234, 2235, 5, 69, 0, 0, 2235, 2236, 5, 67, 0, 0, 2236, 2237, 5, 79, 0, 0, 2237, 2238, 5, 86, 0, 0, 2238, 2239, 5, 69, 0, 0, 2239, 2240, 5, 82, 0, 0, 2240, 418, 1, 0, 0, 0, 2241, 2242, 5, 82, 0, 0, 2242, 2243, 5, 69, 0, 0, 2243, 2244, 5, 68, 0, 0, 2244, 2245, 5, 85, 0, 0, 2245, 2246, 5, 67, 0, 0, 2246, 2247, 5, 69, 0, 0, 2247, 420, 1, 0, 0, 0, 2248, 2249, 5, 82, 0, 0, 2249, 2250, 5, 69, 0, 0, 2250, 2251, 5, 70, 0, 0, 2251, 2252, 5, 69, 0, 0, 2252, 2253, 5, 82, 0, 0, 2253, 2254, 5, 69, 0, 0, 2254, 2255, 5, 78, 0, 0, 2255, 2256, 5, 67, 0, 0, 2256, 2257, 5, 69, 0, 0, 2257, 2258, 5, 83, 0, 0, 2258, 422, 1, 0, 0, 0, 2259, 2260, 5, 82, 0, 0, 2260, 2261, 5, 69, 0, 0, 2261, 2262, 5, 70, 0, 0, 2262, 2263, 5, 82, 0, 0, 2263, 2264, 5, 69, 0, 0, 2264, 2265, 5, 83, 0, 0, 2265, 2266, 5, 72, 0, 0, 2266, 424, 1, 0, 0, 0, 2267, 2268, 5, 82, 0, 0, 2268, 2269, 5, 69, 0, 0, 2269, 2270, 5, 78, 0, 0, 2270, 2271, 5, 65, 0, 0, 2271, 2272, 5, 77, 0, 0, 2272, 2273, 5, 69, 0, 0, 2273, 426, 1, 0, 0, 0, 2274, 2275, 5, 82, 0, 0, 2275, 2276, 5, 69, 0, 0, 2276, 2277, 5, 80, 0, 0, 2277, 2278, 5, 65, 0, 0, 2278, 2279, 5, 73, 0, 0, 2279, 2280, 5, 82, 0, 0, 2280, 428, 1, 0, 0, 0, 2281, 2282, 5, 82, 0, 0, 2282, 2283, 5, 69, 0, 0, 2283, 2284, 5, 80, 0, 0, 2284, 2285, 5, 69, 0, 0, 2285, 2286, 5, 65, 0, 0, 2286, 2287, 5, 84, 0, 0, 2287, 2288, 5, 65, 0, 0, 2288, 2289, 5, 66, 0, 0, 2289, 2290, 5, 76, 0, 0, 2290, 2291, 5, 69, 0, 0, 2291, 430, 1, 0, 0, 0, 2292, 2293, 5, 82, 0, 0, 2293, 2294, 5, 69, 0, 0, 2294, 2295, 5, 80, 0, 0, 2295, 2296, 5, 76, 0, 0, 2296, 2297, 5, 65, 0, 0, 2297, 2298, 5, 67, 0, 0, 2298, 2299, 5, 69, 0, 0, 2299, 432, 1, 0, 0, 0, 2300, 2301, 5, 82, 0, 0, 2301, 2302, 5, 69, 0, 0, 2302, 2303, 5, 83, 0, 0, 2303, 2304, 5, 69, 0, 0, 2304, 2305, 5, 84, 0, 0, 2305, 434, 1, 0, 0, 0, 2306, 2307, 5, 82, 0, 0, 2307, 2308, 5, 69, 0, 0, 2308, 2309, 5, 83, 0, 0, 2309, 2310, 5, 80, 0, 0, 2310, 2311, 5, 69, 0, 0, 2311, 2312, 5, 67, 0, 0, 2312, 2313, 5, 84, 0, 0, 2313, 436, 1, 0, 0, 0, 2314, 2315, 5, 82, 0, 0, 2315, 2316, 5, 69, 0, 0, 2316, 2317, 5, 83, 0, 0, 2317, 2318, 5, 84, 0, 0, 2318, 2319, 5, 82, 0, 0, 2319, 2320, 5, 73, 0, 0, 2320, 2321, 5, 67, 0, 0, 2321, 2322, 5, 84, 0, 0, 2322, 438, 1, 0, 0, 0, 2323, 2324, 5, 82, 0, 0, 2324, 2325, 5, 69, 0, 0, 2325, 2326, 5, 86, 0, 0, 2326, 2327, 5, 79, 0, 0, 2327, 2328, 5, 75, 0, 0, 2328, 2329, 5, 69, 0, 0, 2329, 440, 1, 0, 0, 0, 2330, 2331, 5, 82, 0, 0, 2331, 2332, 5, 73, 0, 0, 2332, 2333, 5, 71, 0, 0, 2333, 2334, 5, 72, 0, 0, 2334, 2335, 5, 84, 0, 0, 2335, 442, 1, 0, 0, 0, 2336, 2337, 5, 82, 0, 0, 2337, 2338, 5, 76, 0, 0, 2338, 2339, 5, 73, 0, 0, 2339, 2340, 5, 75, 0, 0, 2340, 2348, 5, 69, 0, 0, 2341, 2342, 5, 82, 0, 0, 2342, 2343, 5, 69, 0, 0, 2343, 2344, 5, 71, 0, 0, 2344, 2345, 5, 69, 0, 0, 2345, 2346, 5, 88, 0, 0, 2346, 2348, 5, 80, 0, 0, 2347, 2336, 1, 0, 0, 0, 2347, 2341, 1, 0, 0, 0, 2348, 444, 1, 0, 0, 0, 2349, 2350, 5, 82, 0, 0, 2350, 2351, 5, 79, 0, 0, 2351, 2352, 5, 76, 0, 0, 2352, 2353, 5, 69, 0, 0, 2353, 446, 1, 0, 0, 0, 2354, 2355, 5, 82, 0, 0, 2355, 2356, 5, 79, 0, 0, 2356, 2357, 5, 76, 0, 0, 2357, 2358, 5, 69, 0, 0, 2358, 2359, 5, 83, 0, 0, 2359, 448, 1, 0, 0, 0, 2360, 2361, 5, 82, 0, 0, 2361, 2362, 5, 79, 0, 0, 2362, 2363, 5, 76, 0, 0, 2363, 2364, 5, 76, 0, 0, 2364, 2365, 5, 66, 0, 0, 2365, 2366, 5, 65, 0, 0, 2366, 2367, 5, 67, 0, 0, 2367, 2368, 5, 75, 0, 0, 2368, 450, 1, 0, 0, 0, 2369, 2370, 5, 82, 0, 0, 2370, 2371, 5, 79, 0, 0, 2371, 2372, 5, 76, 0, 0, 2372, 2373, 5, 76, 0, 0, 2373, 2374, 5, 85, 0, 0, 2374, 2375, 5, 80, 0, 0, 2375, 452, 1, 0, 0, 0, 2376, 2377, 5, 82, 0, 0, 2377, 2378, 5, 79, 0, 0, 2378, 2379, 5, 87, 0, 0, 2379, 454, 1, 0, 0, 0, 2380, 2381, 5, 82, 0, 0, 2381, 2382, 5, 79, 0, 0, 2382, 2383, 5, 87, 0, 0, 2383, 2384, 5, 83, 0, 0, 2384, 456, 1, 0, 0, 0, 2385, 2386, 5, 83, 0, 0, 2386, 2387, 5, 69, 0, 0, 2387, 2388, 5, 67, 0, 0, 2388, 2389, 5, 79, 0, 0, 2389, 2390, 5, 78, 0, 0, 2390, 2391, 5, 68, 0, 0, 2391, 458, 1, 0, 0, 0, 2392, 2393, 5, 83, 0, 0, 2393, 2394, 5, 69, 0, 0, 2394, 2395, 5, 67, 0, 0, 2395, 2396, 5, 79, 0, 0, 2396, 2397, 5, 78, 0, 0, 2397, 2398, 5, 68, 0, 0, 2398, 2399, 5, 83, 0, 0, 2399, 460, 1, 0, 0, 0, 2400, 2401, 5, 83, 0, 0, 2401, 2402, 5, 67, 0, 0, 2402, 2403, 5, 72, 0, 0, 2403, 2404, 5, 69, 0, 0, 2404, 2405, 5, 77, 0, 0, 2405, 2406, 5, 65, 0, 0, 2406, 462, 1, 0, 0, 0, 2407, 2408, 5, 83, 0, 0, 2408, 2409, 5, 67, 0, 0, 2409, 2410, 5, 72, 0, 0, 2410, 2411, 5, 69, 0, 0, 2411, 2412, 5, 77, 0, 0, 2412, 2413, 5, 65, 0, 0, 2413, 2414, 5, 83, 0, 0, 2414, 464, 1, 0, 0, 0, 2415, 2416, 5, 83, 0, 0, 2416, 2417, 5, 69, 0, 0, 2417, 2418, 5, 76, 0, 0, 2418, 2419, 5, 69, 0, 0, 2419, 2420, 5, 67, 0, 0, 2420, 2421, 5, 84, 0, 0, 2421, 466, 1, 0, 0, 0, 2422, 2423, 5, 83, 0, 0, 2423, 2424, 5, 69, 0, 0, 2424, 2425, 5, 77, 0, 0, 2425, 2426, 5, 73, 0, 0, 2426, 468, 1, 0, 0, 0, 2427, 2428, 5, 83, 0, 0, 2428, 2429, 5, 69, 0, 0, 2429, 2430, 5, 80, 0, 0, 2430, 2431, 5, 65, 0, 0, 2431, 2432, 5, 82, 0, 0, 2432, 2433, 5, 65, 0, 0, 2433, 2434, 5, 84, 0, 0, 2434, 2435, 5, 69, 0, 0, 2435, 2436, 5, 68, 0, 0, 2436, 470, 1, 0, 0, 0, 2437, 2438, 5, 83, 0, 0, 2438, 2439, 5, 69, 0, 0, 2439, 2440, 5, 82, 0, 0, 2440, 2441, 5, 68, 0, 0, 2441, 2442, 5, 69, 0, 0, 2442, 472, 1, 0, 0, 0, 2443, 2444, 5, 83, 0, 0, 2444, 2445, 5, 69, 0, 0, 2445, 2446, 5, 82, 0, 0, 2446, 2447, 5, 68, 0, 0, 2447, 2448, 5, 69, 0, 0, 2448, 2449, 5, 80, 0, 0, 2449, 2450, 5, 82, 0, 0, 2450, 2451, 5, 79, 0, 0, 2451, 2452, 5, 80, 0, 0, 2452, 2453, 5, 69, 0, 0, 2453, 2454, 5, 82, 0, 0, 2454, 2455, 5, 84, 0, 0, 2455, 2456, 5, 73, 0, 0, 2456, 2457, 5, 69, 0, 0, 2457, 2458, 5, 83, 0, 0, 2458, 474, 1, 0, 0, 0, 2459, 2460, 5, 83, 0, 0, 2460, 2461, 5, 69, 0, 0, 2461, 2462, 5, 83, 0, 0, 2462, 2463, 5, 83, 0, 0, 2463, 2464, 5, 73, 0, 0, 2464, 2465, 5, 79, 0, 0, 2465, 2466, 5, 78, 0, 0, 2466, 2467, 5, 95, 0, 0, 2467, 2468, 5, 85, 0, 0, 2468, 2469, 5, 83, 0, 0, 2469, 2470, 5, 69, 0, 0, 2470, 2471, 5, 82, 0, 0, 2471, 476, 1, 0, 0, 0, 2472, 2473, 5, 83, 0, 0, 2473, 2474, 5, 69, 0, 0, 2474, 2475, 5, 84, 0, 0, 2475, 478, 1, 0, 0, 0, 2476, 2477, 5, 77, 0, 0, 2477, 2478, 5, 73, 0, 0, 2478, 2479, 5, 78, 0, 0, 2479, 2480, 5, 85, 0, 0, 2480, 2481, 5, 83, 0, 0, 2481, 480, 1, 0, 0, 0, 2482, 2483, 5, 83, 0, 0, 2483, 2484, 5, 69, 0, 0, 2484, 2485, 5, 84, 0, 0, 2485, 2486, 5, 83, 0, 0, 2486, 482, 1, 0, 0, 0, 2487, 2488, 5, 83, 0, 0, 2488, 2489, 5, 72, 0, 0, 2489, 2490, 5, 79, 0, 0, 2490, 2491, 5, 87, 0, 0, 2491, 484, 1, 0, 0, 0, 2492, 2493, 5, 83, 0, 0, 2493, 2494, 5, 75, 0, 0, 2494, 2495, 5, 69, 0, 0, 2495, 2496, 5, 87, 0, 0, 2496, 2497, 5, 69, 0, 0, 2497, 2498, 5, 68, 0, 0, 2498, 486, 1, 0, 0, 0, 2499, 2500, 5, 83, 0, 0, 2500, 2501, 5, 79, 0, 0, 2501, 2502, 5, 77, 0, 0, 2502, 2503, 5, 69, 0, 0, 2503, 488, 1, 0, 0, 0, 2504, 2505, 5, 83, 0, 0, 2505, 2506, 5, 79, 0, 0, 2506, 2507, 5, 82, 0, 0, 2507, 2508, 5, 84, 0, 0, 2508, 490, 1, 0, 0, 0, 2509, 2510, 5, 83, 0, 0, 2510, 2511, 5, 79, 0, 0, 2511, 2512, 5, 82, 0, 0, 2512, 2513, 5, 84, 0, 0, 2513, 2514, 5, 69, 0, 0, 2514, 2515, 5, 68, 0, 0, 2515, 492, 1, 0, 0, 0, 2516, 2517, 5, 83, 0, 0, 2517, 2518, 5, 79, 0, 0, 2518, 2519, 5, 85, 0, 0, 2519, 2520, 5, 82, 0, 0, 2520, 2521, 5, 67, 0, 0, 2521, 2522, 5, 69, 0, 0, 2522, 494, 1, 0, 0, 0, 2523, 2524, 5, 83, 0, 0, 2524, 2525, 5, 84, 0, 0, 2525, 2526, 5, 65, 0, 0, 2526, 2527, 5, 82, 0, 0, 2527, 2528, 5, 84, 0, 0, 2528, 496, 1, 0, 0, 0, 2529, 2530, 5, 83, 0, 0, 2530, 2531, 5, 84, 0, 0, 2531, 2532, 5, 65, 0, 0, 2532, 2533, 5, 84, 0, 0, 2533, 2534, 5, 73, 0, 0, 2534, 2535, 5, 83, 0, 0, 2535, 2536, 5, 84, 0, 0, 2536, 2537, 5, 73, 0, 0, 2537, 2538, 5, 67, 0, 0, 2538, 2539, 5, 83, 0, 0, 2539, 498, 1, 0, 0, 0, 2540, 2541, 5, 83, 0, 0, 2541, 2542, 5, 84, 0, 0, 2542, 2543, 5, 79, 0, 0, 2543, 2544, 5, 82, 0, 0, 2544, 2545, 5, 69, 0, 0, 2545, 2546, 5, 68, 0, 0, 2546, 500, 1, 0, 0, 0, 2547, 2548, 5, 83, 0, 0, 2548, 2549, 5, 84, 0, 0, 2549, 2550, 5, 82, 0, 0, 2550, 2551, 5, 65, 0, 0, 2551, 2552, 5, 84, 0, 0, 2552, 2553, 5, 73, 0, 0, 2553, 2554, 5, 70, 0, 0, 2554, 2555, 5, 89, 0, 0, 2555, 502, 1, 0, 0, 0, 2556, 2557, 5, 83, 0, 0, 2557, 2558, 5, 84, 0, 0, 2558, 2559, 5, 82, 0, 0, 2559, 2560, 5, 85, 0, 0, 2560, 2561, 5, 67, 0, 0, 2561, 2562, 5, 84, 0, 0, 2562, 504, 1, 0, 0, 0, 2563, 2564, 5, 83, 0, 0, 2564, 2565, 5, 85, 0, 0, 2565, 2566, 5, 66, 0, 0, 2566, 2567, 5, 83, 0, 0, 2567, 2568, 5, 84, 0, 0, 2568, 2569, 5, 82, 0, 0, 2569, 506, 1, 0, 0, 0, 2570, 2571, 5, 83, 0, 0, 2571, 2572, 5, 85, 0, 0, 2572, 2573, 5, 66, 0, 0, 2573, 2574, 5, 83, 0, 0, 2574, 2575, 5, 84, 0, 0, 2575, 2576, 5, 82, 0, 0, 2576, 2577, 5, 73, 0, 0, 2577, 2578, 5, 78, 0, 0, 2578, 2579, 5, 71, 0, 0, 2579, 508, 1, 0, 0, 0, 2580, 2581, 5, 83, 0, 0, 2581, 2582, 5, 89, 0, 0, 2582, 2583, 5, 78, 0, 0, 2583, 2584, 5, 67, 0, 0, 2584, 510, 1, 0, 0, 0, 2585, 2586, 5, 83, 0, 0, 2586, 2587, 5, 89, 0, 0, 2587, 2588, 5, 83, 0, 0, 2588, 2589, 5, 84, 0, 0, 2589, 2590, 5, 69, 0, 0, 2590, 2591, 5, 77, 0, 0, 2591, 2592, 5, 95, 0, 0, 2592, 2593, 5, 84, 0, 0, 2593, 2594, 5, 73, 0, 0, 2594, 2595, 5, 77, 0, 0, 2595, 2596, 5, 69, 0, 0, 2596, 512, 1, 0, 0, 0, 2597, 2598, 5, 83, 0, 0, 2598, 2599, 5, 89, 0, 0, 2599, 2600, 5, 83, 0, 0, 2600, 2601, 5, 84, 0, 0, 2601, 2602, 5, 69, 0, 0, 2602, 2603, 5, 77, 0, 0, 2603, 2604, 5, 95, 0, 0, 2604, 2605, 5, 86, 0, 0, 2605, 2606, 5, 69, 0, 0, 2606, 2607, 5, 82, 0, 0, 2607, 2608, 5, 83, 0, 0, 2608, 2609, 5, 73, 0, 0, 2609, 2610, 5, 79, 0, 0, 2610, 2611, 5, 78, 0, 0, 2611, 514, 1, 0, 0, 0, 2612, 2613, 5, 84, 0, 0, 2613, 2614, 5, 65, 0, 0, 2614, 2615, 5, 66, 0, 0, 2615, 2616, 5, 76, 0, 0, 2616, 2617, 5, 69, 0, 0, 2617, 516, 1, 0, 0, 0, 2618, 2619, 5, 84, 0, 0, 2619, 2620, 5, 65, 0, 0, 2620, 2621, 5, 66, 0, 0, 2621, 2622, 5, 76, 0, 0, 2622, 2623, 5, 69, 0, 0, 2623, 2624, 5, 83, 0, 0, 2624, 518, 1, 0, 0, 0, 2625, 2626, 5, 84, 0, 0, 2626, 2627, 5, 65, 0, 0, 2627, 2628, 5, 66, 0, 0, 2628, 2629, 5, 76, 0, 0, 2629, 2630, 5, 69, 0, 0, 2630, 2631, 5, 83, 0, 0, 2631, 2632, 5, 65, 0, 0, 2632, 2633, 5, 77, 0, 0, 2633, 2634, 5, 80, 0, 0, 2634, 2635, 5, 76, 0, 0, 2635, 2636, 5, 69, 0, 0, 2636, 520, 1, 0, 0, 0, 2637, 2638, 5, 84, 0, 0, 2638, 2639, 5, 65, 0, 0, 2639, 2640, 5, 82, 0, 0, 2640, 2641, 5, 71, 0, 0, 2641, 2642, 5, 69, 0, 0, 2642, 2643, 5, 84, 0, 0, 2643, 522, 1, 0, 0, 0, 2644, 2645, 5, 84, 0, 0, 2645, 2646, 5, 66, 0, 0, 2646, 2647, 5, 76, 0, 0, 2647, 2648, 5, 80, 0, 0, 2648, 2649, 5, 82, 0, 0, 2649, 2650, 5, 79, 0, 0, 2650, 2651, 5, 80, 0, 0, 2651, 2652, 5, 69, 0, 0, 2652, 2653, 5, 82, 0, 0, 2653, 2654, 5, 84, 0, 0, 2654, 2655, 5, 73, 0, 0, 2655, 2656, 5, 69, 0, 0, 2656, 2657, 5, 83, 0, 0, 2657, 524, 1, 0, 0, 0, 2658, 2659, 5, 84, 0, 0, 2659, 2660, 5, 69, 0, 0, 2660, 2661, 5, 77, 0, 0, 2661, 2662, 5, 80, 0, 0, 2662, 2663, 5, 79, 0, 0, 2663, 2664, 5, 82, 0, 0, 2664, 2665, 5, 65, 0, 0, 2665, 2666, 5, 82, 0, 0, 2666, 2672, 5, 89, 0, 0, 2667, 2668, 5, 84, 0, 0, 2668, 2669, 5, 69, 0, 0, 2669, 2670, 5, 77, 0, 0, 2670, 2672, 5, 80, 0, 0, 2671, 2658, 1, 0, 0, 0, 2671, 2667, 1, 0, 0, 0, 2672, 526, 1, 0, 0, 0, 2673, 2674, 5, 84, 0, 0, 2674, 2675, 5, 69, 0, 0, 2675, 2676, 5, 82, 0, 0, 2676, 2677, 5, 77, 0, 0, 2677, 2678, 5, 73, 0, 0, 2678, 2679, 5, 78, 0, 0, 2679, 2680, 5, 65, 0, 0, 2680, 2681, 5, 84, 0, 0, 2681, 2682, 5, 69, 0, 0, 2682, 2683, 5, 68, 0, 0, 2683, 528, 1, 0, 0, 0, 2684, 2685, 5, 84, 0, 0, 2685, 2686, 5, 72, 0, 0, 2686, 2687, 5, 69, 0, 0, 2687, 2688, 5, 78, 0, 0, 2688, 530, 1, 0, 0, 0, 2689, 2690, 5, 84, 0, 0, 2690, 2691, 5, 73, 0, 0, 2691, 2692, 5, 77, 0, 0, 2692, 2693, 5, 69, 0, 0, 2693, 532, 1, 0, 0, 0, 2694, 2695, 5, 84, 0, 0, 2695, 2696, 5, 73, 0, 0, 2696, 2697, 5, 77, 0, 0, 2697, 2698, 5, 69, 0, 0, 2698, 2699, 5, 83, 0, 0, 2699, 2700, 5, 84, 0, 0, 2700, 2701, 5, 65, 0, 0, 2701, 2702, 5, 77, 0, 0, 2702, 2703, 5, 80, 0, 0, 2703, 534, 1, 0, 0, 0, 2704, 2705, 5, 84, 0, 0, 2705, 2706, 5, 73, 0, 0, 2706, 2707, 5, 77, 0, 0, 2707, 2708, 5, 69, 0, 0, 2708, 2709, 5, 83, 0, 0, 2709, 2710, 5, 84, 0, 0, 2710, 2711, 5, 65, 0, 0, 2711, 2712, 5, 77, 0, 0, 2712, 2713, 5, 80, 0, 0, 2713, 2714, 5, 65, 0, 0, 2714, 2715, 5, 68, 0, 0, 2715, 2716, 5, 68, 0, 0, 2716, 536, 1, 0, 0, 0, 2717, 2718, 5, 84, 0, 0, 2718, 2719, 5, 73, 0, 0, 2719, 2720, 5, 77, 0, 0, 2720, 2721, 5, 69, 0, 0, 2721, 2722, 5, 83, 0, 0, 2722, 2723, 5, 84, 0, 0, 2723, 2724, 5, 65, 0, 0, 2724, 2725, 5, 77, 0, 0, 2725, 2726, 5, 80, 0, 0, 2726, 2727, 5, 68, 0, 0, 2727, 2728, 5, 73, 0, 0, 2728, 2729, 5, 70, 0, 0, 2729, 2730, 5, 70, 0, 0, 2730, 538, 1, 0, 0, 0, 2731, 2732, 5, 84, 0, 0, 2732, 2733, 5, 79, 0, 0, 2733, 540, 1, 0, 0, 0, 2734, 2735, 5, 84, 0, 0, 2735, 2736, 5, 79, 0, 0, 2736, 2737, 5, 85, 0, 0, 2737, 2738, 5, 67, 0, 0, 2738, 2739, 5, 72, 0, 0, 2739, 542, 1, 0, 0, 0, 2740, 2741, 5, 84, 0, 0, 2741, 2742, 5, 82, 0, 0, 2742, 2743, 5, 65, 0, 0, 2743, 2744, 5, 73, 0, 0, 2744, 2745, 5, 76, 0, 0, 2745, 2746, 5, 73, 0, 0, 2746, 2747, 5, 78, 0, 0, 2747, 2748, 5, 71, 0, 0, 2748, 544, 1, 0, 0, 0, 2749, 2750, 5, 84, 0, 0, 2750, 2751, 5, 82, 0, 0, 2751, 2752, 5, 65, 0, 0, 2752, 2753, 5, 78, 0, 0, 2753, 2754, 5, 83, 0, 0, 2754, 2755, 5, 65, 0, 0, 2755, 2756, 5, 67, 0, 0, 2756, 2757, 5, 84, 0, 0, 2757, 2758, 5, 73, 0, 0, 2758, 2759, 5, 79, 0, 0, 2759, 2760, 5, 78, 0, 0, 2760, 546, 1, 0, 0, 0, 2761, 2762, 5, 84, 0, 0, 2762, 2763, 5, 82, 0, 0, 2763, 2764, 5, 65, 0, 0, 2764, 2765, 5, 78, 0, 0, 2765, 2766, 5, 83, 0, 0, 2766, 2767, 5, 65, 0, 0, 2767, 2768, 5, 67, 0, 0, 2768, 2769, 5, 84, 0, 0, 2769, 2770, 5, 73, 0, 0, 2770, 2771, 5, 79, 0, 0, 2771, 2772, 5, 78, 0, 0, 2772, 2773, 5, 83, 0, 0, 2773, 548, 1, 0, 0, 0, 2774, 2775, 5, 84, 0, 0, 2775, 2776, 5, 82, 0, 0, 2776, 2777, 5, 65, 0, 0, 2777, 2778, 5, 78, 0, 0, 2778, 2779, 5, 83, 0, 0, 2779, 2780, 5, 70, 0, 0, 2780, 2781, 5, 79, 0, 0, 2781, 2782, 5, 82, 0, 0, 2782, 2783, 5, 77, 0, 0, 2783, 550, 1, 0, 0, 0, 2784, 2785, 5, 84, 0, 0, 2785, 2786, 5, 82, 0, 0, 2786, 2787, 5, 73, 0, 0, 2787, 2788, 5, 77, 0, 0, 2788, 552, 1, 0, 0, 0, 2789, 2790, 5, 84, 0, 0, 2790, 2791, 5, 82, 0, 0, 2791, 2792, 5, 85, 0, 0, 2792, 2793, 5, 69, 0, 0, 2793, 554, 1, 0, 0, 0, 2794, 2795, 5, 84, 0, 0, 2795, 2796, 5, 82, 0, 0, 2796, 2797, 5, 85, 0, 0, 2797, 2798, 5, 78, 0, 0, 2798, 2799, 5, 67, 0, 0, 2799, 2800, 5, 65, 0, 0, 2800, 2801, 5, 84, 0, 0, 2801, 2802, 5, 69, 0, 0, 2802, 556, 1, 0, 0, 0, 2803, 2804, 5, 84, 0, 0, 2804, 2805, 5, 82, 0, 0, 2805, 2806, 5, 89, 0, 0, 2806, 2807, 5, 95, 0, 0, 2807, 2808, 5, 67, 0, 0, 2808, 2809, 5, 65, 0, 0, 2809, 2810, 5, 83, 0, 0, 2810, 2811, 5, 84, 0, 0, 2811, 558, 1, 0, 0, 0, 2812, 2813, 5, 84, 0, 0, 2813, 2814, 5, 89, 0, 0, 2814, 2815, 5, 80, 0, 0, 2815, 2816, 5, 69, 0, 0, 2816, 560, 1, 0, 0, 0, 2817, 2818, 5, 85, 0, 0, 2818, 2819, 5, 78, 0, 0, 2819, 2820, 5, 65, 0, 0, 2820, 2821, 5, 82, 0, 0, 2821, 2822, 5, 67, 0, 0, 2822, 2823, 5, 72, 0, 0, 2823, 2824, 5, 73, 0, 0, 2824, 2825, 5, 86, 0, 0, 2825, 2826, 5, 69, 0, 0, 2826, 562, 1, 0, 0, 0, 2827, 2828, 5, 85, 0, 0, 2828, 2829, 5, 78, 0, 0, 2829, 2830, 5, 66, 0, 0, 2830, 2831, 5, 79, 0, 0, 2831, 2832, 5, 85, 0, 0, 2832, 2833, 5, 78, 0, 0, 2833, 2834, 5, 68, 0, 0, 2834, 2835, 5, 69, 0, 0, 2835, 2836, 5, 68, 0, 0, 2836, 564, 1, 0, 0, 0, 2837, 2838, 5, 85, 0, 0, 2838, 2839, 5, 78, 0, 0, 2839, 2840, 5, 67, 0, 0, 2840, 2841, 5, 65, 0, 0, 2841, 2842, 5, 67, 0, 0, 2842, 2843, 5, 72, 0, 0, 2843, 2844, 5, 69, 0, 0, 2844, 566, 1, 0, 0, 0, 2845, 2846, 5, 85, 0, 0, 2846, 2847, 5, 78, 0, 0, 2847, 2848, 5, 73, 0, 0, 2848, 2849, 5, 79, 0, 0, 2849, 2850, 5, 78, 0, 0, 2850, 568, 1, 0, 0, 0, 2851, 2852, 5, 85, 0, 0, 2852, 2853, 5, 78, 0, 0, 2853, 2854, 5, 73, 0, 0, 2854, 2855, 5, 81, 0, 0, 2855, 2856, 5, 85, 0, 0, 2856, 2857, 5, 69, 0, 0, 2857, 570, 1, 0, 0, 0, 2858, 2859, 5, 85, 0, 0, 2859, 2860, 5, 78, 0, 0, 2860, 2861, 5, 75, 0, 0, 2861, 2862, 5, 78, 0, 0, 2862, 2863, 5, 79, 0, 0, 2863, 2864, 5, 87, 0, 0, 2864, 2865, 5, 78, 0, 0, 2865, 572, 1, 0, 0, 0, 2866, 2867, 5, 85, 0, 0, 2867, 2868, 5, 78, 0, 0, 2868, 2869, 5, 76, 0, 0, 2869, 2870, 5, 79, 0, 0, 2870, 2871, 5, 67, 0, 0, 2871, 2872, 5, 75, 0, 0, 2872, 574, 1, 0, 0, 0, 2873, 2874, 5, 85, 0, 0, 2874, 2875, 5, 78, 0, 0, 2875, 2876, 5, 80, 0, 0, 2876, 2877, 5, 73, 0, 0, 2877, 2878, 5, 86, 0, 0, 2878, 2879, 5, 79, 0, 0, 2879, 2880, 5, 84, 0, 0, 2880, 576, 1, 0, 0, 0, 2881, 2882, 5, 85, 0, 0, 2882, 2883, 5, 78, 0, 0, 2883, 2884, 5, 83, 0, 0, 2884, 2885, 5, 69, 0, 0, 2885, 2886, 5, 84, 0, 0, 2886, 578, 1, 0, 0, 0, 2887, 2888, 5, 85, 0, 0, 2888, 2889, 5, 80, 0, 0, 2889, 2890, 5, 68, 0, 0, 2890, 2891, 5, 65, 0, 0, 2891, 2892, 5, 84, 0, 0, 2892, 2893, 5, 69, 0, 0, 2893, 580, 1, 0, 0, 0, 2894, 2895, 5, 85, 0, 0, 2895, 2896, 5, 83, 0, 0, 2896, 2897, 5, 69, 0, 0, 2897, 582, 1, 0, 0, 0, 2898, 2899, 5, 85, 0, 0, 2899, 2900, 5, 83, 0, 0, 2900, 2901, 5, 69, 0, 0, 2901, 2902, 5, 82, 0, 0, 2902, 584, 1, 0, 0, 0, 2903, 2904, 5, 85, 0, 0, 2904, 2905, 5, 83, 0, 0, 2905, 2906, 5, 73, 0, 0, 2906, 2907, 5, 78, 0, 0, 2907, 2908, 5, 71, 0, 0, 2908, 586, 1, 0, 0, 0, 2909, 2910, 5, 86, 0, 0, 2910, 2911, 5, 65, 0, 0, 2911, 2912, 5, 76, 0, 0, 2912, 2913, 5, 85, 0, 0, 2913, 2914, 5, 69, 0, 0, 2914, 2915, 5, 83, 0, 0, 2915, 588, 1, 0, 0, 0, 2916, 2917, 5, 86, 0, 0, 2917, 2918, 5, 69, 0, 0, 2918, 2919, 5, 82, 0, 0, 2919, 2920, 5, 83, 0, 0, 2920, 2921, 5, 73, 0, 0, 2921, 2922, 5, 79, 0, 0, 2922, 2923, 5, 78, 0, 0, 2923, 590, 1, 0, 0, 0, 2924, 2925, 5, 86, 0, 0, 2925, 2926, 5, 73, 0, 0, 2926, 2927, 5, 69, 0, 0, 2927, 2928, 5, 87, 0, 0, 2928, 592, 1, 0, 0, 0, 2929, 2930, 5, 86, 0, 0, 2930, 2931, 5, 73, 0, 0, 2931, 2932, 5, 69, 0, 0, 2932, 2933, 5, 87, 0, 0, 2933, 2934, 5, 83, 0, 0, 2934, 594, 1, 0, 0, 0, 2935, 2936, 5, 87, 0, 0, 2936, 2937, 5, 69, 0, 0, 2937, 2938, 5, 69, 0, 0, 2938, 2939, 5, 75, 0, 0, 2939, 596, 1, 0, 0, 0, 2940, 2941, 5, 87, 0, 0, 2941, 2942, 5, 69, 0, 0, 2942, 2943, 5, 69, 0, 0, 2943, 2944, 5, 75, 0, 0, 2944, 2945, 5, 83, 0, 0, 2945, 598, 1, 0, 0, 0, 2946, 2947, 5, 87, 0, 0, 2947, 2948, 5, 72, 0, 0, 2948, 2949, 5, 69, 0, 0, 2949, 2950, 5, 78, 0, 0, 2950, 600, 1, 0, 0, 0, 2951, 2952, 5, 87, 0, 0, 2952, 2953, 5, 72, 0, 0, 2953, 2954, 5, 69, 0, 0, 2954, 2955, 5, 82, 0, 0, 2955, 2956, 5, 69, 0, 0, 2956, 602, 1, 0, 0, 0, 2957, 2958, 5, 87, 0, 0, 2958, 2959, 5, 73, 0, 0, 2959, 2960, 5, 78, 0, 0, 2960, 2961, 5, 68, 0, 0, 2961, 2962, 5, 79, 0, 0, 2962, 2963, 5, 87, 0, 0, 2963, 604, 1, 0, 0, 0, 2964, 2965, 5, 87, 0, 0, 2965, 2966, 5, 73, 0, 0, 2966, 2967, 5, 84, 0, 0, 2967, 2968, 5, 72, 0, 0, 2968, 606, 1, 0, 0, 0, 2969, 2970, 5, 87, 0, 0, 2970, 2971, 5, 73, 0, 0, 2971, 2972, 5, 84, 0, 0, 2972, 2973, 5, 72, 0, 0, 2973, 2974, 5, 73, 0, 0, 2974, 2975, 5, 78, 0, 0, 2975, 608, 1, 0, 0, 0, 2976, 2977, 5, 89, 0, 0, 2977, 2978, 5, 69, 0, 0, 2978, 2979, 5, 65, 0, 0, 2979, 2980, 5, 82, 0, 0, 2980, 610, 1, 0, 0, 0, 2981, 2982, 5, 89, 0, 0, 2982, 2983, 5, 69, 0, 0, 2983, 2984, 5, 65, 0, 0, 2984, 2985, 5, 82, 0, 0, 2985, 2986, 5, 83, 0, 0, 2986, 612, 1, 0, 0, 0, 2987, 2988, 5, 90, 0, 0, 2988, 2989, 5, 79, 0, 0, 2989, 2990, 5, 78, 0, 0, 2990, 2991, 5, 69, 0, 0, 2991, 614, 1, 0, 0, 0, 2992, 2996, 5, 61, 0, 0, 2993, 2994, 5, 61, 0, 0, 2994, 2996, 5, 61, 0, 0, 2995, 2992, 1, 0, 0, 0, 2995, 2993, 1, 0, 0, 0, 2996, 616, 1, 0, 0, 0, 2997, 2998, 5, 60, 0, 0, 2998, 2999, 5, 61, 0, 0, 2999, 3000, 5, 62, 0, 0, 3000, 618, 1, 0, 0, 0, 3001, 3002, 5, 60, 0, 0, 3002, 3003, 5, 62, 0, 0, 3003, 620, 1, 0, 0, 0, 3004, 3005, 5, 33, 0, 0, 3005, 3006, 5, 61, 0, 0, 3006, 622, 1, 0, 0, 0, 3007, 3008, 5, 60, 0, 0, 3008, 624, 1, 0, 0, 0, 3009, 3010, 5, 60, 0, 0, 3010, 3014, 5, 61, 0, 0, 3011, 3012, 5, 33, 0, 0, 3012, 3014, 5, 62, 0, 0, 3013, 3009, 1, 0, 0, 0, 3013, 3011, 1, 0, 0, 0, 3014, 626, 1, 0, 0, 0, 3015, 3016, 5, 62, 0, 0, 3016, 628, 1, 0, 0, 0, 3017, 3018, 5, 62, 0, 0, 3018, 3022, 5, 61, 0, 0, 3019, 3020, 5, 33, 0, 0, 3020, 3022, 5, 60, 0, 0, 3021, 3017, 1, 0, 0, 0, 3021, 3019, 1, 0, 0, 0, 3022, 630, 1, 0, 0, 0, 3023, 3024, 5, 43, 0, 0, 3024, 632, 1, 0, 0, 0, 3025, 3026, 5, 45, 0, 0, 3026, 634, 1, 0, 0, 0, 3027, 3028, 5, 42, 0, 0, 3028, 636, 1, 0, 0, 0, 3029, 3030, 5, 47, 0, 0, 3030, 638, 1, 0, 0, 0, 3031, 3032, 5, 37, 0, 0, 3032, 640, 1, 0, 0, 0, 3033, 3034, 5, 126, 0, 0, 3034, 642, 1, 0, 0, 0, 3035, 3036, 5, 38, 0, 0, 3036, 644, 1, 0, 0, 0, 3037, 3038, 5, 124, 0, 0, 3038, 646, 1, 0, 0, 0, 3039, 3040, 5, 124, 0, 0, 3040, 3041, 5, 124, 0, 0, 3041, 648, 1, 0, 0, 0, 3042, 3043, 5, 94, 0, 0, 3043, 650, 1, 0, 0, 0, 3044, 3045, 5, 58, 0, 0, 3045, 652, 1, 0, 0, 0, 3046, 3047, 5, 45, 0, 0, 3047, 3048, 5, 62, 0, 0, 3048, 654, 1, 0, 0, 0, 3049, 3050, 5, 47, 0, 0, 3050, 3051, 5, 42, 0, 0, 3051, 3052, 5, 43, 0, 0, 3052, 656, 1, 0, 0, 0, 3053, 3054, 5, 42, 0, 0, 3054, 3055, 5, 47, 0, 0, 3055, 658, 1, 0, 0, 0, 3056, 3062, 5, 39, 0, 0, 3057, 3061, 8, 0, 0, 0, 3058, 3059, 5, 92, 0, 0, 3059, 3061, 9, 0, 0, 0, 3060, 3057, 1, 0, 0, 0, 3060, 3058, 1, 0, 0, 0, 3061, 3064, 1, 0, 0, 0, 3062, 3060, 1, 0, 0, 0, 3062, 3063, 1, 0, 0, 0, 3063, 3065, 1, 0, 0, 0, 3064, 3062, 1, 0, 0, 0, 3065, 3087, 5, 39, 0, 0, 3066, 3067, 5, 82, 0, 0, 3067, 3068, 5, 39, 0, 0, 3068, 3072, 1, 0, 0, 0, 3069, 3071, 8, 1, 0, 0, 3070, 3069, 1, 0, 0, 0, 3071, 3074, 1, 0, 0, 0, 3072, 3070, 1, 0, 0, 0, 3072, 3073, 1, 0, 0, 0, 3073, 3075, 1, 0, 0, 0, 3074, 3072, 1, 0, 0, 0, 3075, 3087, 5, 39, 0, 0, 3076, 3077, 5, 82, 0, 0, 3077, 3078, 5, 34, 0, 0, 3078, 3082, 1, 0, 0, 0, 3079, 3081, 8, 2, 0, 0, 3080, 3079, 1, 0, 0, 0, 3081, 3084, 1, 0, 0, 0, 3082, 3080, 1, 0, 0, 0, 3082, 3083, 1, 0, 0, 0, 3083, 3085, 1, 0, 0, 0, 3084, 3082, 1, 0, 0, 0, 3085, 3087, 5, 34, 0, 0, 3086, 3056, 1, 0, 0, 0, 3086, 3066, 1, 0, 0, 0, 3086, 3076, 1, 0, 0, 0, 3087, 660, 1, 0, 0, 0, 3088, 3094, 5, 34, 0, 0, 3089, 3093, 8, 3, 0, 0, 3090, 3091, 5, 92, 0, 0, 3091, 3093, 9, 0, 0, 0, 3092, 3089, 1, 0, 0, 0, 3092, 3090, 1, 0, 0, 0, 3093, 3096, 1, 0, 0, 0, 3094, 3092, 1, 0, 0, 0, 3094, 3095, 1, 0, 0, 0, 3095, 3097, 1, 0, 0, 0, 3096, 3094, 1, 0, 0, 0, 3097, 3098, 5, 34, 0, 0, 3098, 662, 1, 0, 0, 0, 3099, 3101, 3, 689, 344, 0, 3100, 3099, 1, 0, 0, 0, 3101, 3102, 1, 0, 0, 0, 3102, 3100, 1, 0, 0, 0, 3102, 3103, 1, 0, 0, 0, 3103, 3104, 1, 0, 0, 0, 3104, 3105, 5, 76, 0, 0, 3105, 664, 1, 0, 0, 0, 3106, 3108, 3, 689, 344, 0, 3107, 3106, 1, 0, 0, 0, 3108, 3109, 1, 0, 0, 0, 3109, 3107, 1, 0, 0, 0, 3109, 3110, 1, 0, 0, 0, 3110, 3111, 1, 0, 0, 0, 3111, 3112, 5, 83, 0, 0, 3112, 666, 1, 0, 0, 0, 3113, 3115, 3, 689, 344, 0, 3114, 3113, 1, 0, 0, 0, 3115, 3116, 1, 0, 0, 0, 3116, 3114, 1, 0, 0, 0, 3116, 3117, 1, 0, 0, 0, 3117, 3118, 1, 0, 0, 0, 3118, 3119, 5, 89, 0, 0, 3119, 668, 1, 0, 0, 0, 3120, 3122, 3, 689, 344, 0, 3121, 3120, 1, 0, 0, 0, 3122, 3123, 1, 0, 0, 0, 3123, 3121, 1, 0, 0, 0, 3123, 3124, 1, 0, 0, 0, 3124, 670, 1, 0, 0, 0, 3125, 3127, 3, 689, 344, 0, 3126, 3125, 1, 0, 0, 0, 3127, 3128, 1, 0, 0, 0, 3128, 3126, 1, 0, 0, 0, 3128, 3129, 1, 0, 0, 0, 3129, 3130, 1, 0, 0, 0, 3130, 3131, 3, 687, 343, 0, 3131, 3137, 1, 0, 0, 0, 3132, 3133, 3, 685, 342, 0, 3133, 3134, 3, 687, 343, 0, 3134, 3135, 4, 335, 0, 0, 3135, 3137, 1, 0, 0, 0, 3136, 3126, 1, 0, 0, 0, 3136, 3132, 1, 0, 0, 0, 3137, 672, 1, 0, 0, 0, 3138, 3139, 3, 685, 342, 0, 3139, 3140, 4, 336, 1, 0, 3140, 674, 1, 0, 0, 0, 3141, 3143, 3, 689, 344, 0, 3142, 3141, 1, 0, 0, 0, 3143, 3144, 1, 0, 0, 0, 3144, 3142, 1, 0, 0, 0, 3144, 3145, 1, 0, 0, 0, 3145, 3147, 1, 0, 0, 0, 3146, 3148, 3, 687, 343, 0, 3147, 3146, 1, 0, 0, 0, 3147, 3148, 1, 0, 0, 0, 3148, 3149, 1, 0, 0, 0, 3149, 3150, 5, 70, 0, 0, 3150, 3159, 1, 0, 0, 0, 3151, 3153, 3, 685, 342, 0, 3152, 3154, 3, 687, 343, 0, 3153, 3152, 1, 0, 0, 0, 3153, 3154, 1, 0, 0, 0, 3154, 3155, 1, 0, 0, 0, 3155, 3156, 5, 70, 0, 0, 3156, 3157, 4, 337, 2, 0, 3157, 3159, 1, 0, 0, 0, 3158, 3142, 1, 0, 0, 0, 3158, 3151, 1, 0, 0, 0, 3159, 676, 1, 0, 0, 0, 3160, 3162, 3, 689, 344, 0, 3161, 3160, 1, 0, 0, 0, 3162, 3163, 1, 0, 0, 0, 3163, 3161, 1, 0, 0, 0, 3163, 3164, 1, 0, 0, 0, 3164, 3166, 1, 0, 0, 0, 3165, 3167, 3, 687, 343, 0, 3166, 3165, 1, 0, 0, 0, 3166, 3167, 1, 0, 0, 0, 3167, 3168, 1, 0, 0, 0, 3168, 3169, 5, 68, 0, 0, 3169, 3178, 1, 0, 0, 0, 3170, 3172, 3, 685, 342, 0, 3171, 3173, 3, 687, 343, 0, 3172, 3171, 1, 0, 0, 0, 3172, 3173, 1, 0, 0, 0, 3173, 3174, 1, 0, 0, 0, 3174, 3175, 5, 68, 0, 0, 3175, 3176, 4, 338, 3, 0, 3176, 3178, 1, 0, 0, 0, 3177, 3161, 1, 0, 0, 0, 3177, 3170, 1, 0, 0, 0, 3178, 678, 1, 0, 0, 0, 3179, 3181, 3, 689, 344, 0, 3180, 3179, 1, 0, 0, 0, 3181, 3182, 1, 0, 0, 0, 3182, 3180, 1, 0, 0, 0, 3182, 3183, 1, 0, 0, 0, 3183, 3185, 1, 0, 0, 0, 3184, 3186, 3, 687, 343, 0, 3185, 3184, 1, 0, 0, 0, 3185, 3186, 1, 0, 0, 0, 3186, 3187, 1, 0, 0, 0, 3187, 3188, 5, 66, 0, 0, 3188, 3189, 5, 68, 0, 0, 3189, 3200, 1, 0, 0, 0, 3190, 3192, 3, 685, 342, 0, 3191, 3193, 3, 687, 343, 0, 3192, 3191, 1, 0, 0, 0, 3192, 3193, 1, 0, 0, 0, 3193, 3194, 1, 0, 0, 0, 3194, 3195, 5, 66, 0, 0, 3195, 3196, 5, 68, 0, 0, 3196, 3197, 1, 0, 0, 0, 3197, 3198, 4, 339, 4, 0, 3198, 3200, 1, 0, 0, 0, 3199, 3180, 1, 0, 0, 0, 3199, 3190, 1, 0, 0, 0, 3200, 680, 1, 0, 0, 0, 3201, 3205, 3, 691, 345, 0, 3202, 3205, 3, 689, 344, 0, 3203, 3205, 5, 95, 0, 0, 3204, 3201, 1, 0, 0, 0, 3204, 3202, 1, 0, 0, 0, 3204, 3203, 1, 0, 0, 0, 3205, 3206, 1, 0, 0, 0, 3206, 3204, 1, 0, 0, 0, 3206, 3207, 1, 0, 0, 0, 3207, 682, 1, 0, 0, 0, 3208, 3214, 5, 96, 0, 0, 3209, 3213, 8, 4, 0, 0, 3210, 3211, 5, 96, 0, 0, 3211, 3213, 5, 96, 0, 0, 3212, 3209, 1, 0, 0, 0, 3212, 3210, 1, 0, 0, 0, 3213, 3216, 1, 0, 0, 0, 3214, 3212, 1, 0, 0, 0, 3214, 3215, 1, 0, 0, 0, 3215, 3217, 1, 0, 0, 0, 3216, 3214, 1, 0, 0, 0, 3217, 3218, 5, 96, 0, 0, 3218, 684, 1, 0, 0, 0, 3219, 3221, 3, 689, 344, 0, 3220, 3219, 1, 0, 0, 0, 3221, 3222, 1, 0, 0, 0, 3222, 3220, 1, 0, 0, 0, 3222, 3223, 1, 0, 0, 0, 3223, 3224, 1, 0, 0, 0, 3224, 3228, 5, 46, 0, 0, 3225, 3227, 3, 689, 344, 0, 3226, 3225, 1, 0, 0, 0, 3227, 3230, 1, 0, 0, 0, 3228, 3226, 1, 0, 0, 0, 3228, 3229, 1, 0, 0, 0, 3229, 3238, 1, 0, 0, 0, 3230, 3228, 1, 0, 0, 0, 3231, 3233, 5, 46, 0, 0, 3232, 3234, 3, 689, 344, 0, 3233, 3232, 1, 0, 0, 0, 3234, 3235, 1, 0, 0, 0, 3235, 3233, 1, 0, 0, 0, 3235, 3236, 1, 0, 0, 0, 3236, 3238, 1, 0, 0, 0, 3237, 3220, 1, 0, 0, 0, 3237, 3231, 1, 0, 0, 0, 3238, 686, 1, 0, 0, 0, 3239, 3241, 5, 69, 0, 0, 3240, 3242, 7, 5, 0, 0, 3241, 3240, 1, 0, 0, 0, 3241, 3242, 1, 0, 0, 0, 3242, 3244, 1, 0, 0, 0, 3243, 3245, 3, 689, 344, 0, 3244, 3243, 1, 0, 0, 0, 3245, 3246, 1, 0, 0, 0, 3246, 3244, 1, 0, 0, 0, 3246, 3247, 1, 0, 0, 0, 3247, 688, 1, 0, 0, 0, 3248, 3249, 7, 6, 0, 0, 3249, 690, 1, 0, 0, 0, 3250, 3251, 7, 7, 0, 0, 3251, 692, 1, 0, 0, 0, 3252, 3253, 5, 45, 0, 0, 3253, 3254, 5, 45, 0, 0, 3254, 3260, 1, 0, 0, 0, 3255, 3256, 5, 92, 0, 0, 3256, 3259, 5, 10, 0, 0, 3257, 3259, 8, 8, 0, 0, 3258, 3255, 1, 0, 0, 0, 3258, 3257, 1, 0, 0, 0, 3259, 3262, 1, 0, 0, 0, 3260, 3258, 1, 0, 0, 0, 3260, 3261, 1, 0, 0, 0, 3261, 3264, 1, 0, 0, 0, 3262, 3260, 1, 0, 0, 0, 3263, 3265, 5, 13, 0, 0, 3264, 3263, 1, 0, 0, 0, 3264, 3265, 1, 0, 0, 0, 3265, 3267, 1, 0, 0, 0, 3266, 3268, 5, 10, 0, 0, 3267, 3266, 1, 0, 0, 0, 3267, 3268, 1, 0, 0, 0, 3268, 3269, 1, 0, 0, 0, 3269, 3270, 6, 346, 0, 0, 3270, 694, 1, 0, 0, 0, 3271, 3272, 5, 47, 0, 0, 3272, 3273, 5, 42, 0, 0, 3273, 3274, 1, 0, 0, 0, 3274, 3279, 4, 347, 5, 0, 3275, 3278, 3, 695, 347, 0, 3276, 3278, 9, 0, 0, 0, 3277, 3275, 1, 0, 0, 0, 3277, 3276, 1, 0, 0, 0, 3278, 3281, 1, 0, 0, 0, 3279, 3280, 1, 0, 0, 0, 3279, 3277, 1, 0, 0, 0, 3280, 3286, 1, 0, 0, 0, 3281, 3279, 1, 0, 0, 0, 3282, 3283, 5, 42, 0, 0, 3283, 3287, 5, 47, 0, 0, 3284, 3285, 6, 347, 1, 0, 3285, 3287, 5, 0, 0, 1, 3286, 3282, 1, 0, 0, 0, 3286, 3284, 1, 0, 0, 0, 3287, 3288, 1, 0, 0, 0, 3288, 3289, 6, 347, 0, 0, 3289, 696, 1, 0, 0, 0, 3290, 3292, 7, 9, 0, 0, 3291, 3290, 1, 0, 0, 0, 3292, 3293, 1, 0, 0, 0, 3293, 3291, 1, 0, 0, 0, 3293, 3294, 1, 0, 0, 0, 3294, 3295, 1, 0, 0, 0, 3295, 3296, 6, 348, 0, 0, 3296, 698, 1, 0, 0, 0, 3297, 3298, 9, 0, 0, 0, 3298, 700, 1, 0, 0, 0, 50, 0, 1935, 2347, 2671, 2995, 3013, 3021, 3060, 3062, 3072, 3082, 3086, 3092, 3094, 3102, 3109, 3116, 3123, 3128, 3136, 3144, 3147, 3153, 3158, 3163, 3166, 3172, 3177, 3182, 3185, 3192, 3199, 3204, 3206, 3212, 3214, 3222, 3228, 3235, 3237, 3241, 3246, 3258, 3260, 3264, 3267, 3277, 3279, 3286, 3293, 2, 0, 1, 0, 1, 347, 0] \ No newline at end of file diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseLexer.py b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseLexer.py new file mode 100644 index 000000000..31af2393d --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseLexer.py @@ -0,0 +1,1925 @@ +# Generated from SqlBaseLexer.g4 by ANTLR 4.13.1 +from antlr4 import * +from io import StringIO +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO + + +def serializedATN(): + return [ + 4,0,346,3299,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7, + 5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12, + 2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19, + 7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25, + 2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,31,7,31,2,32, + 7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,7,38, + 2,39,7,39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,2,45, + 7,45,2,46,7,46,2,47,7,47,2,48,7,48,2,49,7,49,2,50,7,50,2,51,7,51, + 2,52,7,52,2,53,7,53,2,54,7,54,2,55,7,55,2,56,7,56,2,57,7,57,2,58, + 7,58,2,59,7,59,2,60,7,60,2,61,7,61,2,62,7,62,2,63,7,63,2,64,7,64, + 2,65,7,65,2,66,7,66,2,67,7,67,2,68,7,68,2,69,7,69,2,70,7,70,2,71, + 7,71,2,72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,2,77,7,77, + 2,78,7,78,2,79,7,79,2,80,7,80,2,81,7,81,2,82,7,82,2,83,7,83,2,84, + 7,84,2,85,7,85,2,86,7,86,2,87,7,87,2,88,7,88,2,89,7,89,2,90,7,90, + 2,91,7,91,2,92,7,92,2,93,7,93,2,94,7,94,2,95,7,95,2,96,7,96,2,97, + 7,97,2,98,7,98,2,99,7,99,2,100,7,100,2,101,7,101,2,102,7,102,2,103, + 7,103,2,104,7,104,2,105,7,105,2,106,7,106,2,107,7,107,2,108,7,108, + 2,109,7,109,2,110,7,110,2,111,7,111,2,112,7,112,2,113,7,113,2,114, + 7,114,2,115,7,115,2,116,7,116,2,117,7,117,2,118,7,118,2,119,7,119, + 2,120,7,120,2,121,7,121,2,122,7,122,2,123,7,123,2,124,7,124,2,125, + 7,125,2,126,7,126,2,127,7,127,2,128,7,128,2,129,7,129,2,130,7,130, + 2,131,7,131,2,132,7,132,2,133,7,133,2,134,7,134,2,135,7,135,2,136, + 7,136,2,137,7,137,2,138,7,138,2,139,7,139,2,140,7,140,2,141,7,141, + 2,142,7,142,2,143,7,143,2,144,7,144,2,145,7,145,2,146,7,146,2,147, + 7,147,2,148,7,148,2,149,7,149,2,150,7,150,2,151,7,151,2,152,7,152, + 2,153,7,153,2,154,7,154,2,155,7,155,2,156,7,156,2,157,7,157,2,158, + 7,158,2,159,7,159,2,160,7,160,2,161,7,161,2,162,7,162,2,163,7,163, + 2,164,7,164,2,165,7,165,2,166,7,166,2,167,7,167,2,168,7,168,2,169, + 7,169,2,170,7,170,2,171,7,171,2,172,7,172,2,173,7,173,2,174,7,174, + 2,175,7,175,2,176,7,176,2,177,7,177,2,178,7,178,2,179,7,179,2,180, + 7,180,2,181,7,181,2,182,7,182,2,183,7,183,2,184,7,184,2,185,7,185, + 2,186,7,186,2,187,7,187,2,188,7,188,2,189,7,189,2,190,7,190,2,191, + 7,191,2,192,7,192,2,193,7,193,2,194,7,194,2,195,7,195,2,196,7,196, + 2,197,7,197,2,198,7,198,2,199,7,199,2,200,7,200,2,201,7,201,2,202, + 7,202,2,203,7,203,2,204,7,204,2,205,7,205,2,206,7,206,2,207,7,207, + 2,208,7,208,2,209,7,209,2,210,7,210,2,211,7,211,2,212,7,212,2,213, + 7,213,2,214,7,214,2,215,7,215,2,216,7,216,2,217,7,217,2,218,7,218, + 2,219,7,219,2,220,7,220,2,221,7,221,2,222,7,222,2,223,7,223,2,224, + 7,224,2,225,7,225,2,226,7,226,2,227,7,227,2,228,7,228,2,229,7,229, + 2,230,7,230,2,231,7,231,2,232,7,232,2,233,7,233,2,234,7,234,2,235, + 7,235,2,236,7,236,2,237,7,237,2,238,7,238,2,239,7,239,2,240,7,240, + 2,241,7,241,2,242,7,242,2,243,7,243,2,244,7,244,2,245,7,245,2,246, + 7,246,2,247,7,247,2,248,7,248,2,249,7,249,2,250,7,250,2,251,7,251, + 2,252,7,252,2,253,7,253,2,254,7,254,2,255,7,255,2,256,7,256,2,257, + 7,257,2,258,7,258,2,259,7,259,2,260,7,260,2,261,7,261,2,262,7,262, + 2,263,7,263,2,264,7,264,2,265,7,265,2,266,7,266,2,267,7,267,2,268, + 7,268,2,269,7,269,2,270,7,270,2,271,7,271,2,272,7,272,2,273,7,273, + 2,274,7,274,2,275,7,275,2,276,7,276,2,277,7,277,2,278,7,278,2,279, + 7,279,2,280,7,280,2,281,7,281,2,282,7,282,2,283,7,283,2,284,7,284, + 2,285,7,285,2,286,7,286,2,287,7,287,2,288,7,288,2,289,7,289,2,290, + 7,290,2,291,7,291,2,292,7,292,2,293,7,293,2,294,7,294,2,295,7,295, + 2,296,7,296,2,297,7,297,2,298,7,298,2,299,7,299,2,300,7,300,2,301, + 7,301,2,302,7,302,2,303,7,303,2,304,7,304,2,305,7,305,2,306,7,306, + 2,307,7,307,2,308,7,308,2,309,7,309,2,310,7,310,2,311,7,311,2,312, + 7,312,2,313,7,313,2,314,7,314,2,315,7,315,2,316,7,316,2,317,7,317, + 2,318,7,318,2,319,7,319,2,320,7,320,2,321,7,321,2,322,7,322,2,323, + 7,323,2,324,7,324,2,325,7,325,2,326,7,326,2,327,7,327,2,328,7,328, + 2,329,7,329,2,330,7,330,2,331,7,331,2,332,7,332,2,333,7,333,2,334, + 7,334,2,335,7,335,2,336,7,336,2,337,7,337,2,338,7,338,2,339,7,339, + 2,340,7,340,2,341,7,341,2,342,7,342,2,343,7,343,2,344,7,344,2,345, + 7,345,2,346,7,346,2,347,7,347,2,348,7,348,2,349,7,349,1,0,1,0,1, + 1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,6,1,6,1,7,1,7,1,7,1,7,1, + 8,1,8,1,8,1,8,1,8,1,8,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,10,1, + 10,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,1, + 12,1,12,1,12,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,15,1, + 15,1,15,1,15,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1, + 17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,18,1,18,1, + 18,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,22,1,22,1, + 22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1, + 23,1,23,1,23,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,24,1,25,1, + 25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1, + 26,1,27,1,27,1,27,1,28,1,28,1,28,1,28,1,28,1,28,1,29,1,29,1,29,1, + 29,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,30,1,31,1,31,1,31,1, + 31,1,31,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,33,1,33,1,33,1, + 33,1,33,1,33,1,33,1,33,1,33,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1, + 35,1,35,1,35,1,35,1,35,1,35,1,36,1,36,1,36,1,36,1,36,1,36,1,37,1, + 37,1,37,1,37,1,37,1,37,1,37,1,37,1,38,1,38,1,38,1,38,1,38,1,38,1, + 38,1,38,1,38,1,38,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,40,1, + 40,1,40,1,40,1,40,1,40,1,40,1,40,1,41,1,41,1,41,1,41,1,41,1,41,1, + 41,1,41,1,41,1,41,1,41,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,43,1, + 43,1,43,1,43,1,43,1,43,1,43,1,43,1,44,1,44,1,44,1,44,1,44,1,44,1, + 44,1,44,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,46,1,46,1,46,1,46,1, + 46,1,46,1,46,1,46,1,47,1,47,1,47,1,47,1,47,1,47,1,47,1,47,1,47,1, + 47,1,47,1,47,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1,49,1,49,1, + 49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,50,1,50,1,50,1, + 50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,51,1,51,1,51,1,51,1,51,1, + 52,1,52,1,52,1,52,1,52,1,52,1,52,1,53,1,53,1,53,1,53,1,53,1,53,1, + 54,1,54,1,54,1,54,1,54,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1, + 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, + 57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1, + 58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1, + 58,1,58,1,58,1,58,1,58,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1, + 59,1,59,1,59,1,59,1,59,1,60,1,60,1,60,1,60,1,61,1,61,1,61,1,61,1, + 61,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,63,1,63,1, + 63,1,63,1,63,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,65,1, + 65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,66,1,66,1,66,1,66,1, + 66,1,66,1,66,1,66,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1, + 68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1, + 69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,70,1,70,1,70,1,70,1,70,1, + 70,1,70,1,70,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,72,1,72,1,72,1, + 72,1,72,1,72,1,72,1,72,1,72,1,72,1,73,1,73,1,73,1,73,1,73,1,74,1, + 74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,75,1,75,1,75,1,75,1,76,1, + 76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,77,1,77,1, + 77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,78,1,78,1,78,1,78,1,78,1, + 78,1,78,1,78,1,78,1,79,1,79,1,79,1,79,1,79,1,79,1,79,1,79,1,79,1, + 79,1,79,1,80,1,80,1,80,1,80,1,81,1,81,1,81,1,81,1,81,1,82,1,82,1, + 82,1,82,1,82,1,83,1,83,1,83,1,83,1,84,1,84,1,84,1,84,1,84,1,84,1, + 84,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,86,1,86,1,86,1,86,1, + 86,1,86,1,86,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,88,1, + 88,1,88,1,88,1,88,1,88,1,88,1,88,1,89,1,89,1,89,1,89,1,89,1,89,1, + 89,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,91,1,91,1,91,1,91,1, + 91,1,91,1,91,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,93,1, + 93,1,93,1,93,1,93,1,93,1,93,1,93,1,93,1,94,1,94,1,94,1,94,1,94,1, + 94,1,94,1,94,1,95,1,95,1,95,1,95,1,95,1,95,1,96,1,96,1,96,1,96,1, + 96,1,96,1,97,1,97,1,97,1,97,1,97,1,97,1,97,1,98,1,98,1,98,1,98,1, + 98,1,98,1,98,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1, + 99,1,100,1,100,1,100,1,100,1,100,1,100,1,101,1,101,1,101,1,101,1, + 101,1,101,1,101,1,101,1,101,1,101,1,102,1,102,1,102,1,102,1,103, + 1,103,1,103,1,103,1,103,1,103,1,103,1,103,1,104,1,104,1,104,1,104, + 1,104,1,104,1,104,1,105,1,105,1,105,1,105,1,105,1,105,1,105,1,105, + 1,105,1,105,1,106,1,106,1,106,1,106,1,106,1,107,1,107,1,107,1,107, + 1,107,1,108,1,108,1,108,1,108,1,108,1,108,1,108,1,108,1,108,1,109, + 1,109,1,109,1,109,1,109,1,109,1,109,1,109,1,109,1,109,1,110,1,110, + 1,110,1,110,1,110,1,110,1,110,1,110,1,110,1,110,1,111,1,111,1,111, + 1,111,1,111,1,111,1,111,1,112,1,112,1,112,1,112,1,112,1,112,1,113, + 1,113,1,113,1,113,1,113,1,113,1,114,1,114,1,114,1,114,1,114,1,114, + 1,114,1,114,1,114,1,115,1,115,1,115,1,115,1,115,1,115,1,115,1,116, + 1,116,1,116,1,116,1,116,1,117,1,117,1,117,1,117,1,117,1,117,1,118, + 1,118,1,118,1,119,1,119,1,119,1,119,1,119,1,119,1,119,1,120,1,120, + 1,120,1,120,1,120,1,120,1,120,1,121,1,121,1,121,1,122,1,122,1,122, + 1,122,1,122,1,122,1,122,1,122,1,123,1,123,1,123,1,123,1,123,1,123, + 1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,125,1,125,1,125, + 1,125,1,125,1,125,1,126,1,126,1,126,1,126,1,126,1,126,1,126,1,127, + 1,127,1,127,1,127,1,127,1,127,1,127,1,127,1,127,1,127,1,127,1,127, + 1,128,1,128,1,128,1,128,1,128,1,128,1,128,1,129,1,129,1,129,1,129, + 1,129,1,129,1,129,1,129,1,129,1,129,1,130,1,130,1,130,1,130,1,130, + 1,130,1,130,1,130,1,130,1,131,1,131,1,131,1,131,1,131,1,132,1,132, + 1,132,1,133,1,133,1,133,1,133,1,133,1,133,1,134,1,134,1,134,1,134, + 1,134,1,135,1,135,1,135,1,135,1,135,1,136,1,136,1,136,1,136,1,136, + 1,137,1,137,1,137,1,137,1,137,1,137,1,137,1,137,1,138,1,138,1,138, + 1,138,1,138,1,139,1,139,1,139,1,139,1,139,1,139,1,139,1,139,1,140, + 1,140,1,140,1,140,1,140,1,141,1,141,1,141,1,141,1,141,1,142,1,142, + 1,142,1,142,1,142,1,142,1,143,1,143,1,143,1,143,1,143,1,143,1,144, + 1,144,1,144,1,144,1,144,1,144,1,145,1,145,1,145,1,145,1,145,1,146, + 1,146,1,146,1,146,1,146,1,147,1,147,1,147,1,147,1,147,1,147,1,148, + 1,148,1,148,1,148,1,148,1,148,1,148,1,148,1,148,1,149,1,149,1,149, + 1,149,1,149,1,150,1,150,1,150,1,150,1,150,1,150,1,151,1,151,1,151, + 1,151,1,151,1,151,1,151,1,151,1,152,1,152,1,152,1,152,1,152,1,152, + 1,153,1,153,1,153,1,153,1,154,1,154,1,154,1,154,1,154,1,154,1,154, + 1,154,1,155,1,155,1,155,1,155,1,155,1,155,1,156,1,156,1,156,1,156, + 1,156,1,156,1,156,1,156,1,156,1,156,1,156,1,156,1,157,1,157,1,157, + 1,157,1,157,1,157,1,157,1,157,1,157,1,157,1,157,1,157,1,157,1,158, + 1,158,1,158,1,158,1,158,1,158,1,158,1,158,1,158,1,158,1,158,1,158, + 1,159,1,159,1,159,1,159,1,159,1,159,1,159,1,159,1,159,1,159,1,159, + 1,159,1,159,1,160,1,160,1,160,1,160,1,160,1,160,1,160,1,161,1,161, + 1,161,1,161,1,161,1,161,1,161,1,161,1,162,1,162,1,162,1,162,1,162, + 1,162,1,163,1,163,1,163,1,163,1,163,1,163,1,163,1,164,1,164,1,164, + 1,164,1,164,1,165,1,165,1,165,1,165,1,165,1,165,1,165,1,165,1,165, + 1,165,1,166,1,166,1,166,1,166,1,166,1,166,1,166,1,166,1,166,1,166, + 1,166,1,167,1,167,1,167,1,167,1,167,1,167,1,167,1,167,1,167,1,167, + 1,167,1,168,1,168,1,168,1,168,1,168,1,168,1,168,1,168,1,168,1,168, + 1,168,1,168,1,169,1,169,1,169,1,169,1,169,1,169,1,169,1,169,1,170, + 1,170,1,170,1,171,1,171,1,171,1,171,3,171,1936,8,171,1,172,1,172, + 1,172,1,172,1,172,1,173,1,173,1,173,1,173,1,173,1,173,1,174,1,174, + 1,174,1,175,1,175,1,175,1,175,1,175,1,175,1,175,1,176,1,176,1,176, + 1,177,1,177,1,177,1,177,1,177,1,178,1,178,1,178,1,178,1,178,1,178, + 1,178,1,179,1,179,1,179,1,179,1,179,1,179,1,179,1,179,1,180,1,180, + 1,180,1,181,1,181,1,181,1,181,1,181,1,181,1,182,1,182,1,182,1,182, + 1,183,1,183,1,183,1,183,1,183,1,183,1,184,1,184,1,184,1,184,1,184, + 1,184,1,184,1,184,1,184,1,184,1,184,1,184,1,184,1,185,1,185,1,185, + 1,185,1,185,1,186,1,186,1,186,1,186,1,186,1,186,1,186,1,186,1,186, + 1,187,1,187,1,187,1,187,1,187,1,187,1,187,1,187,1,188,1,188,1,188, + 1,188,1,188,1,188,1,188,1,188,1,188,1,188,1,189,1,189,1,189,1,189, + 1,189,1,189,1,189,1,189,1,189,1,189,1,190,1,190,1,190,1,190,1,190, + 1,190,1,190,1,190,1,190,1,190,1,190,1,190,1,191,1,191,1,191,1,191, + 1,191,1,191,1,191,1,191,1,191,1,191,1,191,1,192,1,192,1,192,1,192, + 1,192,1,192,1,192,1,192,1,192,1,192,1,192,1,192,1,192,1,192,1,192, + 1,192,1,193,1,193,1,193,1,193,1,193,1,193,1,193,1,193,1,193,1,193, + 1,193,1,193,1,193,1,193,1,193,1,193,1,194,1,194,1,194,1,194,1,194, + 1,194,1,194,1,194,1,195,1,195,1,195,1,195,1,195,1,195,1,196,1,196, + 1,196,1,196,1,196,1,196,1,196,1,196,1,197,1,197,1,197,1,197,1,197, + 1,197,1,197,1,197,1,197,1,198,1,198,1,198,1,198,1,198,1,198,1,198, + 1,198,1,198,1,198,1,199,1,199,1,199,1,199,1,199,1,199,1,199,1,199, + 1,200,1,200,1,200,1,200,1,200,1,200,1,200,1,200,1,200,1,200,1,200, + 1,201,1,201,1,201,1,201,1,201,1,201,1,201,1,201,1,201,1,201,1,201, + 1,202,1,202,1,202,1,202,1,202,1,202,1,203,1,203,1,203,1,203,1,203, + 1,203,1,203,1,203,1,204,1,204,1,204,1,204,1,204,1,204,1,205,1,205, + 1,205,1,205,1,205,1,205,1,206,1,206,1,206,1,206,1,206,1,206,1,206, + 1,206,1,206,1,206,1,206,1,206,1,206,1,207,1,207,1,207,1,207,1,207, + 1,207,1,207,1,207,1,207,1,207,1,207,1,207,1,207,1,208,1,208,1,208, + 1,208,1,208,1,208,1,208,1,208,1,209,1,209,1,209,1,209,1,209,1,209, + 1,209,1,210,1,210,1,210,1,210,1,210,1,210,1,210,1,210,1,210,1,210, + 1,210,1,211,1,211,1,211,1,211,1,211,1,211,1,211,1,211,1,212,1,212, + 1,212,1,212,1,212,1,212,1,212,1,213,1,213,1,213,1,213,1,213,1,213, + 1,213,1,214,1,214,1,214,1,214,1,214,1,214,1,214,1,214,1,214,1,214, + 1,214,1,215,1,215,1,215,1,215,1,215,1,215,1,215,1,215,1,216,1,216, + 1,216,1,216,1,216,1,216,1,217,1,217,1,217,1,217,1,217,1,217,1,217, + 1,217,1,218,1,218,1,218,1,218,1,218,1,218,1,218,1,218,1,218,1,219, + 1,219,1,219,1,219,1,219,1,219,1,219,1,220,1,220,1,220,1,220,1,220, + 1,220,1,221,1,221,1,221,1,221,1,221,1,221,1,221,1,221,1,221,1,221, + 1,221,3,221,2348,8,221,1,222,1,222,1,222,1,222,1,222,1,223,1,223, + 1,223,1,223,1,223,1,223,1,224,1,224,1,224,1,224,1,224,1,224,1,224, + 1,224,1,224,1,225,1,225,1,225,1,225,1,225,1,225,1,225,1,226,1,226, + 1,226,1,226,1,227,1,227,1,227,1,227,1,227,1,228,1,228,1,228,1,228, + 1,228,1,228,1,228,1,229,1,229,1,229,1,229,1,229,1,229,1,229,1,229, + 1,230,1,230,1,230,1,230,1,230,1,230,1,230,1,231,1,231,1,231,1,231, + 1,231,1,231,1,231,1,231,1,232,1,232,1,232,1,232,1,232,1,232,1,232, + 1,233,1,233,1,233,1,233,1,233,1,234,1,234,1,234,1,234,1,234,1,234, + 1,234,1,234,1,234,1,234,1,235,1,235,1,235,1,235,1,235,1,235,1,236, + 1,236,1,236,1,236,1,236,1,236,1,236,1,236,1,236,1,236,1,236,1,236, + 1,236,1,236,1,236,1,236,1,237,1,237,1,237,1,237,1,237,1,237,1,237, + 1,237,1,237,1,237,1,237,1,237,1,237,1,238,1,238,1,238,1,238,1,239, + 1,239,1,239,1,239,1,239,1,239,1,240,1,240,1,240,1,240,1,240,1,241, + 1,241,1,241,1,241,1,241,1,242,1,242,1,242,1,242,1,242,1,242,1,242, + 1,243,1,243,1,243,1,243,1,243,1,244,1,244,1,244,1,244,1,244,1,245, + 1,245,1,245,1,245,1,245,1,245,1,245,1,246,1,246,1,246,1,246,1,246, + 1,246,1,246,1,247,1,247,1,247,1,247,1,247,1,247,1,248,1,248,1,248, + 1,248,1,248,1,248,1,248,1,248,1,248,1,248,1,248,1,249,1,249,1,249, + 1,249,1,249,1,249,1,249,1,250,1,250,1,250,1,250,1,250,1,250,1,250, + 1,250,1,250,1,251,1,251,1,251,1,251,1,251,1,251,1,251,1,252,1,252, + 1,252,1,252,1,252,1,252,1,252,1,253,1,253,1,253,1,253,1,253,1,253, + 1,253,1,253,1,253,1,253,1,254,1,254,1,254,1,254,1,254,1,255,1,255, + 1,255,1,255,1,255,1,255,1,255,1,255,1,255,1,255,1,255,1,255,1,256, + 1,256,1,256,1,256,1,256,1,256,1,256,1,256,1,256,1,256,1,256,1,256, + 1,256,1,256,1,256,1,257,1,257,1,257,1,257,1,257,1,257,1,258,1,258, + 1,258,1,258,1,258,1,258,1,258,1,259,1,259,1,259,1,259,1,259,1,259, + 1,259,1,259,1,259,1,259,1,259,1,259,1,260,1,260,1,260,1,260,1,260, + 1,260,1,260,1,261,1,261,1,261,1,261,1,261,1,261,1,261,1,261,1,261, + 1,261,1,261,1,261,1,261,1,261,1,262,1,262,1,262,1,262,1,262,1,262, + 1,262,1,262,1,262,1,262,1,262,1,262,1,262,3,262,2672,8,262,1,263, + 1,263,1,263,1,263,1,263,1,263,1,263,1,263,1,263,1,263,1,263,1,264, + 1,264,1,264,1,264,1,264,1,265,1,265,1,265,1,265,1,265,1,266,1,266, + 1,266,1,266,1,266,1,266,1,266,1,266,1,266,1,266,1,267,1,267,1,267, + 1,267,1,267,1,267,1,267,1,267,1,267,1,267,1,267,1,267,1,267,1,268, + 1,268,1,268,1,268,1,268,1,268,1,268,1,268,1,268,1,268,1,268,1,268, + 1,268,1,268,1,269,1,269,1,269,1,270,1,270,1,270,1,270,1,270,1,270, + 1,271,1,271,1,271,1,271,1,271,1,271,1,271,1,271,1,271,1,272,1,272, + 1,272,1,272,1,272,1,272,1,272,1,272,1,272,1,272,1,272,1,272,1,273, + 1,273,1,273,1,273,1,273,1,273,1,273,1,273,1,273,1,273,1,273,1,273, + 1,273,1,274,1,274,1,274,1,274,1,274,1,274,1,274,1,274,1,274,1,274, + 1,275,1,275,1,275,1,275,1,275,1,276,1,276,1,276,1,276,1,276,1,277, + 1,277,1,277,1,277,1,277,1,277,1,277,1,277,1,277,1,278,1,278,1,278, + 1,278,1,278,1,278,1,278,1,278,1,278,1,279,1,279,1,279,1,279,1,279, + 1,280,1,280,1,280,1,280,1,280,1,280,1,280,1,280,1,280,1,280,1,281, + 1,281,1,281,1,281,1,281,1,281,1,281,1,281,1,281,1,281,1,282,1,282, + 1,282,1,282,1,282,1,282,1,282,1,282,1,283,1,283,1,283,1,283,1,283, + 1,283,1,284,1,284,1,284,1,284,1,284,1,284,1,284,1,285,1,285,1,285, + 1,285,1,285,1,285,1,285,1,285,1,286,1,286,1,286,1,286,1,286,1,286, + 1,286,1,287,1,287,1,287,1,287,1,287,1,287,1,287,1,287,1,288,1,288, + 1,288,1,288,1,288,1,288,1,289,1,289,1,289,1,289,1,289,1,289,1,289, + 1,290,1,290,1,290,1,290,1,291,1,291,1,291,1,291,1,291,1,292,1,292, + 1,292,1,292,1,292,1,292,1,293,1,293,1,293,1,293,1,293,1,293,1,293, + 1,294,1,294,1,294,1,294,1,294,1,294,1,294,1,294,1,295,1,295,1,295, + 1,295,1,295,1,296,1,296,1,296,1,296,1,296,1,296,1,297,1,297,1,297, + 1,297,1,297,1,298,1,298,1,298,1,298,1,298,1,298,1,299,1,299,1,299, + 1,299,1,299,1,300,1,300,1,300,1,300,1,300,1,300,1,301,1,301,1,301, + 1,301,1,301,1,301,1,301,1,302,1,302,1,302,1,302,1,302,1,303,1,303, + 1,303,1,303,1,303,1,303,1,303,1,304,1,304,1,304,1,304,1,304,1,305, + 1,305,1,305,1,305,1,305,1,305,1,306,1,306,1,306,1,306,1,306,1,307, + 1,307,1,307,3,307,2996,8,307,1,308,1,308,1,308,1,308,1,309,1,309, + 1,309,1,310,1,310,1,310,1,311,1,311,1,312,1,312,1,312,1,312,3,312, + 3014,8,312,1,313,1,313,1,314,1,314,1,314,1,314,3,314,3022,8,314, + 1,315,1,315,1,316,1,316,1,317,1,317,1,318,1,318,1,319,1,319,1,320, + 1,320,1,321,1,321,1,322,1,322,1,323,1,323,1,323,1,324,1,324,1,325, + 1,325,1,326,1,326,1,326,1,327,1,327,1,327,1,327,1,328,1,328,1,328, + 1,329,1,329,1,329,1,329,5,329,3061,8,329,10,329,12,329,3064,9,329, + 1,329,1,329,1,329,1,329,1,329,5,329,3071,8,329,10,329,12,329,3074, + 9,329,1,329,1,329,1,329,1,329,1,329,5,329,3081,8,329,10,329,12,329, + 3084,9,329,1,329,3,329,3087,8,329,1,330,1,330,1,330,1,330,5,330, + 3093,8,330,10,330,12,330,3096,9,330,1,330,1,330,1,331,4,331,3101, + 8,331,11,331,12,331,3102,1,331,1,331,1,332,4,332,3108,8,332,11,332, + 12,332,3109,1,332,1,332,1,333,4,333,3115,8,333,11,333,12,333,3116, + 1,333,1,333,1,334,4,334,3122,8,334,11,334,12,334,3123,1,335,4,335, + 3127,8,335,11,335,12,335,3128,1,335,1,335,1,335,1,335,1,335,1,335, + 3,335,3137,8,335,1,336,1,336,1,336,1,337,4,337,3143,8,337,11,337, + 12,337,3144,1,337,3,337,3148,8,337,1,337,1,337,1,337,1,337,3,337, + 3154,8,337,1,337,1,337,1,337,3,337,3159,8,337,1,338,4,338,3162,8, + 338,11,338,12,338,3163,1,338,3,338,3167,8,338,1,338,1,338,1,338, + 1,338,3,338,3173,8,338,1,338,1,338,1,338,3,338,3178,8,338,1,339, + 4,339,3181,8,339,11,339,12,339,3182,1,339,3,339,3186,8,339,1,339, + 1,339,1,339,1,339,1,339,3,339,3193,8,339,1,339,1,339,1,339,1,339, + 1,339,3,339,3200,8,339,1,340,1,340,1,340,4,340,3205,8,340,11,340, + 12,340,3206,1,341,1,341,1,341,1,341,5,341,3213,8,341,10,341,12,341, + 3216,9,341,1,341,1,341,1,342,4,342,3221,8,342,11,342,12,342,3222, + 1,342,1,342,5,342,3227,8,342,10,342,12,342,3230,9,342,1,342,1,342, + 4,342,3234,8,342,11,342,12,342,3235,3,342,3238,8,342,1,343,1,343, + 3,343,3242,8,343,1,343,4,343,3245,8,343,11,343,12,343,3246,1,344, + 1,344,1,345,1,345,1,346,1,346,1,346,1,346,1,346,1,346,5,346,3259, + 8,346,10,346,12,346,3262,9,346,1,346,3,346,3265,8,346,1,346,3,346, + 3268,8,346,1,346,1,346,1,347,1,347,1,347,1,347,1,347,1,347,5,347, + 3278,8,347,10,347,12,347,3281,9,347,1,347,1,347,1,347,1,347,3,347, + 3287,8,347,1,347,1,347,1,348,4,348,3292,8,348,11,348,12,348,3293, + 1,348,1,348,1,349,1,349,1,3279,0,350,1,1,3,2,5,3,7,4,9,5,11,6,13, + 7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18, + 37,19,39,20,41,21,43,22,45,23,47,24,49,25,51,26,53,27,55,28,57,29, + 59,30,61,31,63,32,65,33,67,34,69,35,71,36,73,37,75,38,77,39,79,40, + 81,41,83,42,85,43,87,44,89,45,91,46,93,47,95,48,97,49,99,50,101, + 51,103,52,105,53,107,54,109,55,111,56,113,57,115,58,117,59,119,60, + 121,61,123,62,125,63,127,64,129,65,131,66,133,67,135,68,137,69,139, + 70,141,71,143,72,145,73,147,74,149,75,151,76,153,77,155,78,157,79, + 159,80,161,81,163,82,165,83,167,84,169,85,171,86,173,87,175,88,177, + 89,179,90,181,91,183,92,185,93,187,94,189,95,191,96,193,97,195,98, + 197,99,199,100,201,101,203,102,205,103,207,104,209,105,211,106,213, + 107,215,108,217,109,219,110,221,111,223,112,225,113,227,114,229, + 115,231,116,233,117,235,118,237,119,239,120,241,121,243,122,245, + 123,247,124,249,125,251,126,253,127,255,128,257,129,259,130,261, + 131,263,132,265,133,267,134,269,135,271,136,273,137,275,138,277, + 139,279,140,281,141,283,142,285,143,287,144,289,145,291,146,293, + 147,295,148,297,149,299,150,301,151,303,152,305,153,307,154,309, + 155,311,156,313,157,315,158,317,159,319,160,321,161,323,162,325, + 163,327,164,329,165,331,166,333,167,335,168,337,169,339,170,341, + 171,343,172,345,173,347,174,349,175,351,176,353,177,355,178,357, + 179,359,180,361,181,363,182,365,183,367,184,369,185,371,186,373, + 187,375,188,377,189,379,190,381,191,383,192,385,193,387,194,389, + 195,391,196,393,197,395,198,397,199,399,200,401,201,403,202,405, + 203,407,204,409,205,411,206,413,207,415,208,417,209,419,210,421, + 211,423,212,425,213,427,214,429,215,431,216,433,217,435,218,437, + 219,439,220,441,221,443,222,445,223,447,224,449,225,451,226,453, + 227,455,228,457,229,459,230,461,231,463,232,465,233,467,234,469, + 235,471,236,473,237,475,238,477,239,479,240,481,241,483,242,485, + 243,487,244,489,245,491,246,493,247,495,248,497,249,499,250,501, + 251,503,252,505,253,507,254,509,255,511,256,513,257,515,258,517, + 259,519,260,521,261,523,262,525,263,527,264,529,265,531,266,533, + 267,535,268,537,269,539,270,541,271,543,272,545,273,547,274,549, + 275,551,276,553,277,555,278,557,279,559,280,561,281,563,282,565, + 283,567,284,569,285,571,286,573,287,575,288,577,289,579,290,581, + 291,583,292,585,293,587,294,589,295,591,296,593,297,595,298,597, + 299,599,300,601,301,603,302,605,303,607,304,609,305,611,306,613, + 307,615,308,617,309,619,310,621,311,623,312,625,313,627,314,629, + 315,631,316,633,317,635,318,637,319,639,320,641,321,643,322,645, + 323,647,324,649,325,651,326,653,327,655,328,657,329,659,330,661, + 331,663,332,665,333,667,334,669,335,671,336,673,337,675,338,677, + 339,679,340,681,341,683,342,685,0,687,0,689,0,691,0,693,343,695, + 344,697,345,699,346,1,0,10,2,0,39,39,92,92,1,0,39,39,1,0,34,34,2, + 0,34,34,92,92,1,0,96,96,2,0,43,43,45,45,1,0,48,57,1,0,65,90,2,0, + 10,10,13,13,3,0,9,10,13,13,32,32,3345,0,1,1,0,0,0,0,3,1,0,0,0,0, + 5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15, + 1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25, + 1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35, + 1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45, + 1,0,0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,55, + 1,0,0,0,0,57,1,0,0,0,0,59,1,0,0,0,0,61,1,0,0,0,0,63,1,0,0,0,0,65, + 1,0,0,0,0,67,1,0,0,0,0,69,1,0,0,0,0,71,1,0,0,0,0,73,1,0,0,0,0,75, + 1,0,0,0,0,77,1,0,0,0,0,79,1,0,0,0,0,81,1,0,0,0,0,83,1,0,0,0,0,85, + 1,0,0,0,0,87,1,0,0,0,0,89,1,0,0,0,0,91,1,0,0,0,0,93,1,0,0,0,0,95, + 1,0,0,0,0,97,1,0,0,0,0,99,1,0,0,0,0,101,1,0,0,0,0,103,1,0,0,0,0, + 105,1,0,0,0,0,107,1,0,0,0,0,109,1,0,0,0,0,111,1,0,0,0,0,113,1,0, + 0,0,0,115,1,0,0,0,0,117,1,0,0,0,0,119,1,0,0,0,0,121,1,0,0,0,0,123, + 1,0,0,0,0,125,1,0,0,0,0,127,1,0,0,0,0,129,1,0,0,0,0,131,1,0,0,0, + 0,133,1,0,0,0,0,135,1,0,0,0,0,137,1,0,0,0,0,139,1,0,0,0,0,141,1, + 0,0,0,0,143,1,0,0,0,0,145,1,0,0,0,0,147,1,0,0,0,0,149,1,0,0,0,0, + 151,1,0,0,0,0,153,1,0,0,0,0,155,1,0,0,0,0,157,1,0,0,0,0,159,1,0, + 0,0,0,161,1,0,0,0,0,163,1,0,0,0,0,165,1,0,0,0,0,167,1,0,0,0,0,169, + 1,0,0,0,0,171,1,0,0,0,0,173,1,0,0,0,0,175,1,0,0,0,0,177,1,0,0,0, + 0,179,1,0,0,0,0,181,1,0,0,0,0,183,1,0,0,0,0,185,1,0,0,0,0,187,1, + 0,0,0,0,189,1,0,0,0,0,191,1,0,0,0,0,193,1,0,0,0,0,195,1,0,0,0,0, + 197,1,0,0,0,0,199,1,0,0,0,0,201,1,0,0,0,0,203,1,0,0,0,0,205,1,0, + 0,0,0,207,1,0,0,0,0,209,1,0,0,0,0,211,1,0,0,0,0,213,1,0,0,0,0,215, + 1,0,0,0,0,217,1,0,0,0,0,219,1,0,0,0,0,221,1,0,0,0,0,223,1,0,0,0, + 0,225,1,0,0,0,0,227,1,0,0,0,0,229,1,0,0,0,0,231,1,0,0,0,0,233,1, + 0,0,0,0,235,1,0,0,0,0,237,1,0,0,0,0,239,1,0,0,0,0,241,1,0,0,0,0, + 243,1,0,0,0,0,245,1,0,0,0,0,247,1,0,0,0,0,249,1,0,0,0,0,251,1,0, + 0,0,0,253,1,0,0,0,0,255,1,0,0,0,0,257,1,0,0,0,0,259,1,0,0,0,0,261, + 1,0,0,0,0,263,1,0,0,0,0,265,1,0,0,0,0,267,1,0,0,0,0,269,1,0,0,0, + 0,271,1,0,0,0,0,273,1,0,0,0,0,275,1,0,0,0,0,277,1,0,0,0,0,279,1, + 0,0,0,0,281,1,0,0,0,0,283,1,0,0,0,0,285,1,0,0,0,0,287,1,0,0,0,0, + 289,1,0,0,0,0,291,1,0,0,0,0,293,1,0,0,0,0,295,1,0,0,0,0,297,1,0, + 0,0,0,299,1,0,0,0,0,301,1,0,0,0,0,303,1,0,0,0,0,305,1,0,0,0,0,307, + 1,0,0,0,0,309,1,0,0,0,0,311,1,0,0,0,0,313,1,0,0,0,0,315,1,0,0,0, + 0,317,1,0,0,0,0,319,1,0,0,0,0,321,1,0,0,0,0,323,1,0,0,0,0,325,1, + 0,0,0,0,327,1,0,0,0,0,329,1,0,0,0,0,331,1,0,0,0,0,333,1,0,0,0,0, + 335,1,0,0,0,0,337,1,0,0,0,0,339,1,0,0,0,0,341,1,0,0,0,0,343,1,0, + 0,0,0,345,1,0,0,0,0,347,1,0,0,0,0,349,1,0,0,0,0,351,1,0,0,0,0,353, + 1,0,0,0,0,355,1,0,0,0,0,357,1,0,0,0,0,359,1,0,0,0,0,361,1,0,0,0, + 0,363,1,0,0,0,0,365,1,0,0,0,0,367,1,0,0,0,0,369,1,0,0,0,0,371,1, + 0,0,0,0,373,1,0,0,0,0,375,1,0,0,0,0,377,1,0,0,0,0,379,1,0,0,0,0, + 381,1,0,0,0,0,383,1,0,0,0,0,385,1,0,0,0,0,387,1,0,0,0,0,389,1,0, + 0,0,0,391,1,0,0,0,0,393,1,0,0,0,0,395,1,0,0,0,0,397,1,0,0,0,0,399, + 1,0,0,0,0,401,1,0,0,0,0,403,1,0,0,0,0,405,1,0,0,0,0,407,1,0,0,0, + 0,409,1,0,0,0,0,411,1,0,0,0,0,413,1,0,0,0,0,415,1,0,0,0,0,417,1, + 0,0,0,0,419,1,0,0,0,0,421,1,0,0,0,0,423,1,0,0,0,0,425,1,0,0,0,0, + 427,1,0,0,0,0,429,1,0,0,0,0,431,1,0,0,0,0,433,1,0,0,0,0,435,1,0, + 0,0,0,437,1,0,0,0,0,439,1,0,0,0,0,441,1,0,0,0,0,443,1,0,0,0,0,445, + 1,0,0,0,0,447,1,0,0,0,0,449,1,0,0,0,0,451,1,0,0,0,0,453,1,0,0,0, + 0,455,1,0,0,0,0,457,1,0,0,0,0,459,1,0,0,0,0,461,1,0,0,0,0,463,1, + 0,0,0,0,465,1,0,0,0,0,467,1,0,0,0,0,469,1,0,0,0,0,471,1,0,0,0,0, + 473,1,0,0,0,0,475,1,0,0,0,0,477,1,0,0,0,0,479,1,0,0,0,0,481,1,0, + 0,0,0,483,1,0,0,0,0,485,1,0,0,0,0,487,1,0,0,0,0,489,1,0,0,0,0,491, + 1,0,0,0,0,493,1,0,0,0,0,495,1,0,0,0,0,497,1,0,0,0,0,499,1,0,0,0, + 0,501,1,0,0,0,0,503,1,0,0,0,0,505,1,0,0,0,0,507,1,0,0,0,0,509,1, + 0,0,0,0,511,1,0,0,0,0,513,1,0,0,0,0,515,1,0,0,0,0,517,1,0,0,0,0, + 519,1,0,0,0,0,521,1,0,0,0,0,523,1,0,0,0,0,525,1,0,0,0,0,527,1,0, + 0,0,0,529,1,0,0,0,0,531,1,0,0,0,0,533,1,0,0,0,0,535,1,0,0,0,0,537, + 1,0,0,0,0,539,1,0,0,0,0,541,1,0,0,0,0,543,1,0,0,0,0,545,1,0,0,0, + 0,547,1,0,0,0,0,549,1,0,0,0,0,551,1,0,0,0,0,553,1,0,0,0,0,555,1, + 0,0,0,0,557,1,0,0,0,0,559,1,0,0,0,0,561,1,0,0,0,0,563,1,0,0,0,0, + 565,1,0,0,0,0,567,1,0,0,0,0,569,1,0,0,0,0,571,1,0,0,0,0,573,1,0, + 0,0,0,575,1,0,0,0,0,577,1,0,0,0,0,579,1,0,0,0,0,581,1,0,0,0,0,583, + 1,0,0,0,0,585,1,0,0,0,0,587,1,0,0,0,0,589,1,0,0,0,0,591,1,0,0,0, + 0,593,1,0,0,0,0,595,1,0,0,0,0,597,1,0,0,0,0,599,1,0,0,0,0,601,1, + 0,0,0,0,603,1,0,0,0,0,605,1,0,0,0,0,607,1,0,0,0,0,609,1,0,0,0,0, + 611,1,0,0,0,0,613,1,0,0,0,0,615,1,0,0,0,0,617,1,0,0,0,0,619,1,0, + 0,0,0,621,1,0,0,0,0,623,1,0,0,0,0,625,1,0,0,0,0,627,1,0,0,0,0,629, + 1,0,0,0,0,631,1,0,0,0,0,633,1,0,0,0,0,635,1,0,0,0,0,637,1,0,0,0, + 0,639,1,0,0,0,0,641,1,0,0,0,0,643,1,0,0,0,0,645,1,0,0,0,0,647,1, + 0,0,0,0,649,1,0,0,0,0,651,1,0,0,0,0,653,1,0,0,0,0,655,1,0,0,0,0, + 657,1,0,0,0,0,659,1,0,0,0,0,661,1,0,0,0,0,663,1,0,0,0,0,665,1,0, + 0,0,0,667,1,0,0,0,0,669,1,0,0,0,0,671,1,0,0,0,0,673,1,0,0,0,0,675, + 1,0,0,0,0,677,1,0,0,0,0,679,1,0,0,0,0,681,1,0,0,0,0,683,1,0,0,0, + 0,693,1,0,0,0,0,695,1,0,0,0,0,697,1,0,0,0,0,699,1,0,0,0,1,701,1, + 0,0,0,3,703,1,0,0,0,5,705,1,0,0,0,7,707,1,0,0,0,9,709,1,0,0,0,11, + 711,1,0,0,0,13,713,1,0,0,0,15,715,1,0,0,0,17,719,1,0,0,0,19,725, + 1,0,0,0,21,729,1,0,0,0,23,735,1,0,0,0,25,742,1,0,0,0,27,750,1,0, + 0,0,29,754,1,0,0,0,31,759,1,0,0,0,33,763,1,0,0,0,35,773,1,0,0,0, + 37,781,1,0,0,0,39,787,1,0,0,0,41,790,1,0,0,0,43,794,1,0,0,0,45,797, + 1,0,0,0,47,811,1,0,0,0,49,819,1,0,0,0,51,824,1,0,0,0,53,831,1,0, + 0,0,55,839,1,0,0,0,57,842,1,0,0,0,59,848,1,0,0,0,61,856,1,0,0,0, + 63,861,1,0,0,0,65,866,1,0,0,0,67,874,1,0,0,0,69,883,1,0,0,0,71,890, + 1,0,0,0,73,896,1,0,0,0,75,902,1,0,0,0,77,910,1,0,0,0,79,920,1,0, + 0,0,81,928,1,0,0,0,83,936,1,0,0,0,85,947,1,0,0,0,87,954,1,0,0,0, + 89,962,1,0,0,0,91,970,1,0,0,0,93,977,1,0,0,0,95,985,1,0,0,0,97,997, + 1,0,0,0,99,1005,1,0,0,0,101,1017,1,0,0,0,103,1028,1,0,0,0,105,1033, + 1,0,0,0,107,1040,1,0,0,0,109,1046,1,0,0,0,111,1051,1,0,0,0,113,1059, + 1,0,0,0,115,1072,1,0,0,0,117,1085,1,0,0,0,119,1103,1,0,0,0,121,1116, + 1,0,0,0,123,1120,1,0,0,0,125,1125,1,0,0,0,127,1135,1,0,0,0,129,1140, + 1,0,0,0,131,1149,1,0,0,0,133,1159,1,0,0,0,135,1167,1,0,0,0,137,1176, + 1,0,0,0,139,1189,1,0,0,0,141,1197,1,0,0,0,143,1205,1,0,0,0,145,1212, + 1,0,0,0,147,1222,1,0,0,0,149,1227,1,0,0,0,151,1236,1,0,0,0,153,1240, + 1,0,0,0,155,1252,1,0,0,0,157,1262,1,0,0,0,159,1271,1,0,0,0,161,1282, + 1,0,0,0,163,1286,1,0,0,0,165,1291,1,0,0,0,167,1296,1,0,0,0,169,1300, + 1,0,0,0,171,1307,1,0,0,0,173,1315,1,0,0,0,175,1322,1,0,0,0,177,1331, + 1,0,0,0,179,1339,1,0,0,0,181,1346,1,0,0,0,183,1354,1,0,0,0,185,1361, + 1,0,0,0,187,1370,1,0,0,0,189,1379,1,0,0,0,191,1387,1,0,0,0,193,1393, + 1,0,0,0,195,1399,1,0,0,0,197,1406,1,0,0,0,199,1413,1,0,0,0,201,1424, + 1,0,0,0,203,1430,1,0,0,0,205,1440,1,0,0,0,207,1444,1,0,0,0,209,1452, + 1,0,0,0,211,1459,1,0,0,0,213,1469,1,0,0,0,215,1474,1,0,0,0,217,1479, + 1,0,0,0,219,1488,1,0,0,0,221,1498,1,0,0,0,223,1508,1,0,0,0,225,1515, + 1,0,0,0,227,1521,1,0,0,0,229,1527,1,0,0,0,231,1536,1,0,0,0,233,1543, + 1,0,0,0,235,1548,1,0,0,0,237,1554,1,0,0,0,239,1557,1,0,0,0,241,1564, + 1,0,0,0,243,1571,1,0,0,0,245,1574,1,0,0,0,247,1582,1,0,0,0,249,1588, + 1,0,0,0,251,1596,1,0,0,0,253,1602,1,0,0,0,255,1609,1,0,0,0,257,1621, + 1,0,0,0,259,1628,1,0,0,0,261,1638,1,0,0,0,263,1647,1,0,0,0,265,1652, + 1,0,0,0,267,1655,1,0,0,0,269,1661,1,0,0,0,271,1666,1,0,0,0,273,1671, + 1,0,0,0,275,1676,1,0,0,0,277,1684,1,0,0,0,279,1689,1,0,0,0,281,1697, + 1,0,0,0,283,1702,1,0,0,0,285,1707,1,0,0,0,287,1713,1,0,0,0,289,1719, + 1,0,0,0,291,1725,1,0,0,0,293,1730,1,0,0,0,295,1735,1,0,0,0,297,1741, + 1,0,0,0,299,1750,1,0,0,0,301,1755,1,0,0,0,303,1761,1,0,0,0,305,1769, + 1,0,0,0,307,1775,1,0,0,0,309,1779,1,0,0,0,311,1787,1,0,0,0,313,1793, + 1,0,0,0,315,1805,1,0,0,0,317,1818,1,0,0,0,319,1830,1,0,0,0,321,1843, + 1,0,0,0,323,1850,1,0,0,0,325,1858,1,0,0,0,327,1864,1,0,0,0,329,1871, + 1,0,0,0,331,1876,1,0,0,0,333,1886,1,0,0,0,335,1897,1,0,0,0,337,1908, + 1,0,0,0,339,1920,1,0,0,0,341,1928,1,0,0,0,343,1935,1,0,0,0,345,1937, + 1,0,0,0,347,1942,1,0,0,0,349,1948,1,0,0,0,351,1951,1,0,0,0,353,1958, + 1,0,0,0,355,1961,1,0,0,0,357,1966,1,0,0,0,359,1973,1,0,0,0,361,1981, + 1,0,0,0,363,1984,1,0,0,0,365,1990,1,0,0,0,367,1994,1,0,0,0,369,2000, + 1,0,0,0,371,2013,1,0,0,0,373,2018,1,0,0,0,375,2027,1,0,0,0,377,2035, + 1,0,0,0,379,2045,1,0,0,0,381,2055,1,0,0,0,383,2067,1,0,0,0,385,2078, + 1,0,0,0,387,2094,1,0,0,0,389,2110,1,0,0,0,391,2118,1,0,0,0,393,2124, + 1,0,0,0,395,2132,1,0,0,0,397,2141,1,0,0,0,399,2151,1,0,0,0,401,2159, + 1,0,0,0,403,2170,1,0,0,0,405,2181,1,0,0,0,407,2187,1,0,0,0,409,2195, + 1,0,0,0,411,2201,1,0,0,0,413,2207,1,0,0,0,415,2220,1,0,0,0,417,2233, + 1,0,0,0,419,2241,1,0,0,0,421,2248,1,0,0,0,423,2259,1,0,0,0,425,2267, + 1,0,0,0,427,2274,1,0,0,0,429,2281,1,0,0,0,431,2292,1,0,0,0,433,2300, + 1,0,0,0,435,2306,1,0,0,0,437,2314,1,0,0,0,439,2323,1,0,0,0,441,2330, + 1,0,0,0,443,2347,1,0,0,0,445,2349,1,0,0,0,447,2354,1,0,0,0,449,2360, + 1,0,0,0,451,2369,1,0,0,0,453,2376,1,0,0,0,455,2380,1,0,0,0,457,2385, + 1,0,0,0,459,2392,1,0,0,0,461,2400,1,0,0,0,463,2407,1,0,0,0,465,2415, + 1,0,0,0,467,2422,1,0,0,0,469,2427,1,0,0,0,471,2437,1,0,0,0,473,2443, + 1,0,0,0,475,2459,1,0,0,0,477,2472,1,0,0,0,479,2476,1,0,0,0,481,2482, + 1,0,0,0,483,2487,1,0,0,0,485,2492,1,0,0,0,487,2499,1,0,0,0,489,2504, + 1,0,0,0,491,2509,1,0,0,0,493,2516,1,0,0,0,495,2523,1,0,0,0,497,2529, + 1,0,0,0,499,2540,1,0,0,0,501,2547,1,0,0,0,503,2556,1,0,0,0,505,2563, + 1,0,0,0,507,2570,1,0,0,0,509,2580,1,0,0,0,511,2585,1,0,0,0,513,2597, + 1,0,0,0,515,2612,1,0,0,0,517,2618,1,0,0,0,519,2625,1,0,0,0,521,2637, + 1,0,0,0,523,2644,1,0,0,0,525,2671,1,0,0,0,527,2673,1,0,0,0,529,2684, + 1,0,0,0,531,2689,1,0,0,0,533,2694,1,0,0,0,535,2704,1,0,0,0,537,2717, + 1,0,0,0,539,2731,1,0,0,0,541,2734,1,0,0,0,543,2740,1,0,0,0,545,2749, + 1,0,0,0,547,2761,1,0,0,0,549,2774,1,0,0,0,551,2784,1,0,0,0,553,2789, + 1,0,0,0,555,2794,1,0,0,0,557,2803,1,0,0,0,559,2812,1,0,0,0,561,2817, + 1,0,0,0,563,2827,1,0,0,0,565,2837,1,0,0,0,567,2845,1,0,0,0,569,2851, + 1,0,0,0,571,2858,1,0,0,0,573,2866,1,0,0,0,575,2873,1,0,0,0,577,2881, + 1,0,0,0,579,2887,1,0,0,0,581,2894,1,0,0,0,583,2898,1,0,0,0,585,2903, + 1,0,0,0,587,2909,1,0,0,0,589,2916,1,0,0,0,591,2924,1,0,0,0,593,2929, + 1,0,0,0,595,2935,1,0,0,0,597,2940,1,0,0,0,599,2946,1,0,0,0,601,2951, + 1,0,0,0,603,2957,1,0,0,0,605,2964,1,0,0,0,607,2969,1,0,0,0,609,2976, + 1,0,0,0,611,2981,1,0,0,0,613,2987,1,0,0,0,615,2995,1,0,0,0,617,2997, + 1,0,0,0,619,3001,1,0,0,0,621,3004,1,0,0,0,623,3007,1,0,0,0,625,3013, + 1,0,0,0,627,3015,1,0,0,0,629,3021,1,0,0,0,631,3023,1,0,0,0,633,3025, + 1,0,0,0,635,3027,1,0,0,0,637,3029,1,0,0,0,639,3031,1,0,0,0,641,3033, + 1,0,0,0,643,3035,1,0,0,0,645,3037,1,0,0,0,647,3039,1,0,0,0,649,3042, + 1,0,0,0,651,3044,1,0,0,0,653,3046,1,0,0,0,655,3049,1,0,0,0,657,3053, + 1,0,0,0,659,3086,1,0,0,0,661,3088,1,0,0,0,663,3100,1,0,0,0,665,3107, + 1,0,0,0,667,3114,1,0,0,0,669,3121,1,0,0,0,671,3136,1,0,0,0,673,3138, + 1,0,0,0,675,3158,1,0,0,0,677,3177,1,0,0,0,679,3199,1,0,0,0,681,3204, + 1,0,0,0,683,3208,1,0,0,0,685,3237,1,0,0,0,687,3239,1,0,0,0,689,3248, + 1,0,0,0,691,3250,1,0,0,0,693,3252,1,0,0,0,695,3271,1,0,0,0,697,3291, + 1,0,0,0,699,3297,1,0,0,0,701,702,5,59,0,0,702,2,1,0,0,0,703,704, + 5,40,0,0,704,4,1,0,0,0,705,706,5,41,0,0,706,6,1,0,0,0,707,708,5, + 44,0,0,708,8,1,0,0,0,709,710,5,46,0,0,710,10,1,0,0,0,711,712,5,91, + 0,0,712,12,1,0,0,0,713,714,5,93,0,0,714,14,1,0,0,0,715,716,5,65, + 0,0,716,717,5,68,0,0,717,718,5,68,0,0,718,16,1,0,0,0,719,720,5,65, + 0,0,720,721,5,70,0,0,721,722,5,84,0,0,722,723,5,69,0,0,723,724,5, + 82,0,0,724,18,1,0,0,0,725,726,5,65,0,0,726,727,5,76,0,0,727,728, + 5,76,0,0,728,20,1,0,0,0,729,730,5,65,0,0,730,731,5,76,0,0,731,732, + 5,84,0,0,732,733,5,69,0,0,733,734,5,82,0,0,734,22,1,0,0,0,735,736, + 5,65,0,0,736,737,5,76,0,0,737,738,5,87,0,0,738,739,5,65,0,0,739, + 740,5,89,0,0,740,741,5,83,0,0,741,24,1,0,0,0,742,743,5,65,0,0,743, + 744,5,78,0,0,744,745,5,65,0,0,745,746,5,76,0,0,746,747,5,89,0,0, + 747,748,5,90,0,0,748,749,5,69,0,0,749,26,1,0,0,0,750,751,5,65,0, + 0,751,752,5,78,0,0,752,753,5,68,0,0,753,28,1,0,0,0,754,755,5,65, + 0,0,755,756,5,78,0,0,756,757,5,84,0,0,757,758,5,73,0,0,758,30,1, + 0,0,0,759,760,5,65,0,0,760,761,5,78,0,0,761,762,5,89,0,0,762,32, + 1,0,0,0,763,764,5,65,0,0,764,765,5,78,0,0,765,766,5,89,0,0,766,767, + 5,95,0,0,767,768,5,86,0,0,768,769,5,65,0,0,769,770,5,76,0,0,770, + 771,5,85,0,0,771,772,5,69,0,0,772,34,1,0,0,0,773,774,5,65,0,0,774, + 775,5,82,0,0,775,776,5,67,0,0,776,777,5,72,0,0,777,778,5,73,0,0, + 778,779,5,86,0,0,779,780,5,69,0,0,780,36,1,0,0,0,781,782,5,65,0, + 0,782,783,5,82,0,0,783,784,5,82,0,0,784,785,5,65,0,0,785,786,5,89, + 0,0,786,38,1,0,0,0,787,788,5,65,0,0,788,789,5,83,0,0,789,40,1,0, + 0,0,790,791,5,65,0,0,791,792,5,83,0,0,792,793,5,67,0,0,793,42,1, + 0,0,0,794,795,5,65,0,0,795,796,5,84,0,0,796,44,1,0,0,0,797,798,5, + 65,0,0,798,799,5,85,0,0,799,800,5,84,0,0,800,801,5,72,0,0,801,802, + 5,79,0,0,802,803,5,82,0,0,803,804,5,73,0,0,804,805,5,90,0,0,805, + 806,5,65,0,0,806,807,5,84,0,0,807,808,5,73,0,0,808,809,5,79,0,0, + 809,810,5,78,0,0,810,46,1,0,0,0,811,812,5,66,0,0,812,813,5,69,0, + 0,813,814,5,84,0,0,814,815,5,87,0,0,815,816,5,69,0,0,816,817,5,69, + 0,0,817,818,5,78,0,0,818,48,1,0,0,0,819,820,5,66,0,0,820,821,5,79, + 0,0,821,822,5,84,0,0,822,823,5,72,0,0,823,50,1,0,0,0,824,825,5,66, + 0,0,825,826,5,85,0,0,826,827,5,67,0,0,827,828,5,75,0,0,828,829,5, + 69,0,0,829,830,5,84,0,0,830,52,1,0,0,0,831,832,5,66,0,0,832,833, + 5,85,0,0,833,834,5,67,0,0,834,835,5,75,0,0,835,836,5,69,0,0,836, + 837,5,84,0,0,837,838,5,83,0,0,838,54,1,0,0,0,839,840,5,66,0,0,840, + 841,5,89,0,0,841,56,1,0,0,0,842,843,5,67,0,0,843,844,5,65,0,0,844, + 845,5,67,0,0,845,846,5,72,0,0,846,847,5,69,0,0,847,58,1,0,0,0,848, + 849,5,67,0,0,849,850,5,65,0,0,850,851,5,83,0,0,851,852,5,67,0,0, + 852,853,5,65,0,0,853,854,5,68,0,0,854,855,5,69,0,0,855,60,1,0,0, + 0,856,857,5,67,0,0,857,858,5,65,0,0,858,859,5,83,0,0,859,860,5,69, + 0,0,860,62,1,0,0,0,861,862,5,67,0,0,862,863,5,65,0,0,863,864,5,83, + 0,0,864,865,5,84,0,0,865,64,1,0,0,0,866,867,5,67,0,0,867,868,5,65, + 0,0,868,869,5,84,0,0,869,870,5,65,0,0,870,871,5,76,0,0,871,872,5, + 79,0,0,872,873,5,71,0,0,873,66,1,0,0,0,874,875,5,67,0,0,875,876, + 5,65,0,0,876,877,5,84,0,0,877,878,5,65,0,0,878,879,5,76,0,0,879, + 880,5,79,0,0,880,881,5,71,0,0,881,882,5,83,0,0,882,68,1,0,0,0,883, + 884,5,67,0,0,884,885,5,72,0,0,885,886,5,65,0,0,886,887,5,78,0,0, + 887,888,5,71,0,0,888,889,5,69,0,0,889,70,1,0,0,0,890,891,5,67,0, + 0,891,892,5,72,0,0,892,893,5,69,0,0,893,894,5,67,0,0,894,895,5,75, + 0,0,895,72,1,0,0,0,896,897,5,67,0,0,897,898,5,76,0,0,898,899,5,69, + 0,0,899,900,5,65,0,0,900,901,5,82,0,0,901,74,1,0,0,0,902,903,5,67, + 0,0,903,904,5,76,0,0,904,905,5,85,0,0,905,906,5,83,0,0,906,907,5, + 84,0,0,907,908,5,69,0,0,908,909,5,82,0,0,909,76,1,0,0,0,910,911, + 5,67,0,0,911,912,5,76,0,0,912,913,5,85,0,0,913,914,5,83,0,0,914, + 915,5,84,0,0,915,916,5,69,0,0,916,917,5,82,0,0,917,918,5,69,0,0, + 918,919,5,68,0,0,919,78,1,0,0,0,920,921,5,67,0,0,921,922,5,79,0, + 0,922,923,5,68,0,0,923,924,5,69,0,0,924,925,5,71,0,0,925,926,5,69, + 0,0,926,927,5,78,0,0,927,80,1,0,0,0,928,929,5,67,0,0,929,930,5,79, + 0,0,930,931,5,76,0,0,931,932,5,76,0,0,932,933,5,65,0,0,933,934,5, + 84,0,0,934,935,5,69,0,0,935,82,1,0,0,0,936,937,5,67,0,0,937,938, + 5,79,0,0,938,939,5,76,0,0,939,940,5,76,0,0,940,941,5,69,0,0,941, + 942,5,67,0,0,942,943,5,84,0,0,943,944,5,73,0,0,944,945,5,79,0,0, + 945,946,5,78,0,0,946,84,1,0,0,0,947,948,5,67,0,0,948,949,5,79,0, + 0,949,950,5,76,0,0,950,951,5,85,0,0,951,952,5,77,0,0,952,953,5,78, + 0,0,953,86,1,0,0,0,954,955,5,67,0,0,955,956,5,79,0,0,956,957,5,76, + 0,0,957,958,5,85,0,0,958,959,5,77,0,0,959,960,5,78,0,0,960,961,5, + 83,0,0,961,88,1,0,0,0,962,963,5,67,0,0,963,964,5,79,0,0,964,965, + 5,77,0,0,965,966,5,77,0,0,966,967,5,69,0,0,967,968,5,78,0,0,968, + 969,5,84,0,0,969,90,1,0,0,0,970,971,5,67,0,0,971,972,5,79,0,0,972, + 973,5,77,0,0,973,974,5,77,0,0,974,975,5,73,0,0,975,976,5,84,0,0, + 976,92,1,0,0,0,977,978,5,67,0,0,978,979,5,79,0,0,979,980,5,77,0, + 0,980,981,5,80,0,0,981,982,5,65,0,0,982,983,5,67,0,0,983,984,5,84, + 0,0,984,94,1,0,0,0,985,986,5,67,0,0,986,987,5,79,0,0,987,988,5,77, + 0,0,988,989,5,80,0,0,989,990,5,65,0,0,990,991,5,67,0,0,991,992,5, + 84,0,0,992,993,5,73,0,0,993,994,5,79,0,0,994,995,5,78,0,0,995,996, + 5,83,0,0,996,96,1,0,0,0,997,998,5,67,0,0,998,999,5,79,0,0,999,1000, + 5,77,0,0,1000,1001,5,80,0,0,1001,1002,5,85,0,0,1002,1003,5,84,0, + 0,1003,1004,5,69,0,0,1004,98,1,0,0,0,1005,1006,5,67,0,0,1006,1007, + 5,79,0,0,1007,1008,5,78,0,0,1008,1009,5,67,0,0,1009,1010,5,65,0, + 0,1010,1011,5,84,0,0,1011,1012,5,69,0,0,1012,1013,5,78,0,0,1013, + 1014,5,65,0,0,1014,1015,5,84,0,0,1015,1016,5,69,0,0,1016,100,1,0, + 0,0,1017,1018,5,67,0,0,1018,1019,5,79,0,0,1019,1020,5,78,0,0,1020, + 1021,5,83,0,0,1021,1022,5,84,0,0,1022,1023,5,82,0,0,1023,1024,5, + 65,0,0,1024,1025,5,73,0,0,1025,1026,5,78,0,0,1026,1027,5,84,0,0, + 1027,102,1,0,0,0,1028,1029,5,67,0,0,1029,1030,5,79,0,0,1030,1031, + 5,83,0,0,1031,1032,5,84,0,0,1032,104,1,0,0,0,1033,1034,5,67,0,0, + 1034,1035,5,82,0,0,1035,1036,5,69,0,0,1036,1037,5,65,0,0,1037,1038, + 5,84,0,0,1038,1039,5,69,0,0,1039,106,1,0,0,0,1040,1041,5,67,0,0, + 1041,1042,5,82,0,0,1042,1043,5,79,0,0,1043,1044,5,83,0,0,1044,1045, + 5,83,0,0,1045,108,1,0,0,0,1046,1047,5,67,0,0,1047,1048,5,85,0,0, + 1048,1049,5,66,0,0,1049,1050,5,69,0,0,1050,110,1,0,0,0,1051,1052, + 5,67,0,0,1052,1053,5,85,0,0,1053,1054,5,82,0,0,1054,1055,5,82,0, + 0,1055,1056,5,69,0,0,1056,1057,5,78,0,0,1057,1058,5,84,0,0,1058, + 112,1,0,0,0,1059,1060,5,67,0,0,1060,1061,5,85,0,0,1061,1062,5,82, + 0,0,1062,1063,5,82,0,0,1063,1064,5,69,0,0,1064,1065,5,78,0,0,1065, + 1066,5,84,0,0,1066,1067,5,95,0,0,1067,1068,5,68,0,0,1068,1069,5, + 65,0,0,1069,1070,5,84,0,0,1070,1071,5,69,0,0,1071,114,1,0,0,0,1072, + 1073,5,67,0,0,1073,1074,5,85,0,0,1074,1075,5,82,0,0,1075,1076,5, + 82,0,0,1076,1077,5,69,0,0,1077,1078,5,78,0,0,1078,1079,5,84,0,0, + 1079,1080,5,95,0,0,1080,1081,5,84,0,0,1081,1082,5,73,0,0,1082,1083, + 5,77,0,0,1083,1084,5,69,0,0,1084,116,1,0,0,0,1085,1086,5,67,0,0, + 1086,1087,5,85,0,0,1087,1088,5,82,0,0,1088,1089,5,82,0,0,1089,1090, + 5,69,0,0,1090,1091,5,78,0,0,1091,1092,5,84,0,0,1092,1093,5,95,0, + 0,1093,1094,5,84,0,0,1094,1095,5,73,0,0,1095,1096,5,77,0,0,1096, + 1097,5,69,0,0,1097,1098,5,83,0,0,1098,1099,5,84,0,0,1099,1100,5, + 65,0,0,1100,1101,5,77,0,0,1101,1102,5,80,0,0,1102,118,1,0,0,0,1103, + 1104,5,67,0,0,1104,1105,5,85,0,0,1105,1106,5,82,0,0,1106,1107,5, + 82,0,0,1107,1108,5,69,0,0,1108,1109,5,78,0,0,1109,1110,5,84,0,0, + 1110,1111,5,95,0,0,1111,1112,5,85,0,0,1112,1113,5,83,0,0,1113,1114, + 5,69,0,0,1114,1115,5,82,0,0,1115,120,1,0,0,0,1116,1117,5,68,0,0, + 1117,1118,5,65,0,0,1118,1119,5,89,0,0,1119,122,1,0,0,0,1120,1121, + 5,68,0,0,1121,1122,5,65,0,0,1122,1123,5,89,0,0,1123,1124,5,83,0, + 0,1124,124,1,0,0,0,1125,1126,5,68,0,0,1126,1127,5,65,0,0,1127,1128, + 5,89,0,0,1128,1129,5,79,0,0,1129,1130,5,70,0,0,1130,1131,5,89,0, + 0,1131,1132,5,69,0,0,1132,1133,5,65,0,0,1133,1134,5,82,0,0,1134, + 126,1,0,0,0,1135,1136,5,68,0,0,1136,1137,5,65,0,0,1137,1138,5,84, + 0,0,1138,1139,5,65,0,0,1139,128,1,0,0,0,1140,1141,5,68,0,0,1141, + 1142,5,65,0,0,1142,1143,5,84,0,0,1143,1144,5,65,0,0,1144,1145,5, + 66,0,0,1145,1146,5,65,0,0,1146,1147,5,83,0,0,1147,1148,5,69,0,0, + 1148,130,1,0,0,0,1149,1150,5,68,0,0,1150,1151,5,65,0,0,1151,1152, + 5,84,0,0,1152,1153,5,65,0,0,1153,1154,5,66,0,0,1154,1155,5,65,0, + 0,1155,1156,5,83,0,0,1156,1157,5,69,0,0,1157,1158,5,83,0,0,1158, + 132,1,0,0,0,1159,1160,5,68,0,0,1160,1161,5,65,0,0,1161,1162,5,84, + 0,0,1162,1163,5,69,0,0,1163,1164,5,65,0,0,1164,1165,5,68,0,0,1165, + 1166,5,68,0,0,1166,134,1,0,0,0,1167,1168,5,68,0,0,1168,1169,5,65, + 0,0,1169,1170,5,84,0,0,1170,1171,5,69,0,0,1171,1172,5,68,0,0,1172, + 1173,5,73,0,0,1173,1174,5,70,0,0,1174,1175,5,70,0,0,1175,136,1,0, + 0,0,1176,1177,5,68,0,0,1177,1178,5,66,0,0,1178,1179,5,80,0,0,1179, + 1180,5,82,0,0,1180,1181,5,79,0,0,1181,1182,5,80,0,0,1182,1183,5, + 69,0,0,1183,1184,5,82,0,0,1184,1185,5,84,0,0,1185,1186,5,73,0,0, + 1186,1187,5,69,0,0,1187,1188,5,83,0,0,1188,138,1,0,0,0,1189,1190, + 5,68,0,0,1190,1191,5,69,0,0,1191,1192,5,70,0,0,1192,1193,5,65,0, + 0,1193,1194,5,85,0,0,1194,1195,5,76,0,0,1195,1196,5,84,0,0,1196, + 140,1,0,0,0,1197,1198,5,68,0,0,1198,1199,5,69,0,0,1199,1200,5,70, + 0,0,1200,1201,5,73,0,0,1201,1202,5,78,0,0,1202,1203,5,69,0,0,1203, + 1204,5,68,0,0,1204,142,1,0,0,0,1205,1206,5,68,0,0,1206,1207,5,69, + 0,0,1207,1208,5,76,0,0,1208,1209,5,69,0,0,1209,1210,5,84,0,0,1210, + 1211,5,69,0,0,1211,144,1,0,0,0,1212,1213,5,68,0,0,1213,1214,5,69, + 0,0,1214,1215,5,76,0,0,1215,1216,5,73,0,0,1216,1217,5,77,0,0,1217, + 1218,5,73,0,0,1218,1219,5,84,0,0,1219,1220,5,69,0,0,1220,1221,5, + 68,0,0,1221,146,1,0,0,0,1222,1223,5,68,0,0,1223,1224,5,69,0,0,1224, + 1225,5,83,0,0,1225,1226,5,67,0,0,1226,148,1,0,0,0,1227,1228,5,68, + 0,0,1228,1229,5,69,0,0,1229,1230,5,83,0,0,1230,1231,5,67,0,0,1231, + 1232,5,82,0,0,1232,1233,5,73,0,0,1233,1234,5,66,0,0,1234,1235,5, + 69,0,0,1235,150,1,0,0,0,1236,1237,5,68,0,0,1237,1238,5,70,0,0,1238, + 1239,5,83,0,0,1239,152,1,0,0,0,1240,1241,5,68,0,0,1241,1242,5,73, + 0,0,1242,1243,5,82,0,0,1243,1244,5,69,0,0,1244,1245,5,67,0,0,1245, + 1246,5,84,0,0,1246,1247,5,79,0,0,1247,1248,5,82,0,0,1248,1249,5, + 73,0,0,1249,1250,5,69,0,0,1250,1251,5,83,0,0,1251,154,1,0,0,0,1252, + 1253,5,68,0,0,1253,1254,5,73,0,0,1254,1255,5,82,0,0,1255,1256,5, + 69,0,0,1256,1257,5,67,0,0,1257,1258,5,84,0,0,1258,1259,5,79,0,0, + 1259,1260,5,82,0,0,1260,1261,5,89,0,0,1261,156,1,0,0,0,1262,1263, + 5,68,0,0,1263,1264,5,73,0,0,1264,1265,5,83,0,0,1265,1266,5,84,0, + 0,1266,1267,5,73,0,0,1267,1268,5,78,0,0,1268,1269,5,67,0,0,1269, + 1270,5,84,0,0,1270,158,1,0,0,0,1271,1272,5,68,0,0,1272,1273,5,73, + 0,0,1273,1274,5,83,0,0,1274,1275,5,84,0,0,1275,1276,5,82,0,0,1276, + 1277,5,73,0,0,1277,1278,5,66,0,0,1278,1279,5,85,0,0,1279,1280,5, + 84,0,0,1280,1281,5,69,0,0,1281,160,1,0,0,0,1282,1283,5,68,0,0,1283, + 1284,5,73,0,0,1284,1285,5,86,0,0,1285,162,1,0,0,0,1286,1287,5,68, + 0,0,1287,1288,5,82,0,0,1288,1289,5,79,0,0,1289,1290,5,80,0,0,1290, + 164,1,0,0,0,1291,1292,5,69,0,0,1292,1293,5,76,0,0,1293,1294,5,83, + 0,0,1294,1295,5,69,0,0,1295,166,1,0,0,0,1296,1297,5,69,0,0,1297, + 1298,5,78,0,0,1298,1299,5,68,0,0,1299,168,1,0,0,0,1300,1301,5,69, + 0,0,1301,1302,5,83,0,0,1302,1303,5,67,0,0,1303,1304,5,65,0,0,1304, + 1305,5,80,0,0,1305,1306,5,69,0,0,1306,170,1,0,0,0,1307,1308,5,69, + 0,0,1308,1309,5,83,0,0,1309,1310,5,67,0,0,1310,1311,5,65,0,0,1311, + 1312,5,80,0,0,1312,1313,5,69,0,0,1313,1314,5,68,0,0,1314,172,1,0, + 0,0,1315,1316,5,69,0,0,1316,1317,5,88,0,0,1317,1318,5,67,0,0,1318, + 1319,5,69,0,0,1319,1320,5,80,0,0,1320,1321,5,84,0,0,1321,174,1,0, + 0,0,1322,1323,5,69,0,0,1323,1324,5,88,0,0,1324,1325,5,67,0,0,1325, + 1326,5,72,0,0,1326,1327,5,65,0,0,1327,1328,5,78,0,0,1328,1329,5, + 71,0,0,1329,1330,5,69,0,0,1330,176,1,0,0,0,1331,1332,5,69,0,0,1332, + 1333,5,88,0,0,1333,1334,5,67,0,0,1334,1335,5,76,0,0,1335,1336,5, + 85,0,0,1336,1337,5,68,0,0,1337,1338,5,69,0,0,1338,178,1,0,0,0,1339, + 1340,5,69,0,0,1340,1341,5,88,0,0,1341,1342,5,73,0,0,1342,1343,5, + 83,0,0,1343,1344,5,84,0,0,1344,1345,5,83,0,0,1345,180,1,0,0,0,1346, + 1347,5,69,0,0,1347,1348,5,88,0,0,1348,1349,5,80,0,0,1349,1350,5, + 76,0,0,1350,1351,5,65,0,0,1351,1352,5,73,0,0,1352,1353,5,78,0,0, + 1353,182,1,0,0,0,1354,1355,5,69,0,0,1355,1356,5,88,0,0,1356,1357, + 5,80,0,0,1357,1358,5,79,0,0,1358,1359,5,82,0,0,1359,1360,5,84,0, + 0,1360,184,1,0,0,0,1361,1362,5,69,0,0,1362,1363,5,88,0,0,1363,1364, + 5,84,0,0,1364,1365,5,69,0,0,1365,1366,5,78,0,0,1366,1367,5,68,0, + 0,1367,1368,5,69,0,0,1368,1369,5,68,0,0,1369,186,1,0,0,0,1370,1371, + 5,69,0,0,1371,1372,5,88,0,0,1372,1373,5,84,0,0,1373,1374,5,69,0, + 0,1374,1375,5,82,0,0,1375,1376,5,78,0,0,1376,1377,5,65,0,0,1377, + 1378,5,76,0,0,1378,188,1,0,0,0,1379,1380,5,69,0,0,1380,1381,5,88, + 0,0,1381,1382,5,84,0,0,1382,1383,5,82,0,0,1383,1384,5,65,0,0,1384, + 1385,5,67,0,0,1385,1386,5,84,0,0,1386,190,1,0,0,0,1387,1388,5,70, + 0,0,1388,1389,5,65,0,0,1389,1390,5,76,0,0,1390,1391,5,83,0,0,1391, + 1392,5,69,0,0,1392,192,1,0,0,0,1393,1394,5,70,0,0,1394,1395,5,69, + 0,0,1395,1396,5,84,0,0,1396,1397,5,67,0,0,1397,1398,5,72,0,0,1398, + 194,1,0,0,0,1399,1400,5,70,0,0,1400,1401,5,73,0,0,1401,1402,5,69, + 0,0,1402,1403,5,76,0,0,1403,1404,5,68,0,0,1404,1405,5,83,0,0,1405, + 196,1,0,0,0,1406,1407,5,70,0,0,1407,1408,5,73,0,0,1408,1409,5,76, + 0,0,1409,1410,5,84,0,0,1410,1411,5,69,0,0,1411,1412,5,82,0,0,1412, + 198,1,0,0,0,1413,1414,5,70,0,0,1414,1415,5,73,0,0,1415,1416,5,76, + 0,0,1416,1417,5,69,0,0,1417,1418,5,70,0,0,1418,1419,5,79,0,0,1419, + 1420,5,82,0,0,1420,1421,5,77,0,0,1421,1422,5,65,0,0,1422,1423,5, + 84,0,0,1423,200,1,0,0,0,1424,1425,5,70,0,0,1425,1426,5,73,0,0,1426, + 1427,5,82,0,0,1427,1428,5,83,0,0,1428,1429,5,84,0,0,1429,202,1,0, + 0,0,1430,1431,5,70,0,0,1431,1432,5,79,0,0,1432,1433,5,76,0,0,1433, + 1434,5,76,0,0,1434,1435,5,79,0,0,1435,1436,5,87,0,0,1436,1437,5, + 73,0,0,1437,1438,5,78,0,0,1438,1439,5,71,0,0,1439,204,1,0,0,0,1440, + 1441,5,70,0,0,1441,1442,5,79,0,0,1442,1443,5,82,0,0,1443,206,1,0, + 0,0,1444,1445,5,70,0,0,1445,1446,5,79,0,0,1446,1447,5,82,0,0,1447, + 1448,5,69,0,0,1448,1449,5,73,0,0,1449,1450,5,71,0,0,1450,1451,5, + 78,0,0,1451,208,1,0,0,0,1452,1453,5,70,0,0,1453,1454,5,79,0,0,1454, + 1455,5,82,0,0,1455,1456,5,77,0,0,1456,1457,5,65,0,0,1457,1458,5, + 84,0,0,1458,210,1,0,0,0,1459,1460,5,70,0,0,1460,1461,5,79,0,0,1461, + 1462,5,82,0,0,1462,1463,5,77,0,0,1463,1464,5,65,0,0,1464,1465,5, + 84,0,0,1465,1466,5,84,0,0,1466,1467,5,69,0,0,1467,1468,5,68,0,0, + 1468,212,1,0,0,0,1469,1470,5,70,0,0,1470,1471,5,82,0,0,1471,1472, + 5,79,0,0,1472,1473,5,77,0,0,1473,214,1,0,0,0,1474,1475,5,70,0,0, + 1475,1476,5,85,0,0,1476,1477,5,76,0,0,1477,1478,5,76,0,0,1478,216, + 1,0,0,0,1479,1480,5,70,0,0,1480,1481,5,85,0,0,1481,1482,5,78,0,0, + 1482,1483,5,67,0,0,1483,1484,5,84,0,0,1484,1485,5,73,0,0,1485,1486, + 5,79,0,0,1486,1487,5,78,0,0,1487,218,1,0,0,0,1488,1489,5,70,0,0, + 1489,1490,5,85,0,0,1490,1491,5,78,0,0,1491,1492,5,67,0,0,1492,1493, + 5,84,0,0,1493,1494,5,73,0,0,1494,1495,5,79,0,0,1495,1496,5,78,0, + 0,1496,1497,5,83,0,0,1497,220,1,0,0,0,1498,1499,5,71,0,0,1499,1500, + 5,69,0,0,1500,1501,5,78,0,0,1501,1502,5,69,0,0,1502,1503,5,82,0, + 0,1503,1504,5,65,0,0,1504,1505,5,84,0,0,1505,1506,5,69,0,0,1506, + 1507,5,68,0,0,1507,222,1,0,0,0,1508,1509,5,71,0,0,1509,1510,5,76, + 0,0,1510,1511,5,79,0,0,1511,1512,5,66,0,0,1512,1513,5,65,0,0,1513, + 1514,5,76,0,0,1514,224,1,0,0,0,1515,1516,5,71,0,0,1516,1517,5,82, + 0,0,1517,1518,5,65,0,0,1518,1519,5,78,0,0,1519,1520,5,84,0,0,1520, + 226,1,0,0,0,1521,1522,5,71,0,0,1522,1523,5,82,0,0,1523,1524,5,79, + 0,0,1524,1525,5,85,0,0,1525,1526,5,80,0,0,1526,228,1,0,0,0,1527, + 1528,5,71,0,0,1528,1529,5,82,0,0,1529,1530,5,79,0,0,1530,1531,5, + 85,0,0,1531,1532,5,80,0,0,1532,1533,5,73,0,0,1533,1534,5,78,0,0, + 1534,1535,5,71,0,0,1535,230,1,0,0,0,1536,1537,5,72,0,0,1537,1538, + 5,65,0,0,1538,1539,5,86,0,0,1539,1540,5,73,0,0,1540,1541,5,78,0, + 0,1541,1542,5,71,0,0,1542,232,1,0,0,0,1543,1544,5,72,0,0,1544,1545, + 5,79,0,0,1545,1546,5,85,0,0,1546,1547,5,82,0,0,1547,234,1,0,0,0, + 1548,1549,5,72,0,0,1549,1550,5,79,0,0,1550,1551,5,85,0,0,1551,1552, + 5,82,0,0,1552,1553,5,83,0,0,1553,236,1,0,0,0,1554,1555,5,73,0,0, + 1555,1556,5,70,0,0,1556,238,1,0,0,0,1557,1558,5,73,0,0,1558,1559, + 5,71,0,0,1559,1560,5,78,0,0,1560,1561,5,79,0,0,1561,1562,5,82,0, + 0,1562,1563,5,69,0,0,1563,240,1,0,0,0,1564,1565,5,73,0,0,1565,1566, + 5,77,0,0,1566,1567,5,80,0,0,1567,1568,5,79,0,0,1568,1569,5,82,0, + 0,1569,1570,5,84,0,0,1570,242,1,0,0,0,1571,1572,5,73,0,0,1572,1573, + 5,78,0,0,1573,244,1,0,0,0,1574,1575,5,73,0,0,1575,1576,5,78,0,0, + 1576,1577,5,67,0,0,1577,1578,5,76,0,0,1578,1579,5,85,0,0,1579,1580, + 5,68,0,0,1580,1581,5,69,0,0,1581,246,1,0,0,0,1582,1583,5,73,0,0, + 1583,1584,5,78,0,0,1584,1585,5,68,0,0,1585,1586,5,69,0,0,1586,1587, + 5,88,0,0,1587,248,1,0,0,0,1588,1589,5,73,0,0,1589,1590,5,78,0,0, + 1590,1591,5,68,0,0,1591,1592,5,69,0,0,1592,1593,5,88,0,0,1593,1594, + 5,69,0,0,1594,1595,5,83,0,0,1595,250,1,0,0,0,1596,1597,5,73,0,0, + 1597,1598,5,78,0,0,1598,1599,5,78,0,0,1599,1600,5,69,0,0,1600,1601, + 5,82,0,0,1601,252,1,0,0,0,1602,1603,5,73,0,0,1603,1604,5,78,0,0, + 1604,1605,5,80,0,0,1605,1606,5,65,0,0,1606,1607,5,84,0,0,1607,1608, + 5,72,0,0,1608,254,1,0,0,0,1609,1610,5,73,0,0,1610,1611,5,78,0,0, + 1611,1612,5,80,0,0,1612,1613,5,85,0,0,1613,1614,5,84,0,0,1614,1615, + 5,70,0,0,1615,1616,5,79,0,0,1616,1617,5,82,0,0,1617,1618,5,77,0, + 0,1618,1619,5,65,0,0,1619,1620,5,84,0,0,1620,256,1,0,0,0,1621,1622, + 5,73,0,0,1622,1623,5,78,0,0,1623,1624,5,83,0,0,1624,1625,5,69,0, + 0,1625,1626,5,82,0,0,1626,1627,5,84,0,0,1627,258,1,0,0,0,1628,1629, + 5,73,0,0,1629,1630,5,78,0,0,1630,1631,5,84,0,0,1631,1632,5,69,0, + 0,1632,1633,5,82,0,0,1633,1634,5,83,0,0,1634,1635,5,69,0,0,1635, + 1636,5,67,0,0,1636,1637,5,84,0,0,1637,260,1,0,0,0,1638,1639,5,73, + 0,0,1639,1640,5,78,0,0,1640,1641,5,84,0,0,1641,1642,5,69,0,0,1642, + 1643,5,82,0,0,1643,1644,5,86,0,0,1644,1645,5,65,0,0,1645,1646,5, + 76,0,0,1646,262,1,0,0,0,1647,1648,5,73,0,0,1648,1649,5,78,0,0,1649, + 1650,5,84,0,0,1650,1651,5,79,0,0,1651,264,1,0,0,0,1652,1653,5,73, + 0,0,1653,1654,5,83,0,0,1654,266,1,0,0,0,1655,1656,5,73,0,0,1656, + 1657,5,84,0,0,1657,1658,5,69,0,0,1658,1659,5,77,0,0,1659,1660,5, + 83,0,0,1660,268,1,0,0,0,1661,1662,5,74,0,0,1662,1663,5,79,0,0,1663, + 1664,5,73,0,0,1664,1665,5,78,0,0,1665,270,1,0,0,0,1666,1667,5,75, + 0,0,1667,1668,5,69,0,0,1668,1669,5,89,0,0,1669,1670,5,83,0,0,1670, + 272,1,0,0,0,1671,1672,5,76,0,0,1672,1673,5,65,0,0,1673,1674,5,83, + 0,0,1674,1675,5,84,0,0,1675,274,1,0,0,0,1676,1677,5,76,0,0,1677, + 1678,5,65,0,0,1678,1679,5,84,0,0,1679,1680,5,69,0,0,1680,1681,5, + 82,0,0,1681,1682,5,65,0,0,1682,1683,5,76,0,0,1683,276,1,0,0,0,1684, + 1685,5,76,0,0,1685,1686,5,65,0,0,1686,1687,5,90,0,0,1687,1688,5, + 89,0,0,1688,278,1,0,0,0,1689,1690,5,76,0,0,1690,1691,5,69,0,0,1691, + 1692,5,65,0,0,1692,1693,5,68,0,0,1693,1694,5,73,0,0,1694,1695,5, + 78,0,0,1695,1696,5,71,0,0,1696,280,1,0,0,0,1697,1698,5,76,0,0,1698, + 1699,5,69,0,0,1699,1700,5,70,0,0,1700,1701,5,84,0,0,1701,282,1,0, + 0,0,1702,1703,5,76,0,0,1703,1704,5,73,0,0,1704,1705,5,75,0,0,1705, + 1706,5,69,0,0,1706,284,1,0,0,0,1707,1708,5,73,0,0,1708,1709,5,76, + 0,0,1709,1710,5,73,0,0,1710,1711,5,75,0,0,1711,1712,5,69,0,0,1712, + 286,1,0,0,0,1713,1714,5,76,0,0,1714,1715,5,73,0,0,1715,1716,5,77, + 0,0,1716,1717,5,73,0,0,1717,1718,5,84,0,0,1718,288,1,0,0,0,1719, + 1720,5,76,0,0,1720,1721,5,73,0,0,1721,1722,5,78,0,0,1722,1723,5, + 69,0,0,1723,1724,5,83,0,0,1724,290,1,0,0,0,1725,1726,5,76,0,0,1726, + 1727,5,73,0,0,1727,1728,5,83,0,0,1728,1729,5,84,0,0,1729,292,1,0, + 0,0,1730,1731,5,76,0,0,1731,1732,5,79,0,0,1732,1733,5,65,0,0,1733, + 1734,5,68,0,0,1734,294,1,0,0,0,1735,1736,5,76,0,0,1736,1737,5,79, + 0,0,1737,1738,5,67,0,0,1738,1739,5,65,0,0,1739,1740,5,76,0,0,1740, + 296,1,0,0,0,1741,1742,5,76,0,0,1742,1743,5,79,0,0,1743,1744,5,67, + 0,0,1744,1745,5,65,0,0,1745,1746,5,84,0,0,1746,1747,5,73,0,0,1747, + 1748,5,79,0,0,1748,1749,5,78,0,0,1749,298,1,0,0,0,1750,1751,5,76, + 0,0,1751,1752,5,79,0,0,1752,1753,5,67,0,0,1753,1754,5,75,0,0,1754, + 300,1,0,0,0,1755,1756,5,76,0,0,1756,1757,5,79,0,0,1757,1758,5,67, + 0,0,1758,1759,5,75,0,0,1759,1760,5,83,0,0,1760,302,1,0,0,0,1761, + 1762,5,76,0,0,1762,1763,5,79,0,0,1763,1764,5,71,0,0,1764,1765,5, + 73,0,0,1765,1766,5,67,0,0,1766,1767,5,65,0,0,1767,1768,5,76,0,0, + 1768,304,1,0,0,0,1769,1770,5,77,0,0,1770,1771,5,65,0,0,1771,1772, + 5,67,0,0,1772,1773,5,82,0,0,1773,1774,5,79,0,0,1774,306,1,0,0,0, + 1775,1776,5,77,0,0,1776,1777,5,65,0,0,1777,1778,5,80,0,0,1778,308, + 1,0,0,0,1779,1780,5,77,0,0,1780,1781,5,65,0,0,1781,1782,5,84,0,0, + 1782,1783,5,67,0,0,1783,1784,5,72,0,0,1784,1785,5,69,0,0,1785,1786, + 5,68,0,0,1786,310,1,0,0,0,1787,1788,5,77,0,0,1788,1789,5,69,0,0, + 1789,1790,5,82,0,0,1790,1791,5,71,0,0,1791,1792,5,69,0,0,1792,312, + 1,0,0,0,1793,1794,5,77,0,0,1794,1795,5,73,0,0,1795,1796,5,67,0,0, + 1796,1797,5,82,0,0,1797,1798,5,79,0,0,1798,1799,5,83,0,0,1799,1800, + 5,69,0,0,1800,1801,5,67,0,0,1801,1802,5,79,0,0,1802,1803,5,78,0, + 0,1803,1804,5,68,0,0,1804,314,1,0,0,0,1805,1806,5,77,0,0,1806,1807, + 5,73,0,0,1807,1808,5,67,0,0,1808,1809,5,82,0,0,1809,1810,5,79,0, + 0,1810,1811,5,83,0,0,1811,1812,5,69,0,0,1812,1813,5,67,0,0,1813, + 1814,5,79,0,0,1814,1815,5,78,0,0,1815,1816,5,68,0,0,1816,1817,5, + 83,0,0,1817,316,1,0,0,0,1818,1819,5,77,0,0,1819,1820,5,73,0,0,1820, + 1821,5,76,0,0,1821,1822,5,76,0,0,1822,1823,5,73,0,0,1823,1824,5, + 83,0,0,1824,1825,5,69,0,0,1825,1826,5,67,0,0,1826,1827,5,79,0,0, + 1827,1828,5,78,0,0,1828,1829,5,68,0,0,1829,318,1,0,0,0,1830,1831, + 5,77,0,0,1831,1832,5,73,0,0,1832,1833,5,76,0,0,1833,1834,5,76,0, + 0,1834,1835,5,73,0,0,1835,1836,5,83,0,0,1836,1837,5,69,0,0,1837, + 1838,5,67,0,0,1838,1839,5,79,0,0,1839,1840,5,78,0,0,1840,1841,5, + 68,0,0,1841,1842,5,83,0,0,1842,320,1,0,0,0,1843,1844,5,77,0,0,1844, + 1845,5,73,0,0,1845,1846,5,78,0,0,1846,1847,5,85,0,0,1847,1848,5, + 84,0,0,1848,1849,5,69,0,0,1849,322,1,0,0,0,1850,1851,5,77,0,0,1851, + 1852,5,73,0,0,1852,1853,5,78,0,0,1853,1854,5,85,0,0,1854,1855,5, + 84,0,0,1855,1856,5,69,0,0,1856,1857,5,83,0,0,1857,324,1,0,0,0,1858, + 1859,5,77,0,0,1859,1860,5,79,0,0,1860,1861,5,78,0,0,1861,1862,5, + 84,0,0,1862,1863,5,72,0,0,1863,326,1,0,0,0,1864,1865,5,77,0,0,1865, + 1866,5,79,0,0,1866,1867,5,78,0,0,1867,1868,5,84,0,0,1868,1869,5, + 72,0,0,1869,1870,5,83,0,0,1870,328,1,0,0,0,1871,1872,5,77,0,0,1872, + 1873,5,83,0,0,1873,1874,5,67,0,0,1874,1875,5,75,0,0,1875,330,1,0, + 0,0,1876,1877,5,78,0,0,1877,1878,5,65,0,0,1878,1879,5,77,0,0,1879, + 1880,5,69,0,0,1880,1881,5,83,0,0,1881,1882,5,80,0,0,1882,1883,5, + 65,0,0,1883,1884,5,67,0,0,1884,1885,5,69,0,0,1885,332,1,0,0,0,1886, + 1887,5,78,0,0,1887,1888,5,65,0,0,1888,1889,5,77,0,0,1889,1890,5, + 69,0,0,1890,1891,5,83,0,0,1891,1892,5,80,0,0,1892,1893,5,65,0,0, + 1893,1894,5,67,0,0,1894,1895,5,69,0,0,1895,1896,5,83,0,0,1896,334, + 1,0,0,0,1897,1898,5,78,0,0,1898,1899,5,65,0,0,1899,1900,5,78,0,0, + 1900,1901,5,79,0,0,1901,1902,5,83,0,0,1902,1903,5,69,0,0,1903,1904, + 5,67,0,0,1904,1905,5,79,0,0,1905,1906,5,78,0,0,1906,1907,5,68,0, + 0,1907,336,1,0,0,0,1908,1909,5,78,0,0,1909,1910,5,65,0,0,1910,1911, + 5,78,0,0,1911,1912,5,79,0,0,1912,1913,5,83,0,0,1913,1914,5,69,0, + 0,1914,1915,5,67,0,0,1915,1916,5,79,0,0,1916,1917,5,78,0,0,1917, + 1918,5,68,0,0,1918,1919,5,83,0,0,1919,338,1,0,0,0,1920,1921,5,78, + 0,0,1921,1922,5,65,0,0,1922,1923,5,84,0,0,1923,1924,5,85,0,0,1924, + 1925,5,82,0,0,1925,1926,5,65,0,0,1926,1927,5,76,0,0,1927,340,1,0, + 0,0,1928,1929,5,78,0,0,1929,1930,5,79,0,0,1930,342,1,0,0,0,1931, + 1932,5,78,0,0,1932,1933,5,79,0,0,1933,1936,5,84,0,0,1934,1936,5, + 33,0,0,1935,1931,1,0,0,0,1935,1934,1,0,0,0,1936,344,1,0,0,0,1937, + 1938,5,78,0,0,1938,1939,5,85,0,0,1939,1940,5,76,0,0,1940,1941,5, + 76,0,0,1941,346,1,0,0,0,1942,1943,5,78,0,0,1943,1944,5,85,0,0,1944, + 1945,5,76,0,0,1945,1946,5,76,0,0,1946,1947,5,83,0,0,1947,348,1,0, + 0,0,1948,1949,5,79,0,0,1949,1950,5,70,0,0,1950,350,1,0,0,0,1951, + 1952,5,79,0,0,1952,1953,5,70,0,0,1953,1954,5,70,0,0,1954,1955,5, + 83,0,0,1955,1956,5,69,0,0,1956,1957,5,84,0,0,1957,352,1,0,0,0,1958, + 1959,5,79,0,0,1959,1960,5,78,0,0,1960,354,1,0,0,0,1961,1962,5,79, + 0,0,1962,1963,5,78,0,0,1963,1964,5,76,0,0,1964,1965,5,89,0,0,1965, + 356,1,0,0,0,1966,1967,5,79,0,0,1967,1968,5,80,0,0,1968,1969,5,84, + 0,0,1969,1970,5,73,0,0,1970,1971,5,79,0,0,1971,1972,5,78,0,0,1972, + 358,1,0,0,0,1973,1974,5,79,0,0,1974,1975,5,80,0,0,1975,1976,5,84, + 0,0,1976,1977,5,73,0,0,1977,1978,5,79,0,0,1978,1979,5,78,0,0,1979, + 1980,5,83,0,0,1980,360,1,0,0,0,1981,1982,5,79,0,0,1982,1983,5,82, + 0,0,1983,362,1,0,0,0,1984,1985,5,79,0,0,1985,1986,5,82,0,0,1986, + 1987,5,68,0,0,1987,1988,5,69,0,0,1988,1989,5,82,0,0,1989,364,1,0, + 0,0,1990,1991,5,79,0,0,1991,1992,5,85,0,0,1992,1993,5,84,0,0,1993, + 366,1,0,0,0,1994,1995,5,79,0,0,1995,1996,5,85,0,0,1996,1997,5,84, + 0,0,1997,1998,5,69,0,0,1998,1999,5,82,0,0,1999,368,1,0,0,0,2000, + 2001,5,79,0,0,2001,2002,5,85,0,0,2002,2003,5,84,0,0,2003,2004,5, + 80,0,0,2004,2005,5,85,0,0,2005,2006,5,84,0,0,2006,2007,5,70,0,0, + 2007,2008,5,79,0,0,2008,2009,5,82,0,0,2009,2010,5,77,0,0,2010,2011, + 5,65,0,0,2011,2012,5,84,0,0,2012,370,1,0,0,0,2013,2014,5,79,0,0, + 2014,2015,5,86,0,0,2015,2016,5,69,0,0,2016,2017,5,82,0,0,2017,372, + 1,0,0,0,2018,2019,5,79,0,0,2019,2020,5,86,0,0,2020,2021,5,69,0,0, + 2021,2022,5,82,0,0,2022,2023,5,76,0,0,2023,2024,5,65,0,0,2024,2025, + 5,80,0,0,2025,2026,5,83,0,0,2026,374,1,0,0,0,2027,2028,5,79,0,0, + 2028,2029,5,86,0,0,2029,2030,5,69,0,0,2030,2031,5,82,0,0,2031,2032, + 5,76,0,0,2032,2033,5,65,0,0,2033,2034,5,89,0,0,2034,376,1,0,0,0, + 2035,2036,5,79,0,0,2036,2037,5,86,0,0,2037,2038,5,69,0,0,2038,2039, + 5,82,0,0,2039,2040,5,87,0,0,2040,2041,5,82,0,0,2041,2042,5,73,0, + 0,2042,2043,5,84,0,0,2043,2044,5,69,0,0,2044,378,1,0,0,0,2045,2046, + 5,80,0,0,2046,2047,5,65,0,0,2047,2048,5,82,0,0,2048,2049,5,84,0, + 0,2049,2050,5,73,0,0,2050,2051,5,84,0,0,2051,2052,5,73,0,0,2052, + 2053,5,79,0,0,2053,2054,5,78,0,0,2054,380,1,0,0,0,2055,2056,5,80, + 0,0,2056,2057,5,65,0,0,2057,2058,5,82,0,0,2058,2059,5,84,0,0,2059, + 2060,5,73,0,0,2060,2061,5,84,0,0,2061,2062,5,73,0,0,2062,2063,5, + 79,0,0,2063,2064,5,78,0,0,2064,2065,5,69,0,0,2065,2066,5,68,0,0, + 2066,382,1,0,0,0,2067,2068,5,80,0,0,2068,2069,5,65,0,0,2069,2070, + 5,82,0,0,2070,2071,5,84,0,0,2071,2072,5,73,0,0,2072,2073,5,84,0, + 0,2073,2074,5,73,0,0,2074,2075,5,79,0,0,2075,2076,5,78,0,0,2076, + 2077,5,83,0,0,2077,384,1,0,0,0,2078,2079,5,80,0,0,2079,2080,5,69, + 0,0,2080,2081,5,82,0,0,2081,2082,5,67,0,0,2082,2083,5,69,0,0,2083, + 2084,5,78,0,0,2084,2085,5,84,0,0,2085,2086,5,73,0,0,2086,2087,5, + 76,0,0,2087,2088,5,69,0,0,2088,2089,5,95,0,0,2089,2090,5,67,0,0, + 2090,2091,5,79,0,0,2091,2092,5,78,0,0,2092,2093,5,84,0,0,2093,386, + 1,0,0,0,2094,2095,5,80,0,0,2095,2096,5,69,0,0,2096,2097,5,82,0,0, + 2097,2098,5,67,0,0,2098,2099,5,69,0,0,2099,2100,5,78,0,0,2100,2101, + 5,84,0,0,2101,2102,5,73,0,0,2102,2103,5,76,0,0,2103,2104,5,69,0, + 0,2104,2105,5,95,0,0,2105,2106,5,68,0,0,2106,2107,5,73,0,0,2107, + 2108,5,83,0,0,2108,2109,5,67,0,0,2109,388,1,0,0,0,2110,2111,5,80, + 0,0,2111,2112,5,69,0,0,2112,2113,5,82,0,0,2113,2114,5,67,0,0,2114, + 2115,5,69,0,0,2115,2116,5,78,0,0,2116,2117,5,84,0,0,2117,390,1,0, + 0,0,2118,2119,5,80,0,0,2119,2120,5,73,0,0,2120,2121,5,86,0,0,2121, + 2122,5,79,0,0,2122,2123,5,84,0,0,2123,392,1,0,0,0,2124,2125,5,80, + 0,0,2125,2126,5,76,0,0,2126,2127,5,65,0,0,2127,2128,5,67,0,0,2128, + 2129,5,73,0,0,2129,2130,5,78,0,0,2130,2131,5,71,0,0,2131,394,1,0, + 0,0,2132,2133,5,80,0,0,2133,2134,5,79,0,0,2134,2135,5,83,0,0,2135, + 2136,5,73,0,0,2136,2137,5,84,0,0,2137,2138,5,73,0,0,2138,2139,5, + 79,0,0,2139,2140,5,78,0,0,2140,396,1,0,0,0,2141,2142,5,80,0,0,2142, + 2143,5,82,0,0,2143,2144,5,69,0,0,2144,2145,5,67,0,0,2145,2146,5, + 69,0,0,2146,2147,5,68,0,0,2147,2148,5,73,0,0,2148,2149,5,78,0,0, + 2149,2150,5,71,0,0,2150,398,1,0,0,0,2151,2152,5,80,0,0,2152,2153, + 5,82,0,0,2153,2154,5,73,0,0,2154,2155,5,77,0,0,2155,2156,5,65,0, + 0,2156,2157,5,82,0,0,2157,2158,5,89,0,0,2158,400,1,0,0,0,2159,2160, + 5,80,0,0,2160,2161,5,82,0,0,2161,2162,5,73,0,0,2162,2163,5,78,0, + 0,2163,2164,5,67,0,0,2164,2165,5,73,0,0,2165,2166,5,80,0,0,2166, + 2167,5,65,0,0,2167,2168,5,76,0,0,2168,2169,5,83,0,0,2169,402,1,0, + 0,0,2170,2171,5,80,0,0,2171,2172,5,82,0,0,2172,2173,5,79,0,0,2173, + 2174,5,80,0,0,2174,2175,5,69,0,0,2175,2176,5,82,0,0,2176,2177,5, + 84,0,0,2177,2178,5,73,0,0,2178,2179,5,69,0,0,2179,2180,5,83,0,0, + 2180,404,1,0,0,0,2181,2182,5,80,0,0,2182,2183,5,85,0,0,2183,2184, + 5,82,0,0,2184,2185,5,71,0,0,2185,2186,5,69,0,0,2186,406,1,0,0,0, + 2187,2188,5,81,0,0,2188,2189,5,85,0,0,2189,2190,5,65,0,0,2190,2191, + 5,82,0,0,2191,2192,5,84,0,0,2192,2193,5,69,0,0,2193,2194,5,82,0, + 0,2194,408,1,0,0,0,2195,2196,5,81,0,0,2196,2197,5,85,0,0,2197,2198, + 5,69,0,0,2198,2199,5,82,0,0,2199,2200,5,89,0,0,2200,410,1,0,0,0, + 2201,2202,5,82,0,0,2202,2203,5,65,0,0,2203,2204,5,78,0,0,2204,2205, + 5,71,0,0,2205,2206,5,69,0,0,2206,412,1,0,0,0,2207,2208,5,82,0,0, + 2208,2209,5,69,0,0,2209,2210,5,67,0,0,2210,2211,5,79,0,0,2211,2212, + 5,82,0,0,2212,2213,5,68,0,0,2213,2214,5,82,0,0,2214,2215,5,69,0, + 0,2215,2216,5,65,0,0,2216,2217,5,68,0,0,2217,2218,5,69,0,0,2218, + 2219,5,82,0,0,2219,414,1,0,0,0,2220,2221,5,82,0,0,2221,2222,5,69, + 0,0,2222,2223,5,67,0,0,2223,2224,5,79,0,0,2224,2225,5,82,0,0,2225, + 2226,5,68,0,0,2226,2227,5,87,0,0,2227,2228,5,82,0,0,2228,2229,5, + 73,0,0,2229,2230,5,84,0,0,2230,2231,5,69,0,0,2231,2232,5,82,0,0, + 2232,416,1,0,0,0,2233,2234,5,82,0,0,2234,2235,5,69,0,0,2235,2236, + 5,67,0,0,2236,2237,5,79,0,0,2237,2238,5,86,0,0,2238,2239,5,69,0, + 0,2239,2240,5,82,0,0,2240,418,1,0,0,0,2241,2242,5,82,0,0,2242,2243, + 5,69,0,0,2243,2244,5,68,0,0,2244,2245,5,85,0,0,2245,2246,5,67,0, + 0,2246,2247,5,69,0,0,2247,420,1,0,0,0,2248,2249,5,82,0,0,2249,2250, + 5,69,0,0,2250,2251,5,70,0,0,2251,2252,5,69,0,0,2252,2253,5,82,0, + 0,2253,2254,5,69,0,0,2254,2255,5,78,0,0,2255,2256,5,67,0,0,2256, + 2257,5,69,0,0,2257,2258,5,83,0,0,2258,422,1,0,0,0,2259,2260,5,82, + 0,0,2260,2261,5,69,0,0,2261,2262,5,70,0,0,2262,2263,5,82,0,0,2263, + 2264,5,69,0,0,2264,2265,5,83,0,0,2265,2266,5,72,0,0,2266,424,1,0, + 0,0,2267,2268,5,82,0,0,2268,2269,5,69,0,0,2269,2270,5,78,0,0,2270, + 2271,5,65,0,0,2271,2272,5,77,0,0,2272,2273,5,69,0,0,2273,426,1,0, + 0,0,2274,2275,5,82,0,0,2275,2276,5,69,0,0,2276,2277,5,80,0,0,2277, + 2278,5,65,0,0,2278,2279,5,73,0,0,2279,2280,5,82,0,0,2280,428,1,0, + 0,0,2281,2282,5,82,0,0,2282,2283,5,69,0,0,2283,2284,5,80,0,0,2284, + 2285,5,69,0,0,2285,2286,5,65,0,0,2286,2287,5,84,0,0,2287,2288,5, + 65,0,0,2288,2289,5,66,0,0,2289,2290,5,76,0,0,2290,2291,5,69,0,0, + 2291,430,1,0,0,0,2292,2293,5,82,0,0,2293,2294,5,69,0,0,2294,2295, + 5,80,0,0,2295,2296,5,76,0,0,2296,2297,5,65,0,0,2297,2298,5,67,0, + 0,2298,2299,5,69,0,0,2299,432,1,0,0,0,2300,2301,5,82,0,0,2301,2302, + 5,69,0,0,2302,2303,5,83,0,0,2303,2304,5,69,0,0,2304,2305,5,84,0, + 0,2305,434,1,0,0,0,2306,2307,5,82,0,0,2307,2308,5,69,0,0,2308,2309, + 5,83,0,0,2309,2310,5,80,0,0,2310,2311,5,69,0,0,2311,2312,5,67,0, + 0,2312,2313,5,84,0,0,2313,436,1,0,0,0,2314,2315,5,82,0,0,2315,2316, + 5,69,0,0,2316,2317,5,83,0,0,2317,2318,5,84,0,0,2318,2319,5,82,0, + 0,2319,2320,5,73,0,0,2320,2321,5,67,0,0,2321,2322,5,84,0,0,2322, + 438,1,0,0,0,2323,2324,5,82,0,0,2324,2325,5,69,0,0,2325,2326,5,86, + 0,0,2326,2327,5,79,0,0,2327,2328,5,75,0,0,2328,2329,5,69,0,0,2329, + 440,1,0,0,0,2330,2331,5,82,0,0,2331,2332,5,73,0,0,2332,2333,5,71, + 0,0,2333,2334,5,72,0,0,2334,2335,5,84,0,0,2335,442,1,0,0,0,2336, + 2337,5,82,0,0,2337,2338,5,76,0,0,2338,2339,5,73,0,0,2339,2340,5, + 75,0,0,2340,2348,5,69,0,0,2341,2342,5,82,0,0,2342,2343,5,69,0,0, + 2343,2344,5,71,0,0,2344,2345,5,69,0,0,2345,2346,5,88,0,0,2346,2348, + 5,80,0,0,2347,2336,1,0,0,0,2347,2341,1,0,0,0,2348,444,1,0,0,0,2349, + 2350,5,82,0,0,2350,2351,5,79,0,0,2351,2352,5,76,0,0,2352,2353,5, + 69,0,0,2353,446,1,0,0,0,2354,2355,5,82,0,0,2355,2356,5,79,0,0,2356, + 2357,5,76,0,0,2357,2358,5,69,0,0,2358,2359,5,83,0,0,2359,448,1,0, + 0,0,2360,2361,5,82,0,0,2361,2362,5,79,0,0,2362,2363,5,76,0,0,2363, + 2364,5,76,0,0,2364,2365,5,66,0,0,2365,2366,5,65,0,0,2366,2367,5, + 67,0,0,2367,2368,5,75,0,0,2368,450,1,0,0,0,2369,2370,5,82,0,0,2370, + 2371,5,79,0,0,2371,2372,5,76,0,0,2372,2373,5,76,0,0,2373,2374,5, + 85,0,0,2374,2375,5,80,0,0,2375,452,1,0,0,0,2376,2377,5,82,0,0,2377, + 2378,5,79,0,0,2378,2379,5,87,0,0,2379,454,1,0,0,0,2380,2381,5,82, + 0,0,2381,2382,5,79,0,0,2382,2383,5,87,0,0,2383,2384,5,83,0,0,2384, + 456,1,0,0,0,2385,2386,5,83,0,0,2386,2387,5,69,0,0,2387,2388,5,67, + 0,0,2388,2389,5,79,0,0,2389,2390,5,78,0,0,2390,2391,5,68,0,0,2391, + 458,1,0,0,0,2392,2393,5,83,0,0,2393,2394,5,69,0,0,2394,2395,5,67, + 0,0,2395,2396,5,79,0,0,2396,2397,5,78,0,0,2397,2398,5,68,0,0,2398, + 2399,5,83,0,0,2399,460,1,0,0,0,2400,2401,5,83,0,0,2401,2402,5,67, + 0,0,2402,2403,5,72,0,0,2403,2404,5,69,0,0,2404,2405,5,77,0,0,2405, + 2406,5,65,0,0,2406,462,1,0,0,0,2407,2408,5,83,0,0,2408,2409,5,67, + 0,0,2409,2410,5,72,0,0,2410,2411,5,69,0,0,2411,2412,5,77,0,0,2412, + 2413,5,65,0,0,2413,2414,5,83,0,0,2414,464,1,0,0,0,2415,2416,5,83, + 0,0,2416,2417,5,69,0,0,2417,2418,5,76,0,0,2418,2419,5,69,0,0,2419, + 2420,5,67,0,0,2420,2421,5,84,0,0,2421,466,1,0,0,0,2422,2423,5,83, + 0,0,2423,2424,5,69,0,0,2424,2425,5,77,0,0,2425,2426,5,73,0,0,2426, + 468,1,0,0,0,2427,2428,5,83,0,0,2428,2429,5,69,0,0,2429,2430,5,80, + 0,0,2430,2431,5,65,0,0,2431,2432,5,82,0,0,2432,2433,5,65,0,0,2433, + 2434,5,84,0,0,2434,2435,5,69,0,0,2435,2436,5,68,0,0,2436,470,1,0, + 0,0,2437,2438,5,83,0,0,2438,2439,5,69,0,0,2439,2440,5,82,0,0,2440, + 2441,5,68,0,0,2441,2442,5,69,0,0,2442,472,1,0,0,0,2443,2444,5,83, + 0,0,2444,2445,5,69,0,0,2445,2446,5,82,0,0,2446,2447,5,68,0,0,2447, + 2448,5,69,0,0,2448,2449,5,80,0,0,2449,2450,5,82,0,0,2450,2451,5, + 79,0,0,2451,2452,5,80,0,0,2452,2453,5,69,0,0,2453,2454,5,82,0,0, + 2454,2455,5,84,0,0,2455,2456,5,73,0,0,2456,2457,5,69,0,0,2457,2458, + 5,83,0,0,2458,474,1,0,0,0,2459,2460,5,83,0,0,2460,2461,5,69,0,0, + 2461,2462,5,83,0,0,2462,2463,5,83,0,0,2463,2464,5,73,0,0,2464,2465, + 5,79,0,0,2465,2466,5,78,0,0,2466,2467,5,95,0,0,2467,2468,5,85,0, + 0,2468,2469,5,83,0,0,2469,2470,5,69,0,0,2470,2471,5,82,0,0,2471, + 476,1,0,0,0,2472,2473,5,83,0,0,2473,2474,5,69,0,0,2474,2475,5,84, + 0,0,2475,478,1,0,0,0,2476,2477,5,77,0,0,2477,2478,5,73,0,0,2478, + 2479,5,78,0,0,2479,2480,5,85,0,0,2480,2481,5,83,0,0,2481,480,1,0, + 0,0,2482,2483,5,83,0,0,2483,2484,5,69,0,0,2484,2485,5,84,0,0,2485, + 2486,5,83,0,0,2486,482,1,0,0,0,2487,2488,5,83,0,0,2488,2489,5,72, + 0,0,2489,2490,5,79,0,0,2490,2491,5,87,0,0,2491,484,1,0,0,0,2492, + 2493,5,83,0,0,2493,2494,5,75,0,0,2494,2495,5,69,0,0,2495,2496,5, + 87,0,0,2496,2497,5,69,0,0,2497,2498,5,68,0,0,2498,486,1,0,0,0,2499, + 2500,5,83,0,0,2500,2501,5,79,0,0,2501,2502,5,77,0,0,2502,2503,5, + 69,0,0,2503,488,1,0,0,0,2504,2505,5,83,0,0,2505,2506,5,79,0,0,2506, + 2507,5,82,0,0,2507,2508,5,84,0,0,2508,490,1,0,0,0,2509,2510,5,83, + 0,0,2510,2511,5,79,0,0,2511,2512,5,82,0,0,2512,2513,5,84,0,0,2513, + 2514,5,69,0,0,2514,2515,5,68,0,0,2515,492,1,0,0,0,2516,2517,5,83, + 0,0,2517,2518,5,79,0,0,2518,2519,5,85,0,0,2519,2520,5,82,0,0,2520, + 2521,5,67,0,0,2521,2522,5,69,0,0,2522,494,1,0,0,0,2523,2524,5,83, + 0,0,2524,2525,5,84,0,0,2525,2526,5,65,0,0,2526,2527,5,82,0,0,2527, + 2528,5,84,0,0,2528,496,1,0,0,0,2529,2530,5,83,0,0,2530,2531,5,84, + 0,0,2531,2532,5,65,0,0,2532,2533,5,84,0,0,2533,2534,5,73,0,0,2534, + 2535,5,83,0,0,2535,2536,5,84,0,0,2536,2537,5,73,0,0,2537,2538,5, + 67,0,0,2538,2539,5,83,0,0,2539,498,1,0,0,0,2540,2541,5,83,0,0,2541, + 2542,5,84,0,0,2542,2543,5,79,0,0,2543,2544,5,82,0,0,2544,2545,5, + 69,0,0,2545,2546,5,68,0,0,2546,500,1,0,0,0,2547,2548,5,83,0,0,2548, + 2549,5,84,0,0,2549,2550,5,82,0,0,2550,2551,5,65,0,0,2551,2552,5, + 84,0,0,2552,2553,5,73,0,0,2553,2554,5,70,0,0,2554,2555,5,89,0,0, + 2555,502,1,0,0,0,2556,2557,5,83,0,0,2557,2558,5,84,0,0,2558,2559, + 5,82,0,0,2559,2560,5,85,0,0,2560,2561,5,67,0,0,2561,2562,5,84,0, + 0,2562,504,1,0,0,0,2563,2564,5,83,0,0,2564,2565,5,85,0,0,2565,2566, + 5,66,0,0,2566,2567,5,83,0,0,2567,2568,5,84,0,0,2568,2569,5,82,0, + 0,2569,506,1,0,0,0,2570,2571,5,83,0,0,2571,2572,5,85,0,0,2572,2573, + 5,66,0,0,2573,2574,5,83,0,0,2574,2575,5,84,0,0,2575,2576,5,82,0, + 0,2576,2577,5,73,0,0,2577,2578,5,78,0,0,2578,2579,5,71,0,0,2579, + 508,1,0,0,0,2580,2581,5,83,0,0,2581,2582,5,89,0,0,2582,2583,5,78, + 0,0,2583,2584,5,67,0,0,2584,510,1,0,0,0,2585,2586,5,83,0,0,2586, + 2587,5,89,0,0,2587,2588,5,83,0,0,2588,2589,5,84,0,0,2589,2590,5, + 69,0,0,2590,2591,5,77,0,0,2591,2592,5,95,0,0,2592,2593,5,84,0,0, + 2593,2594,5,73,0,0,2594,2595,5,77,0,0,2595,2596,5,69,0,0,2596,512, + 1,0,0,0,2597,2598,5,83,0,0,2598,2599,5,89,0,0,2599,2600,5,83,0,0, + 2600,2601,5,84,0,0,2601,2602,5,69,0,0,2602,2603,5,77,0,0,2603,2604, + 5,95,0,0,2604,2605,5,86,0,0,2605,2606,5,69,0,0,2606,2607,5,82,0, + 0,2607,2608,5,83,0,0,2608,2609,5,73,0,0,2609,2610,5,79,0,0,2610, + 2611,5,78,0,0,2611,514,1,0,0,0,2612,2613,5,84,0,0,2613,2614,5,65, + 0,0,2614,2615,5,66,0,0,2615,2616,5,76,0,0,2616,2617,5,69,0,0,2617, + 516,1,0,0,0,2618,2619,5,84,0,0,2619,2620,5,65,0,0,2620,2621,5,66, + 0,0,2621,2622,5,76,0,0,2622,2623,5,69,0,0,2623,2624,5,83,0,0,2624, + 518,1,0,0,0,2625,2626,5,84,0,0,2626,2627,5,65,0,0,2627,2628,5,66, + 0,0,2628,2629,5,76,0,0,2629,2630,5,69,0,0,2630,2631,5,83,0,0,2631, + 2632,5,65,0,0,2632,2633,5,77,0,0,2633,2634,5,80,0,0,2634,2635,5, + 76,0,0,2635,2636,5,69,0,0,2636,520,1,0,0,0,2637,2638,5,84,0,0,2638, + 2639,5,65,0,0,2639,2640,5,82,0,0,2640,2641,5,71,0,0,2641,2642,5, + 69,0,0,2642,2643,5,84,0,0,2643,522,1,0,0,0,2644,2645,5,84,0,0,2645, + 2646,5,66,0,0,2646,2647,5,76,0,0,2647,2648,5,80,0,0,2648,2649,5, + 82,0,0,2649,2650,5,79,0,0,2650,2651,5,80,0,0,2651,2652,5,69,0,0, + 2652,2653,5,82,0,0,2653,2654,5,84,0,0,2654,2655,5,73,0,0,2655,2656, + 5,69,0,0,2656,2657,5,83,0,0,2657,524,1,0,0,0,2658,2659,5,84,0,0, + 2659,2660,5,69,0,0,2660,2661,5,77,0,0,2661,2662,5,80,0,0,2662,2663, + 5,79,0,0,2663,2664,5,82,0,0,2664,2665,5,65,0,0,2665,2666,5,82,0, + 0,2666,2672,5,89,0,0,2667,2668,5,84,0,0,2668,2669,5,69,0,0,2669, + 2670,5,77,0,0,2670,2672,5,80,0,0,2671,2658,1,0,0,0,2671,2667,1,0, + 0,0,2672,526,1,0,0,0,2673,2674,5,84,0,0,2674,2675,5,69,0,0,2675, + 2676,5,82,0,0,2676,2677,5,77,0,0,2677,2678,5,73,0,0,2678,2679,5, + 78,0,0,2679,2680,5,65,0,0,2680,2681,5,84,0,0,2681,2682,5,69,0,0, + 2682,2683,5,68,0,0,2683,528,1,0,0,0,2684,2685,5,84,0,0,2685,2686, + 5,72,0,0,2686,2687,5,69,0,0,2687,2688,5,78,0,0,2688,530,1,0,0,0, + 2689,2690,5,84,0,0,2690,2691,5,73,0,0,2691,2692,5,77,0,0,2692,2693, + 5,69,0,0,2693,532,1,0,0,0,2694,2695,5,84,0,0,2695,2696,5,73,0,0, + 2696,2697,5,77,0,0,2697,2698,5,69,0,0,2698,2699,5,83,0,0,2699,2700, + 5,84,0,0,2700,2701,5,65,0,0,2701,2702,5,77,0,0,2702,2703,5,80,0, + 0,2703,534,1,0,0,0,2704,2705,5,84,0,0,2705,2706,5,73,0,0,2706,2707, + 5,77,0,0,2707,2708,5,69,0,0,2708,2709,5,83,0,0,2709,2710,5,84,0, + 0,2710,2711,5,65,0,0,2711,2712,5,77,0,0,2712,2713,5,80,0,0,2713, + 2714,5,65,0,0,2714,2715,5,68,0,0,2715,2716,5,68,0,0,2716,536,1,0, + 0,0,2717,2718,5,84,0,0,2718,2719,5,73,0,0,2719,2720,5,77,0,0,2720, + 2721,5,69,0,0,2721,2722,5,83,0,0,2722,2723,5,84,0,0,2723,2724,5, + 65,0,0,2724,2725,5,77,0,0,2725,2726,5,80,0,0,2726,2727,5,68,0,0, + 2727,2728,5,73,0,0,2728,2729,5,70,0,0,2729,2730,5,70,0,0,2730,538, + 1,0,0,0,2731,2732,5,84,0,0,2732,2733,5,79,0,0,2733,540,1,0,0,0,2734, + 2735,5,84,0,0,2735,2736,5,79,0,0,2736,2737,5,85,0,0,2737,2738,5, + 67,0,0,2738,2739,5,72,0,0,2739,542,1,0,0,0,2740,2741,5,84,0,0,2741, + 2742,5,82,0,0,2742,2743,5,65,0,0,2743,2744,5,73,0,0,2744,2745,5, + 76,0,0,2745,2746,5,73,0,0,2746,2747,5,78,0,0,2747,2748,5,71,0,0, + 2748,544,1,0,0,0,2749,2750,5,84,0,0,2750,2751,5,82,0,0,2751,2752, + 5,65,0,0,2752,2753,5,78,0,0,2753,2754,5,83,0,0,2754,2755,5,65,0, + 0,2755,2756,5,67,0,0,2756,2757,5,84,0,0,2757,2758,5,73,0,0,2758, + 2759,5,79,0,0,2759,2760,5,78,0,0,2760,546,1,0,0,0,2761,2762,5,84, + 0,0,2762,2763,5,82,0,0,2763,2764,5,65,0,0,2764,2765,5,78,0,0,2765, + 2766,5,83,0,0,2766,2767,5,65,0,0,2767,2768,5,67,0,0,2768,2769,5, + 84,0,0,2769,2770,5,73,0,0,2770,2771,5,79,0,0,2771,2772,5,78,0,0, + 2772,2773,5,83,0,0,2773,548,1,0,0,0,2774,2775,5,84,0,0,2775,2776, + 5,82,0,0,2776,2777,5,65,0,0,2777,2778,5,78,0,0,2778,2779,5,83,0, + 0,2779,2780,5,70,0,0,2780,2781,5,79,0,0,2781,2782,5,82,0,0,2782, + 2783,5,77,0,0,2783,550,1,0,0,0,2784,2785,5,84,0,0,2785,2786,5,82, + 0,0,2786,2787,5,73,0,0,2787,2788,5,77,0,0,2788,552,1,0,0,0,2789, + 2790,5,84,0,0,2790,2791,5,82,0,0,2791,2792,5,85,0,0,2792,2793,5, + 69,0,0,2793,554,1,0,0,0,2794,2795,5,84,0,0,2795,2796,5,82,0,0,2796, + 2797,5,85,0,0,2797,2798,5,78,0,0,2798,2799,5,67,0,0,2799,2800,5, + 65,0,0,2800,2801,5,84,0,0,2801,2802,5,69,0,0,2802,556,1,0,0,0,2803, + 2804,5,84,0,0,2804,2805,5,82,0,0,2805,2806,5,89,0,0,2806,2807,5, + 95,0,0,2807,2808,5,67,0,0,2808,2809,5,65,0,0,2809,2810,5,83,0,0, + 2810,2811,5,84,0,0,2811,558,1,0,0,0,2812,2813,5,84,0,0,2813,2814, + 5,89,0,0,2814,2815,5,80,0,0,2815,2816,5,69,0,0,2816,560,1,0,0,0, + 2817,2818,5,85,0,0,2818,2819,5,78,0,0,2819,2820,5,65,0,0,2820,2821, + 5,82,0,0,2821,2822,5,67,0,0,2822,2823,5,72,0,0,2823,2824,5,73,0, + 0,2824,2825,5,86,0,0,2825,2826,5,69,0,0,2826,562,1,0,0,0,2827,2828, + 5,85,0,0,2828,2829,5,78,0,0,2829,2830,5,66,0,0,2830,2831,5,79,0, + 0,2831,2832,5,85,0,0,2832,2833,5,78,0,0,2833,2834,5,68,0,0,2834, + 2835,5,69,0,0,2835,2836,5,68,0,0,2836,564,1,0,0,0,2837,2838,5,85, + 0,0,2838,2839,5,78,0,0,2839,2840,5,67,0,0,2840,2841,5,65,0,0,2841, + 2842,5,67,0,0,2842,2843,5,72,0,0,2843,2844,5,69,0,0,2844,566,1,0, + 0,0,2845,2846,5,85,0,0,2846,2847,5,78,0,0,2847,2848,5,73,0,0,2848, + 2849,5,79,0,0,2849,2850,5,78,0,0,2850,568,1,0,0,0,2851,2852,5,85, + 0,0,2852,2853,5,78,0,0,2853,2854,5,73,0,0,2854,2855,5,81,0,0,2855, + 2856,5,85,0,0,2856,2857,5,69,0,0,2857,570,1,0,0,0,2858,2859,5,85, + 0,0,2859,2860,5,78,0,0,2860,2861,5,75,0,0,2861,2862,5,78,0,0,2862, + 2863,5,79,0,0,2863,2864,5,87,0,0,2864,2865,5,78,0,0,2865,572,1,0, + 0,0,2866,2867,5,85,0,0,2867,2868,5,78,0,0,2868,2869,5,76,0,0,2869, + 2870,5,79,0,0,2870,2871,5,67,0,0,2871,2872,5,75,0,0,2872,574,1,0, + 0,0,2873,2874,5,85,0,0,2874,2875,5,78,0,0,2875,2876,5,80,0,0,2876, + 2877,5,73,0,0,2877,2878,5,86,0,0,2878,2879,5,79,0,0,2879,2880,5, + 84,0,0,2880,576,1,0,0,0,2881,2882,5,85,0,0,2882,2883,5,78,0,0,2883, + 2884,5,83,0,0,2884,2885,5,69,0,0,2885,2886,5,84,0,0,2886,578,1,0, + 0,0,2887,2888,5,85,0,0,2888,2889,5,80,0,0,2889,2890,5,68,0,0,2890, + 2891,5,65,0,0,2891,2892,5,84,0,0,2892,2893,5,69,0,0,2893,580,1,0, + 0,0,2894,2895,5,85,0,0,2895,2896,5,83,0,0,2896,2897,5,69,0,0,2897, + 582,1,0,0,0,2898,2899,5,85,0,0,2899,2900,5,83,0,0,2900,2901,5,69, + 0,0,2901,2902,5,82,0,0,2902,584,1,0,0,0,2903,2904,5,85,0,0,2904, + 2905,5,83,0,0,2905,2906,5,73,0,0,2906,2907,5,78,0,0,2907,2908,5, + 71,0,0,2908,586,1,0,0,0,2909,2910,5,86,0,0,2910,2911,5,65,0,0,2911, + 2912,5,76,0,0,2912,2913,5,85,0,0,2913,2914,5,69,0,0,2914,2915,5, + 83,0,0,2915,588,1,0,0,0,2916,2917,5,86,0,0,2917,2918,5,69,0,0,2918, + 2919,5,82,0,0,2919,2920,5,83,0,0,2920,2921,5,73,0,0,2921,2922,5, + 79,0,0,2922,2923,5,78,0,0,2923,590,1,0,0,0,2924,2925,5,86,0,0,2925, + 2926,5,73,0,0,2926,2927,5,69,0,0,2927,2928,5,87,0,0,2928,592,1,0, + 0,0,2929,2930,5,86,0,0,2930,2931,5,73,0,0,2931,2932,5,69,0,0,2932, + 2933,5,87,0,0,2933,2934,5,83,0,0,2934,594,1,0,0,0,2935,2936,5,87, + 0,0,2936,2937,5,69,0,0,2937,2938,5,69,0,0,2938,2939,5,75,0,0,2939, + 596,1,0,0,0,2940,2941,5,87,0,0,2941,2942,5,69,0,0,2942,2943,5,69, + 0,0,2943,2944,5,75,0,0,2944,2945,5,83,0,0,2945,598,1,0,0,0,2946, + 2947,5,87,0,0,2947,2948,5,72,0,0,2948,2949,5,69,0,0,2949,2950,5, + 78,0,0,2950,600,1,0,0,0,2951,2952,5,87,0,0,2952,2953,5,72,0,0,2953, + 2954,5,69,0,0,2954,2955,5,82,0,0,2955,2956,5,69,0,0,2956,602,1,0, + 0,0,2957,2958,5,87,0,0,2958,2959,5,73,0,0,2959,2960,5,78,0,0,2960, + 2961,5,68,0,0,2961,2962,5,79,0,0,2962,2963,5,87,0,0,2963,604,1,0, + 0,0,2964,2965,5,87,0,0,2965,2966,5,73,0,0,2966,2967,5,84,0,0,2967, + 2968,5,72,0,0,2968,606,1,0,0,0,2969,2970,5,87,0,0,2970,2971,5,73, + 0,0,2971,2972,5,84,0,0,2972,2973,5,72,0,0,2973,2974,5,73,0,0,2974, + 2975,5,78,0,0,2975,608,1,0,0,0,2976,2977,5,89,0,0,2977,2978,5,69, + 0,0,2978,2979,5,65,0,0,2979,2980,5,82,0,0,2980,610,1,0,0,0,2981, + 2982,5,89,0,0,2982,2983,5,69,0,0,2983,2984,5,65,0,0,2984,2985,5, + 82,0,0,2985,2986,5,83,0,0,2986,612,1,0,0,0,2987,2988,5,90,0,0,2988, + 2989,5,79,0,0,2989,2990,5,78,0,0,2990,2991,5,69,0,0,2991,614,1,0, + 0,0,2992,2996,5,61,0,0,2993,2994,5,61,0,0,2994,2996,5,61,0,0,2995, + 2992,1,0,0,0,2995,2993,1,0,0,0,2996,616,1,0,0,0,2997,2998,5,60,0, + 0,2998,2999,5,61,0,0,2999,3000,5,62,0,0,3000,618,1,0,0,0,3001,3002, + 5,60,0,0,3002,3003,5,62,0,0,3003,620,1,0,0,0,3004,3005,5,33,0,0, + 3005,3006,5,61,0,0,3006,622,1,0,0,0,3007,3008,5,60,0,0,3008,624, + 1,0,0,0,3009,3010,5,60,0,0,3010,3014,5,61,0,0,3011,3012,5,33,0,0, + 3012,3014,5,62,0,0,3013,3009,1,0,0,0,3013,3011,1,0,0,0,3014,626, + 1,0,0,0,3015,3016,5,62,0,0,3016,628,1,0,0,0,3017,3018,5,62,0,0,3018, + 3022,5,61,0,0,3019,3020,5,33,0,0,3020,3022,5,60,0,0,3021,3017,1, + 0,0,0,3021,3019,1,0,0,0,3022,630,1,0,0,0,3023,3024,5,43,0,0,3024, + 632,1,0,0,0,3025,3026,5,45,0,0,3026,634,1,0,0,0,3027,3028,5,42,0, + 0,3028,636,1,0,0,0,3029,3030,5,47,0,0,3030,638,1,0,0,0,3031,3032, + 5,37,0,0,3032,640,1,0,0,0,3033,3034,5,126,0,0,3034,642,1,0,0,0,3035, + 3036,5,38,0,0,3036,644,1,0,0,0,3037,3038,5,124,0,0,3038,646,1,0, + 0,0,3039,3040,5,124,0,0,3040,3041,5,124,0,0,3041,648,1,0,0,0,3042, + 3043,5,94,0,0,3043,650,1,0,0,0,3044,3045,5,58,0,0,3045,652,1,0,0, + 0,3046,3047,5,45,0,0,3047,3048,5,62,0,0,3048,654,1,0,0,0,3049,3050, + 5,47,0,0,3050,3051,5,42,0,0,3051,3052,5,43,0,0,3052,656,1,0,0,0, + 3053,3054,5,42,0,0,3054,3055,5,47,0,0,3055,658,1,0,0,0,3056,3062, + 5,39,0,0,3057,3061,8,0,0,0,3058,3059,5,92,0,0,3059,3061,9,0,0,0, + 3060,3057,1,0,0,0,3060,3058,1,0,0,0,3061,3064,1,0,0,0,3062,3060, + 1,0,0,0,3062,3063,1,0,0,0,3063,3065,1,0,0,0,3064,3062,1,0,0,0,3065, + 3087,5,39,0,0,3066,3067,5,82,0,0,3067,3068,5,39,0,0,3068,3072,1, + 0,0,0,3069,3071,8,1,0,0,3070,3069,1,0,0,0,3071,3074,1,0,0,0,3072, + 3070,1,0,0,0,3072,3073,1,0,0,0,3073,3075,1,0,0,0,3074,3072,1,0,0, + 0,3075,3087,5,39,0,0,3076,3077,5,82,0,0,3077,3078,5,34,0,0,3078, + 3082,1,0,0,0,3079,3081,8,2,0,0,3080,3079,1,0,0,0,3081,3084,1,0,0, + 0,3082,3080,1,0,0,0,3082,3083,1,0,0,0,3083,3085,1,0,0,0,3084,3082, + 1,0,0,0,3085,3087,5,34,0,0,3086,3056,1,0,0,0,3086,3066,1,0,0,0,3086, + 3076,1,0,0,0,3087,660,1,0,0,0,3088,3094,5,34,0,0,3089,3093,8,3,0, + 0,3090,3091,5,92,0,0,3091,3093,9,0,0,0,3092,3089,1,0,0,0,3092,3090, + 1,0,0,0,3093,3096,1,0,0,0,3094,3092,1,0,0,0,3094,3095,1,0,0,0,3095, + 3097,1,0,0,0,3096,3094,1,0,0,0,3097,3098,5,34,0,0,3098,662,1,0,0, + 0,3099,3101,3,689,344,0,3100,3099,1,0,0,0,3101,3102,1,0,0,0,3102, + 3100,1,0,0,0,3102,3103,1,0,0,0,3103,3104,1,0,0,0,3104,3105,5,76, + 0,0,3105,664,1,0,0,0,3106,3108,3,689,344,0,3107,3106,1,0,0,0,3108, + 3109,1,0,0,0,3109,3107,1,0,0,0,3109,3110,1,0,0,0,3110,3111,1,0,0, + 0,3111,3112,5,83,0,0,3112,666,1,0,0,0,3113,3115,3,689,344,0,3114, + 3113,1,0,0,0,3115,3116,1,0,0,0,3116,3114,1,0,0,0,3116,3117,1,0,0, + 0,3117,3118,1,0,0,0,3118,3119,5,89,0,0,3119,668,1,0,0,0,3120,3122, + 3,689,344,0,3121,3120,1,0,0,0,3122,3123,1,0,0,0,3123,3121,1,0,0, + 0,3123,3124,1,0,0,0,3124,670,1,0,0,0,3125,3127,3,689,344,0,3126, + 3125,1,0,0,0,3127,3128,1,0,0,0,3128,3126,1,0,0,0,3128,3129,1,0,0, + 0,3129,3130,1,0,0,0,3130,3131,3,687,343,0,3131,3137,1,0,0,0,3132, + 3133,3,685,342,0,3133,3134,3,687,343,0,3134,3135,4,335,0,0,3135, + 3137,1,0,0,0,3136,3126,1,0,0,0,3136,3132,1,0,0,0,3137,672,1,0,0, + 0,3138,3139,3,685,342,0,3139,3140,4,336,1,0,3140,674,1,0,0,0,3141, + 3143,3,689,344,0,3142,3141,1,0,0,0,3143,3144,1,0,0,0,3144,3142,1, + 0,0,0,3144,3145,1,0,0,0,3145,3147,1,0,0,0,3146,3148,3,687,343,0, + 3147,3146,1,0,0,0,3147,3148,1,0,0,0,3148,3149,1,0,0,0,3149,3150, + 5,70,0,0,3150,3159,1,0,0,0,3151,3153,3,685,342,0,3152,3154,3,687, + 343,0,3153,3152,1,0,0,0,3153,3154,1,0,0,0,3154,3155,1,0,0,0,3155, + 3156,5,70,0,0,3156,3157,4,337,2,0,3157,3159,1,0,0,0,3158,3142,1, + 0,0,0,3158,3151,1,0,0,0,3159,676,1,0,0,0,3160,3162,3,689,344,0,3161, + 3160,1,0,0,0,3162,3163,1,0,0,0,3163,3161,1,0,0,0,3163,3164,1,0,0, + 0,3164,3166,1,0,0,0,3165,3167,3,687,343,0,3166,3165,1,0,0,0,3166, + 3167,1,0,0,0,3167,3168,1,0,0,0,3168,3169,5,68,0,0,3169,3178,1,0, + 0,0,3170,3172,3,685,342,0,3171,3173,3,687,343,0,3172,3171,1,0,0, + 0,3172,3173,1,0,0,0,3173,3174,1,0,0,0,3174,3175,5,68,0,0,3175,3176, + 4,338,3,0,3176,3178,1,0,0,0,3177,3161,1,0,0,0,3177,3170,1,0,0,0, + 3178,678,1,0,0,0,3179,3181,3,689,344,0,3180,3179,1,0,0,0,3181,3182, + 1,0,0,0,3182,3180,1,0,0,0,3182,3183,1,0,0,0,3183,3185,1,0,0,0,3184, + 3186,3,687,343,0,3185,3184,1,0,0,0,3185,3186,1,0,0,0,3186,3187,1, + 0,0,0,3187,3188,5,66,0,0,3188,3189,5,68,0,0,3189,3200,1,0,0,0,3190, + 3192,3,685,342,0,3191,3193,3,687,343,0,3192,3191,1,0,0,0,3192,3193, + 1,0,0,0,3193,3194,1,0,0,0,3194,3195,5,66,0,0,3195,3196,5,68,0,0, + 3196,3197,1,0,0,0,3197,3198,4,339,4,0,3198,3200,1,0,0,0,3199,3180, + 1,0,0,0,3199,3190,1,0,0,0,3200,680,1,0,0,0,3201,3205,3,691,345,0, + 3202,3205,3,689,344,0,3203,3205,5,95,0,0,3204,3201,1,0,0,0,3204, + 3202,1,0,0,0,3204,3203,1,0,0,0,3205,3206,1,0,0,0,3206,3204,1,0,0, + 0,3206,3207,1,0,0,0,3207,682,1,0,0,0,3208,3214,5,96,0,0,3209,3213, + 8,4,0,0,3210,3211,5,96,0,0,3211,3213,5,96,0,0,3212,3209,1,0,0,0, + 3212,3210,1,0,0,0,3213,3216,1,0,0,0,3214,3212,1,0,0,0,3214,3215, + 1,0,0,0,3215,3217,1,0,0,0,3216,3214,1,0,0,0,3217,3218,5,96,0,0,3218, + 684,1,0,0,0,3219,3221,3,689,344,0,3220,3219,1,0,0,0,3221,3222,1, + 0,0,0,3222,3220,1,0,0,0,3222,3223,1,0,0,0,3223,3224,1,0,0,0,3224, + 3228,5,46,0,0,3225,3227,3,689,344,0,3226,3225,1,0,0,0,3227,3230, + 1,0,0,0,3228,3226,1,0,0,0,3228,3229,1,0,0,0,3229,3238,1,0,0,0,3230, + 3228,1,0,0,0,3231,3233,5,46,0,0,3232,3234,3,689,344,0,3233,3232, + 1,0,0,0,3234,3235,1,0,0,0,3235,3233,1,0,0,0,3235,3236,1,0,0,0,3236, + 3238,1,0,0,0,3237,3220,1,0,0,0,3237,3231,1,0,0,0,3238,686,1,0,0, + 0,3239,3241,5,69,0,0,3240,3242,7,5,0,0,3241,3240,1,0,0,0,3241,3242, + 1,0,0,0,3242,3244,1,0,0,0,3243,3245,3,689,344,0,3244,3243,1,0,0, + 0,3245,3246,1,0,0,0,3246,3244,1,0,0,0,3246,3247,1,0,0,0,3247,688, + 1,0,0,0,3248,3249,7,6,0,0,3249,690,1,0,0,0,3250,3251,7,7,0,0,3251, + 692,1,0,0,0,3252,3253,5,45,0,0,3253,3254,5,45,0,0,3254,3260,1,0, + 0,0,3255,3256,5,92,0,0,3256,3259,5,10,0,0,3257,3259,8,8,0,0,3258, + 3255,1,0,0,0,3258,3257,1,0,0,0,3259,3262,1,0,0,0,3260,3258,1,0,0, + 0,3260,3261,1,0,0,0,3261,3264,1,0,0,0,3262,3260,1,0,0,0,3263,3265, + 5,13,0,0,3264,3263,1,0,0,0,3264,3265,1,0,0,0,3265,3267,1,0,0,0,3266, + 3268,5,10,0,0,3267,3266,1,0,0,0,3267,3268,1,0,0,0,3268,3269,1,0, + 0,0,3269,3270,6,346,0,0,3270,694,1,0,0,0,3271,3272,5,47,0,0,3272, + 3273,5,42,0,0,3273,3274,1,0,0,0,3274,3279,4,347,5,0,3275,3278,3, + 695,347,0,3276,3278,9,0,0,0,3277,3275,1,0,0,0,3277,3276,1,0,0,0, + 3278,3281,1,0,0,0,3279,3280,1,0,0,0,3279,3277,1,0,0,0,3280,3286, + 1,0,0,0,3281,3279,1,0,0,0,3282,3283,5,42,0,0,3283,3287,5,47,0,0, + 3284,3285,6,347,1,0,3285,3287,5,0,0,1,3286,3282,1,0,0,0,3286,3284, + 1,0,0,0,3287,3288,1,0,0,0,3288,3289,6,347,0,0,3289,696,1,0,0,0,3290, + 3292,7,9,0,0,3291,3290,1,0,0,0,3292,3293,1,0,0,0,3293,3291,1,0,0, + 0,3293,3294,1,0,0,0,3294,3295,1,0,0,0,3295,3296,6,348,0,0,3296,698, + 1,0,0,0,3297,3298,9,0,0,0,3298,700,1,0,0,0,50,0,1935,2347,2671,2995, + 3013,3021,3060,3062,3072,3082,3086,3092,3094,3102,3109,3116,3123, + 3128,3136,3144,3147,3153,3158,3163,3166,3172,3177,3182,3185,3192, + 3199,3204,3206,3212,3214,3222,3228,3235,3237,3241,3246,3258,3260, + 3264,3267,3277,3279,3286,3293,2,0,1,0,1,347,0 + ] + +class SqlBaseLexer(Lexer): + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + SEMICOLON = 1 + LEFT_PAREN = 2 + RIGHT_PAREN = 3 + COMMA = 4 + DOT = 5 + LEFT_BRACKET = 6 + RIGHT_BRACKET = 7 + ADD = 8 + AFTER = 9 + ALL = 10 + ALTER = 11 + ALWAYS = 12 + ANALYZE = 13 + AND = 14 + ANTI = 15 + ANY = 16 + ANY_VALUE = 17 + ARCHIVE = 18 + ARRAY = 19 + AS = 20 + ASC = 21 + AT = 22 + AUTHORIZATION = 23 + BETWEEN = 24 + BOTH = 25 + BUCKET = 26 + BUCKETS = 27 + BY = 28 + CACHE = 29 + CASCADE = 30 + CASE = 31 + CAST = 32 + CATALOG = 33 + CATALOGS = 34 + CHANGE = 35 + CHECK = 36 + CLEAR = 37 + CLUSTER = 38 + CLUSTERED = 39 + CODEGEN = 40 + COLLATE = 41 + COLLECTION = 42 + COLUMN = 43 + COLUMNS = 44 + COMMENT = 45 + COMMIT = 46 + COMPACT = 47 + COMPACTIONS = 48 + COMPUTE = 49 + CONCATENATE = 50 + CONSTRAINT = 51 + COST = 52 + CREATE = 53 + CROSS = 54 + CUBE = 55 + CURRENT = 56 + CURRENT_DATE = 57 + CURRENT_TIME = 58 + CURRENT_TIMESTAMP = 59 + CURRENT_USER = 60 + DAY = 61 + DAYS = 62 + DAYOFYEAR = 63 + DATA = 64 + DATABASE = 65 + DATABASES = 66 + DATEADD = 67 + DATEDIFF = 68 + DBPROPERTIES = 69 + DEFAULT = 70 + DEFINED = 71 + DELETE = 72 + DELIMITED = 73 + DESC = 74 + DESCRIBE = 75 + DFS = 76 + DIRECTORIES = 77 + DIRECTORY = 78 + DISTINCT = 79 + DISTRIBUTE = 80 + DIV = 81 + DROP = 82 + ELSE = 83 + END = 84 + ESCAPE = 85 + ESCAPED = 86 + EXCEPT = 87 + EXCHANGE = 88 + EXCLUDE = 89 + EXISTS = 90 + EXPLAIN = 91 + EXPORT = 92 + EXTENDED = 93 + EXTERNAL = 94 + EXTRACT = 95 + FALSE = 96 + FETCH = 97 + FIELDS = 98 + FILTER = 99 + FILEFORMAT = 100 + FIRST = 101 + FOLLOWING = 102 + FOR = 103 + FOREIGN = 104 + FORMAT = 105 + FORMATTED = 106 + FROM = 107 + FULL = 108 + FUNCTION = 109 + FUNCTIONS = 110 + GENERATED = 111 + GLOBAL = 112 + GRANT = 113 + GROUP = 114 + GROUPING = 115 + HAVING = 116 + HOUR = 117 + HOURS = 118 + IF = 119 + IGNORE = 120 + IMPORT = 121 + IN = 122 + INCLUDE = 123 + INDEX = 124 + INDEXES = 125 + INNER = 126 + INPATH = 127 + INPUTFORMAT = 128 + INSERT = 129 + INTERSECT = 130 + INTERVAL = 131 + INTO = 132 + IS = 133 + ITEMS = 134 + JOIN = 135 + KEYS = 136 + LAST = 137 + LATERAL = 138 + LAZY = 139 + LEADING = 140 + LEFT = 141 + LIKE = 142 + ILIKE = 143 + LIMIT = 144 + LINES = 145 + LIST = 146 + LOAD = 147 + LOCAL = 148 + LOCATION = 149 + LOCK = 150 + LOCKS = 151 + LOGICAL = 152 + MACRO = 153 + MAP = 154 + MATCHED = 155 + MERGE = 156 + MICROSECOND = 157 + MICROSECONDS = 158 + MILLISECOND = 159 + MILLISECONDS = 160 + MINUTE = 161 + MINUTES = 162 + MONTH = 163 + MONTHS = 164 + MSCK = 165 + NAMESPACE = 166 + NAMESPACES = 167 + NANOSECOND = 168 + NANOSECONDS = 169 + NATURAL = 170 + NO = 171 + NOT = 172 + NULL = 173 + NULLS = 174 + OF = 175 + OFFSET = 176 + ON = 177 + ONLY = 178 + OPTION = 179 + OPTIONS = 180 + OR = 181 + ORDER = 182 + OUT = 183 + OUTER = 184 + OUTPUTFORMAT = 185 + OVER = 186 + OVERLAPS = 187 + OVERLAY = 188 + OVERWRITE = 189 + PARTITION = 190 + PARTITIONED = 191 + PARTITIONS = 192 + PERCENTILE_CONT = 193 + PERCENTILE_DISC = 194 + PERCENTLIT = 195 + PIVOT = 196 + PLACING = 197 + POSITION = 198 + PRECEDING = 199 + PRIMARY = 200 + PRINCIPALS = 201 + PROPERTIES = 202 + PURGE = 203 + QUARTER = 204 + QUERY = 205 + RANGE = 206 + RECORDREADER = 207 + RECORDWRITER = 208 + RECOVER = 209 + REDUCE = 210 + REFERENCES = 211 + REFRESH = 212 + RENAME = 213 + REPAIR = 214 + REPEATABLE = 215 + REPLACE = 216 + RESET = 217 + RESPECT = 218 + RESTRICT = 219 + REVOKE = 220 + RIGHT = 221 + RLIKE = 222 + ROLE = 223 + ROLES = 224 + ROLLBACK = 225 + ROLLUP = 226 + ROW = 227 + ROWS = 228 + SECOND = 229 + SECONDS = 230 + SCHEMA = 231 + SCHEMAS = 232 + SELECT = 233 + SEMI = 234 + SEPARATED = 235 + SERDE = 236 + SERDEPROPERTIES = 237 + SESSION_USER = 238 + SET = 239 + SETMINUS = 240 + SETS = 241 + SHOW = 242 + SKEWED = 243 + SOME = 244 + SORT = 245 + SORTED = 246 + SOURCE = 247 + START = 248 + STATISTICS = 249 + STORED = 250 + STRATIFY = 251 + STRUCT = 252 + SUBSTR = 253 + SUBSTRING = 254 + SYNC = 255 + SYSTEM_TIME = 256 + SYSTEM_VERSION = 257 + TABLE = 258 + TABLES = 259 + TABLESAMPLE = 260 + TARGET = 261 + TBLPROPERTIES = 262 + TEMPORARY = 263 + TERMINATED = 264 + THEN = 265 + TIME = 266 + TIMESTAMP = 267 + TIMESTAMPADD = 268 + TIMESTAMPDIFF = 269 + TO = 270 + TOUCH = 271 + TRAILING = 272 + TRANSACTION = 273 + TRANSACTIONS = 274 + TRANSFORM = 275 + TRIM = 276 + TRUE = 277 + TRUNCATE = 278 + TRY_CAST = 279 + TYPE = 280 + UNARCHIVE = 281 + UNBOUNDED = 282 + UNCACHE = 283 + UNION = 284 + UNIQUE = 285 + UNKNOWN = 286 + UNLOCK = 287 + UNPIVOT = 288 + UNSET = 289 + UPDATE = 290 + USE = 291 + USER = 292 + USING = 293 + VALUES = 294 + VERSION = 295 + VIEW = 296 + VIEWS = 297 + WEEK = 298 + WEEKS = 299 + WHEN = 300 + WHERE = 301 + WINDOW = 302 + WITH = 303 + WITHIN = 304 + YEAR = 305 + YEARS = 306 + ZONE = 307 + EQ = 308 + NSEQ = 309 + NEQ = 310 + NEQJ = 311 + LT = 312 + LTE = 313 + GT = 314 + GTE = 315 + PLUS = 316 + MINUS = 317 + ASTERISK = 318 + SLASH = 319 + PERCENT = 320 + TILDE = 321 + AMPERSAND = 322 + PIPE = 323 + CONCAT_PIPE = 324 + HAT = 325 + COLON = 326 + ARROW = 327 + HENT_START = 328 + HENT_END = 329 + STRING = 330 + DOUBLEQUOTED_STRING = 331 + BIGINT_LITERAL = 332 + SMALLINT_LITERAL = 333 + TINYINT_LITERAL = 334 + INTEGER_VALUE = 335 + EXPONENT_VALUE = 336 + DECIMAL_VALUE = 337 + FLOAT_LITERAL = 338 + DOUBLE_LITERAL = 339 + BIGDECIMAL_LITERAL = 340 + IDENTIFIER = 341 + BACKQUOTED_IDENTIFIER = 342 + SIMPLE_COMMENT = 343 + BRACKETED_COMMENT = 344 + WS = 345 + UNRECOGNIZED = 346 + + channelNames = [ u"DEFAULT_TOKEN_CHANNEL", u"HIDDEN" ] + + modeNames = [ "DEFAULT_MODE" ] + + literalNames = [ "", + "';'", "'('", "')'", "','", "'.'", "'['", "']'", "'ADD'", "'AFTER'", + "'ALL'", "'ALTER'", "'ALWAYS'", "'ANALYZE'", "'AND'", "'ANTI'", + "'ANY'", "'ANY_VALUE'", "'ARCHIVE'", "'ARRAY'", "'AS'", "'ASC'", + "'AT'", "'AUTHORIZATION'", "'BETWEEN'", "'BOTH'", "'BUCKET'", + "'BUCKETS'", "'BY'", "'CACHE'", "'CASCADE'", "'CASE'", "'CAST'", + "'CATALOG'", "'CATALOGS'", "'CHANGE'", "'CHECK'", "'CLEAR'", + "'CLUSTER'", "'CLUSTERED'", "'CODEGEN'", "'COLLATE'", "'COLLECTION'", + "'COLUMN'", "'COLUMNS'", "'COMMENT'", "'COMMIT'", "'COMPACT'", + "'COMPACTIONS'", "'COMPUTE'", "'CONCATENATE'", "'CONSTRAINT'", + "'COST'", "'CREATE'", "'CROSS'", "'CUBE'", "'CURRENT'", "'CURRENT_DATE'", + "'CURRENT_TIME'", "'CURRENT_TIMESTAMP'", "'CURRENT_USER'", "'DAY'", + "'DAYS'", "'DAYOFYEAR'", "'DATA'", "'DATABASE'", "'DATABASES'", + "'DATEADD'", "'DATEDIFF'", "'DBPROPERTIES'", "'DEFAULT'", "'DEFINED'", + "'DELETE'", "'DELIMITED'", "'DESC'", "'DESCRIBE'", "'DFS'", + "'DIRECTORIES'", "'DIRECTORY'", "'DISTINCT'", "'DISTRIBUTE'", + "'DIV'", "'DROP'", "'ELSE'", "'END'", "'ESCAPE'", "'ESCAPED'", + "'EXCEPT'", "'EXCHANGE'", "'EXCLUDE'", "'EXISTS'", "'EXPLAIN'", + "'EXPORT'", "'EXTENDED'", "'EXTERNAL'", "'EXTRACT'", "'FALSE'", + "'FETCH'", "'FIELDS'", "'FILTER'", "'FILEFORMAT'", "'FIRST'", + "'FOLLOWING'", "'FOR'", "'FOREIGN'", "'FORMAT'", "'FORMATTED'", + "'FROM'", "'FULL'", "'FUNCTION'", "'FUNCTIONS'", "'GENERATED'", + "'GLOBAL'", "'GRANT'", "'GROUP'", "'GROUPING'", "'HAVING'", + "'HOUR'", "'HOURS'", "'IF'", "'IGNORE'", "'IMPORT'", "'IN'", + "'INCLUDE'", "'INDEX'", "'INDEXES'", "'INNER'", "'INPATH'", + "'INPUTFORMAT'", "'INSERT'", "'INTERSECT'", "'INTERVAL'", "'INTO'", + "'IS'", "'ITEMS'", "'JOIN'", "'KEYS'", "'LAST'", "'LATERAL'", + "'LAZY'", "'LEADING'", "'LEFT'", "'LIKE'", "'ILIKE'", "'LIMIT'", + "'LINES'", "'LIST'", "'LOAD'", "'LOCAL'", "'LOCATION'", "'LOCK'", + "'LOCKS'", "'LOGICAL'", "'MACRO'", "'MAP'", "'MATCHED'", "'MERGE'", + "'MICROSECOND'", "'MICROSECONDS'", "'MILLISECOND'", "'MILLISECONDS'", + "'MINUTE'", "'MINUTES'", "'MONTH'", "'MONTHS'", "'MSCK'", "'NAMESPACE'", + "'NAMESPACES'", "'NANOSECOND'", "'NANOSECONDS'", "'NATURAL'", + "'NO'", "'NULL'", "'NULLS'", "'OF'", "'OFFSET'", "'ON'", "'ONLY'", + "'OPTION'", "'OPTIONS'", "'OR'", "'ORDER'", "'OUT'", "'OUTER'", + "'OUTPUTFORMAT'", "'OVER'", "'OVERLAPS'", "'OVERLAY'", "'OVERWRITE'", + "'PARTITION'", "'PARTITIONED'", "'PARTITIONS'", "'PERCENTILE_CONT'", + "'PERCENTILE_DISC'", "'PERCENT'", "'PIVOT'", "'PLACING'", "'POSITION'", + "'PRECEDING'", "'PRIMARY'", "'PRINCIPALS'", "'PROPERTIES'", + "'PURGE'", "'QUARTER'", "'QUERY'", "'RANGE'", "'RECORDREADER'", + "'RECORDWRITER'", "'RECOVER'", "'REDUCE'", "'REFERENCES'", "'REFRESH'", + "'RENAME'", "'REPAIR'", "'REPEATABLE'", "'REPLACE'", "'RESET'", + "'RESPECT'", "'RESTRICT'", "'REVOKE'", "'RIGHT'", "'ROLE'", + "'ROLES'", "'ROLLBACK'", "'ROLLUP'", "'ROW'", "'ROWS'", "'SECOND'", + "'SECONDS'", "'SCHEMA'", "'SCHEMAS'", "'SELECT'", "'SEMI'", + "'SEPARATED'", "'SERDE'", "'SERDEPROPERTIES'", "'SESSION_USER'", + "'SET'", "'MINUS'", "'SETS'", "'SHOW'", "'SKEWED'", "'SOME'", + "'SORT'", "'SORTED'", "'SOURCE'", "'START'", "'STATISTICS'", + "'STORED'", "'STRATIFY'", "'STRUCT'", "'SUBSTR'", "'SUBSTRING'", + "'SYNC'", "'SYSTEM_TIME'", "'SYSTEM_VERSION'", "'TABLE'", "'TABLES'", + "'TABLESAMPLE'", "'TARGET'", "'TBLPROPERTIES'", "'TERMINATED'", + "'THEN'", "'TIME'", "'TIMESTAMP'", "'TIMESTAMPADD'", "'TIMESTAMPDIFF'", + "'TO'", "'TOUCH'", "'TRAILING'", "'TRANSACTION'", "'TRANSACTIONS'", + "'TRANSFORM'", "'TRIM'", "'TRUE'", "'TRUNCATE'", "'TRY_CAST'", + "'TYPE'", "'UNARCHIVE'", "'UNBOUNDED'", "'UNCACHE'", "'UNION'", + "'UNIQUE'", "'UNKNOWN'", "'UNLOCK'", "'UNPIVOT'", "'UNSET'", + "'UPDATE'", "'USE'", "'USER'", "'USING'", "'VALUES'", "'VERSION'", + "'VIEW'", "'VIEWS'", "'WEEK'", "'WEEKS'", "'WHEN'", "'WHERE'", + "'WINDOW'", "'WITH'", "'WITHIN'", "'YEAR'", "'YEARS'", "'ZONE'", + "'<=>'", "'<>'", "'!='", "'<'", "'>'", "'+'", "'-'", "'*'", + "'/'", "'%'", "'~'", "'&'", "'|'", "'||'", "'^'", "':'", "'->'", + "'/*+'", "'*/'" ] + + symbolicNames = [ "", + "SEMICOLON", "LEFT_PAREN", "RIGHT_PAREN", "COMMA", "DOT", "LEFT_BRACKET", + "RIGHT_BRACKET", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", + "AND", "ANTI", "ANY", "ANY_VALUE", "ARCHIVE", "ARRAY", "AS", + "ASC", "AT", "AUTHORIZATION", "BETWEEN", "BOTH", "BUCKET", "BUCKETS", + "BY", "CACHE", "CASCADE", "CASE", "CAST", "CATALOG", "CATALOGS", + "CHANGE", "CHECK", "CLEAR", "CLUSTER", "CLUSTERED", "CODEGEN", + "COLLATE", "COLLECTION", "COLUMN", "COLUMNS", "COMMENT", "COMMIT", + "COMPACT", "COMPACTIONS", "COMPUTE", "CONCATENATE", "CONSTRAINT", + "COST", "CREATE", "CROSS", "CUBE", "CURRENT", "CURRENT_DATE", + "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER", "DAY", + "DAYS", "DAYOFYEAR", "DATA", "DATABASE", "DATABASES", "DATEADD", + "DATEDIFF", "DBPROPERTIES", "DEFAULT", "DEFINED", "DELETE", + "DELIMITED", "DESC", "DESCRIBE", "DFS", "DIRECTORIES", "DIRECTORY", + "DISTINCT", "DISTRIBUTE", "DIV", "DROP", "ELSE", "END", "ESCAPE", + "ESCAPED", "EXCEPT", "EXCHANGE", "EXCLUDE", "EXISTS", "EXPLAIN", + "EXPORT", "EXTENDED", "EXTERNAL", "EXTRACT", "FALSE", "FETCH", + "FIELDS", "FILTER", "FILEFORMAT", "FIRST", "FOLLOWING", "FOR", + "FOREIGN", "FORMAT", "FORMATTED", "FROM", "FULL", "FUNCTION", + "FUNCTIONS", "GENERATED", "GLOBAL", "GRANT", "GROUP", "GROUPING", + "HAVING", "HOUR", "HOURS", "IF", "IGNORE", "IMPORT", "IN", "INCLUDE", + "INDEX", "INDEXES", "INNER", "INPATH", "INPUTFORMAT", "INSERT", + "INTERSECT", "INTERVAL", "INTO", "IS", "ITEMS", "JOIN", "KEYS", + "LAST", "LATERAL", "LAZY", "LEADING", "LEFT", "LIKE", "ILIKE", + "LIMIT", "LINES", "LIST", "LOAD", "LOCAL", "LOCATION", "LOCK", + "LOCKS", "LOGICAL", "MACRO", "MAP", "MATCHED", "MERGE", "MICROSECOND", + "MICROSECONDS", "MILLISECOND", "MILLISECONDS", "MINUTE", "MINUTES", + "MONTH", "MONTHS", "MSCK", "NAMESPACE", "NAMESPACES", "NANOSECOND", + "NANOSECONDS", "NATURAL", "NO", "NOT", "NULL", "NULLS", "OF", + "OFFSET", "ON", "ONLY", "OPTION", "OPTIONS", "OR", "ORDER", + "OUT", "OUTER", "OUTPUTFORMAT", "OVER", "OVERLAPS", "OVERLAY", + "OVERWRITE", "PARTITION", "PARTITIONED", "PARTITIONS", "PERCENTILE_CONT", + "PERCENTILE_DISC", "PERCENTLIT", "PIVOT", "PLACING", "POSITION", + "PRECEDING", "PRIMARY", "PRINCIPALS", "PROPERTIES", "PURGE", + "QUARTER", "QUERY", "RANGE", "RECORDREADER", "RECORDWRITER", + "RECOVER", "REDUCE", "REFERENCES", "REFRESH", "RENAME", "REPAIR", + "REPEATABLE", "REPLACE", "RESET", "RESPECT", "RESTRICT", "REVOKE", + "RIGHT", "RLIKE", "ROLE", "ROLES", "ROLLBACK", "ROLLUP", "ROW", + "ROWS", "SECOND", "SECONDS", "SCHEMA", "SCHEMAS", "SELECT", + "SEMI", "SEPARATED", "SERDE", "SERDEPROPERTIES", "SESSION_USER", + "SET", "SETMINUS", "SETS", "SHOW", "SKEWED", "SOME", "SORT", + "SORTED", "SOURCE", "START", "STATISTICS", "STORED", "STRATIFY", + "STRUCT", "SUBSTR", "SUBSTRING", "SYNC", "SYSTEM_TIME", "SYSTEM_VERSION", + "TABLE", "TABLES", "TABLESAMPLE", "TARGET", "TBLPROPERTIES", + "TEMPORARY", "TERMINATED", "THEN", "TIME", "TIMESTAMP", "TIMESTAMPADD", + "TIMESTAMPDIFF", "TO", "TOUCH", "TRAILING", "TRANSACTION", "TRANSACTIONS", + "TRANSFORM", "TRIM", "TRUE", "TRUNCATE", "TRY_CAST", "TYPE", + "UNARCHIVE", "UNBOUNDED", "UNCACHE", "UNION", "UNIQUE", "UNKNOWN", + "UNLOCK", "UNPIVOT", "UNSET", "UPDATE", "USE", "USER", "USING", + "VALUES", "VERSION", "VIEW", "VIEWS", "WEEK", "WEEKS", "WHEN", + "WHERE", "WINDOW", "WITH", "WITHIN", "YEAR", "YEARS", "ZONE", + "EQ", "NSEQ", "NEQ", "NEQJ", "LT", "LTE", "GT", "GTE", "PLUS", + "MINUS", "ASTERISK", "SLASH", "PERCENT", "TILDE", "AMPERSAND", + "PIPE", "CONCAT_PIPE", "HAT", "COLON", "ARROW", "HENT_START", + "HENT_END", "STRING", "DOUBLEQUOTED_STRING", "BIGINT_LITERAL", + "SMALLINT_LITERAL", "TINYINT_LITERAL", "INTEGER_VALUE", "EXPONENT_VALUE", + "DECIMAL_VALUE", "FLOAT_LITERAL", "DOUBLE_LITERAL", "BIGDECIMAL_LITERAL", + "IDENTIFIER", "BACKQUOTED_IDENTIFIER", "SIMPLE_COMMENT", "BRACKETED_COMMENT", + "WS", "UNRECOGNIZED" ] + + ruleNames = [ "SEMICOLON", "LEFT_PAREN", "RIGHT_PAREN", "COMMA", "DOT", + "LEFT_BRACKET", "RIGHT_BRACKET", "ADD", "AFTER", "ALL", + "ALTER", "ALWAYS", "ANALYZE", "AND", "ANTI", "ANY", "ANY_VALUE", + "ARCHIVE", "ARRAY", "AS", "ASC", "AT", "AUTHORIZATION", + "BETWEEN", "BOTH", "BUCKET", "BUCKETS", "BY", "CACHE", + "CASCADE", "CASE", "CAST", "CATALOG", "CATALOGS", "CHANGE", + "CHECK", "CLEAR", "CLUSTER", "CLUSTERED", "CODEGEN", "COLLATE", + "COLLECTION", "COLUMN", "COLUMNS", "COMMENT", "COMMIT", + "COMPACT", "COMPACTIONS", "COMPUTE", "CONCATENATE", "CONSTRAINT", + "COST", "CREATE", "CROSS", "CUBE", "CURRENT", "CURRENT_DATE", + "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER", "DAY", + "DAYS", "DAYOFYEAR", "DATA", "DATABASE", "DATABASES", + "DATEADD", "DATEDIFF", "DBPROPERTIES", "DEFAULT", "DEFINED", + "DELETE", "DELIMITED", "DESC", "DESCRIBE", "DFS", "DIRECTORIES", + "DIRECTORY", "DISTINCT", "DISTRIBUTE", "DIV", "DROP", + "ELSE", "END", "ESCAPE", "ESCAPED", "EXCEPT", "EXCHANGE", + "EXCLUDE", "EXISTS", "EXPLAIN", "EXPORT", "EXTENDED", + "EXTERNAL", "EXTRACT", "FALSE", "FETCH", "FIELDS", "FILTER", + "FILEFORMAT", "FIRST", "FOLLOWING", "FOR", "FOREIGN", + "FORMAT", "FORMATTED", "FROM", "FULL", "FUNCTION", "FUNCTIONS", + "GENERATED", "GLOBAL", "GRANT", "GROUP", "GROUPING", "HAVING", + "HOUR", "HOURS", "IF", "IGNORE", "IMPORT", "IN", "INCLUDE", + "INDEX", "INDEXES", "INNER", "INPATH", "INPUTFORMAT", + "INSERT", "INTERSECT", "INTERVAL", "INTO", "IS", "ITEMS", + "JOIN", "KEYS", "LAST", "LATERAL", "LAZY", "LEADING", + "LEFT", "LIKE", "ILIKE", "LIMIT", "LINES", "LIST", "LOAD", + "LOCAL", "LOCATION", "LOCK", "LOCKS", "LOGICAL", "MACRO", + "MAP", "MATCHED", "MERGE", "MICROSECOND", "MICROSECONDS", + "MILLISECOND", "MILLISECONDS", "MINUTE", "MINUTES", "MONTH", + "MONTHS", "MSCK", "NAMESPACE", "NAMESPACES", "NANOSECOND", + "NANOSECONDS", "NATURAL", "NO", "NOT", "NULL", "NULLS", + "OF", "OFFSET", "ON", "ONLY", "OPTION", "OPTIONS", "OR", + "ORDER", "OUT", "OUTER", "OUTPUTFORMAT", "OVER", "OVERLAPS", + "OVERLAY", "OVERWRITE", "PARTITION", "PARTITIONED", "PARTITIONS", + "PERCENTILE_CONT", "PERCENTILE_DISC", "PERCENTLIT", "PIVOT", + "PLACING", "POSITION", "PRECEDING", "PRIMARY", "PRINCIPALS", + "PROPERTIES", "PURGE", "QUARTER", "QUERY", "RANGE", "RECORDREADER", + "RECORDWRITER", "RECOVER", "REDUCE", "REFERENCES", "REFRESH", + "RENAME", "REPAIR", "REPEATABLE", "REPLACE", "RESET", + "RESPECT", "RESTRICT", "REVOKE", "RIGHT", "RLIKE", "ROLE", + "ROLES", "ROLLBACK", "ROLLUP", "ROW", "ROWS", "SECOND", + "SECONDS", "SCHEMA", "SCHEMAS", "SELECT", "SEMI", "SEPARATED", + "SERDE", "SERDEPROPERTIES", "SESSION_USER", "SET", "SETMINUS", + "SETS", "SHOW", "SKEWED", "SOME", "SORT", "SORTED", "SOURCE", + "START", "STATISTICS", "STORED", "STRATIFY", "STRUCT", + "SUBSTR", "SUBSTRING", "SYNC", "SYSTEM_TIME", "SYSTEM_VERSION", + "TABLE", "TABLES", "TABLESAMPLE", "TARGET", "TBLPROPERTIES", + "TEMPORARY", "TERMINATED", "THEN", "TIME", "TIMESTAMP", + "TIMESTAMPADD", "TIMESTAMPDIFF", "TO", "TOUCH", "TRAILING", + "TRANSACTION", "TRANSACTIONS", "TRANSFORM", "TRIM", "TRUE", + "TRUNCATE", "TRY_CAST", "TYPE", "UNARCHIVE", "UNBOUNDED", + "UNCACHE", "UNION", "UNIQUE", "UNKNOWN", "UNLOCK", "UNPIVOT", + "UNSET", "UPDATE", "USE", "USER", "USING", "VALUES", "VERSION", + "VIEW", "VIEWS", "WEEK", "WEEKS", "WHEN", "WHERE", "WINDOW", + "WITH", "WITHIN", "YEAR", "YEARS", "ZONE", "EQ", "NSEQ", + "NEQ", "NEQJ", "LT", "LTE", "GT", "GTE", "PLUS", "MINUS", + "ASTERISK", "SLASH", "PERCENT", "TILDE", "AMPERSAND", + "PIPE", "CONCAT_PIPE", "HAT", "COLON", "ARROW", "HENT_START", + "HENT_END", "STRING", "DOUBLEQUOTED_STRING", "BIGINT_LITERAL", + "SMALLINT_LITERAL", "TINYINT_LITERAL", "INTEGER_VALUE", + "EXPONENT_VALUE", "DECIMAL_VALUE", "FLOAT_LITERAL", "DOUBLE_LITERAL", + "BIGDECIMAL_LITERAL", "IDENTIFIER", "BACKQUOTED_IDENTIFIER", + "DECIMAL_DIGITS", "EXPONENT", "DIGIT", "LETTER", "SIMPLE_COMMENT", + "BRACKETED_COMMENT", "WS", "UNRECOGNIZED" ] + + grammarFileName = "SqlBaseLexer.g4" + + def __init__(self, input=None, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.13.1") + self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache()) + self._actions = None + self._predicates = None + + # When true, parser should throw ParseException for unclosed bracketed comment. + has_unclosed_bracketed_comment: bool = False + + def isValidDecimal(self): + """ + Verify whether current token is a valid decimal token (which contains dot). + Returns true if the character that follows the token is not a digit or letter or underscore. + + * For example: + * For char stream "2.3", "2." is not a valid decimal token, because it is followed by digit '3'. + * For char stream "2.3_", "2.3" is not a valid decimal token, because it is followed by '_'. + * For char stream "2.3W", "2.3" is not a valid decimal token, because it is followed by 'W'. + * For char stream "12.0D 34.E2+0.12 " 12.0D is a valid decimal token because it is followed + * by a space. 34.E2 is a valid decimal token because it is followed by symbol '+' + * which is not a digit or letter or underscore. + """ + nextChar = chr(self._input.LA(1)) + if ( + nextChar >= 'A' + and nextChar <= 'Z' + or nextChar >= '0' + and nextChar <= '9' + or nextChar == '_' + ): + return False + else: + return True + + def isHint(self) -> bool: + """ + This method will be called when we see '/*' and try to match it as a bracketed comment. + If the next character is '+', it should be parsed as hint later, and we cannot match + it as a bracketed comment. + Returns true if the next character is '+'. + """ + nextChar = chr(self._input.LA(1)) + if nextChar == '+': + return True + else: + return False + + def markUnclosedComment(self): + """ + This method will be called when the character stream ends and try to find out the + unclosed bracketed comment. + If the method be called, it means the end of the entire character stream match, + and we set the flag and fail later. + """ + self.has_unclosed_bracketed_comment = True + + def action(self, localctx:RuleContext, ruleIndex:int, actionIndex:int): + if self._actions is None: + actions = dict() + actions[347] = self.BRACKETED_COMMENT_action + self._actions = actions + action = self._actions.get(ruleIndex, None) + if action is not None: + action(localctx, actionIndex) + else: + raise Exception("No registered action for:" + str(ruleIndex)) + + + def BRACKETED_COMMENT_action(self, localctx:RuleContext , actionIndex:int): + if actionIndex == 0: + self.markUnclosedComment() + + def sempred(self, localctx:RuleContext, ruleIndex:int, predIndex:int): + if self._predicates is None: + preds = dict() + preds[335] = self.EXPONENT_VALUE_sempred + preds[336] = self.DECIMAL_VALUE_sempred + preds[337] = self.FLOAT_LITERAL_sempred + preds[338] = self.DOUBLE_LITERAL_sempred + preds[339] = self.BIGDECIMAL_LITERAL_sempred + preds[347] = self.BRACKETED_COMMENT_sempred + self._predicates = preds + pred = self._predicates.get(ruleIndex, None) + if pred is not None: + return pred(localctx, predIndex) + else: + raise Exception("No registered predicate for:" + str(ruleIndex)) + + def EXPONENT_VALUE_sempred(self, localctx:RuleContext, predIndex:int): + if predIndex == 0: + return self.isValidDecimal() + + def DECIMAL_VALUE_sempred(self, localctx:RuleContext, predIndex:int): + if predIndex == 1: + return self.isValidDecimal() + + def FLOAT_LITERAL_sempred(self, localctx:RuleContext, predIndex:int): + if predIndex == 2: + return self.isValidDecimal() + + def DOUBLE_LITERAL_sempred(self, localctx:RuleContext, predIndex:int): + if predIndex == 3: + return self.isValidDecimal() + + def BIGDECIMAL_LITERAL_sempred(self, localctx:RuleContext, predIndex:int): + if predIndex == 4: + return self.isValidDecimal() + + def BRACKETED_COMMENT_sempred(self, localctx:RuleContext, predIndex:int): + if predIndex == 5: + return not self.isHint() diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseLexer.tokens b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseLexer.tokens new file mode 100644 index 000000000..c76d66e32 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseLexer.tokens @@ -0,0 +1,669 @@ +SEMICOLON=1 +LEFT_PAREN=2 +RIGHT_PAREN=3 +COMMA=4 +DOT=5 +LEFT_BRACKET=6 +RIGHT_BRACKET=7 +ADD=8 +AFTER=9 +ALL=10 +ALTER=11 +ALWAYS=12 +ANALYZE=13 +AND=14 +ANTI=15 +ANY=16 +ANY_VALUE=17 +ARCHIVE=18 +ARRAY=19 +AS=20 +ASC=21 +AT=22 +AUTHORIZATION=23 +BETWEEN=24 +BOTH=25 +BUCKET=26 +BUCKETS=27 +BY=28 +CACHE=29 +CASCADE=30 +CASE=31 +CAST=32 +CATALOG=33 +CATALOGS=34 +CHANGE=35 +CHECK=36 +CLEAR=37 +CLUSTER=38 +CLUSTERED=39 +CODEGEN=40 +COLLATE=41 +COLLECTION=42 +COLUMN=43 +COLUMNS=44 +COMMENT=45 +COMMIT=46 +COMPACT=47 +COMPACTIONS=48 +COMPUTE=49 +CONCATENATE=50 +CONSTRAINT=51 +COST=52 +CREATE=53 +CROSS=54 +CUBE=55 +CURRENT=56 +CURRENT_DATE=57 +CURRENT_TIME=58 +CURRENT_TIMESTAMP=59 +CURRENT_USER=60 +DAY=61 +DAYS=62 +DAYOFYEAR=63 +DATA=64 +DATABASE=65 +DATABASES=66 +DATEADD=67 +DATEDIFF=68 +DBPROPERTIES=69 +DEFAULT=70 +DEFINED=71 +DELETE=72 +DELIMITED=73 +DESC=74 +DESCRIBE=75 +DFS=76 +DIRECTORIES=77 +DIRECTORY=78 +DISTINCT=79 +DISTRIBUTE=80 +DIV=81 +DROP=82 +ELSE=83 +END=84 +ESCAPE=85 +ESCAPED=86 +EXCEPT=87 +EXCHANGE=88 +EXCLUDE=89 +EXISTS=90 +EXPLAIN=91 +EXPORT=92 +EXTENDED=93 +EXTERNAL=94 +EXTRACT=95 +FALSE=96 +FETCH=97 +FIELDS=98 +FILTER=99 +FILEFORMAT=100 +FIRST=101 +FOLLOWING=102 +FOR=103 +FOREIGN=104 +FORMAT=105 +FORMATTED=106 +FROM=107 +FULL=108 +FUNCTION=109 +FUNCTIONS=110 +GENERATED=111 +GLOBAL=112 +GRANT=113 +GROUP=114 +GROUPING=115 +HAVING=116 +HOUR=117 +HOURS=118 +IF=119 +IGNORE=120 +IMPORT=121 +IN=122 +INCLUDE=123 +INDEX=124 +INDEXES=125 +INNER=126 +INPATH=127 +INPUTFORMAT=128 +INSERT=129 +INTERSECT=130 +INTERVAL=131 +INTO=132 +IS=133 +ITEMS=134 +JOIN=135 +KEYS=136 +LAST=137 +LATERAL=138 +LAZY=139 +LEADING=140 +LEFT=141 +LIKE=142 +ILIKE=143 +LIMIT=144 +LINES=145 +LIST=146 +LOAD=147 +LOCAL=148 +LOCATION=149 +LOCK=150 +LOCKS=151 +LOGICAL=152 +MACRO=153 +MAP=154 +MATCHED=155 +MERGE=156 +MICROSECOND=157 +MICROSECONDS=158 +MILLISECOND=159 +MILLISECONDS=160 +MINUTE=161 +MINUTES=162 +MONTH=163 +MONTHS=164 +MSCK=165 +NAMESPACE=166 +NAMESPACES=167 +NANOSECOND=168 +NANOSECONDS=169 +NATURAL=170 +NO=171 +NOT=172 +NULL=173 +NULLS=174 +OF=175 +OFFSET=176 +ON=177 +ONLY=178 +OPTION=179 +OPTIONS=180 +OR=181 +ORDER=182 +OUT=183 +OUTER=184 +OUTPUTFORMAT=185 +OVER=186 +OVERLAPS=187 +OVERLAY=188 +OVERWRITE=189 +PARTITION=190 +PARTITIONED=191 +PARTITIONS=192 +PERCENTILE_CONT=193 +PERCENTILE_DISC=194 +PERCENTLIT=195 +PIVOT=196 +PLACING=197 +POSITION=198 +PRECEDING=199 +PRIMARY=200 +PRINCIPALS=201 +PROPERTIES=202 +PURGE=203 +QUARTER=204 +QUERY=205 +RANGE=206 +RECORDREADER=207 +RECORDWRITER=208 +RECOVER=209 +REDUCE=210 +REFERENCES=211 +REFRESH=212 +RENAME=213 +REPAIR=214 +REPEATABLE=215 +REPLACE=216 +RESET=217 +RESPECT=218 +RESTRICT=219 +REVOKE=220 +RIGHT=221 +RLIKE=222 +ROLE=223 +ROLES=224 +ROLLBACK=225 +ROLLUP=226 +ROW=227 +ROWS=228 +SECOND=229 +SECONDS=230 +SCHEMA=231 +SCHEMAS=232 +SELECT=233 +SEMI=234 +SEPARATED=235 +SERDE=236 +SERDEPROPERTIES=237 +SESSION_USER=238 +SET=239 +SETMINUS=240 +SETS=241 +SHOW=242 +SKEWED=243 +SOME=244 +SORT=245 +SORTED=246 +SOURCE=247 +START=248 +STATISTICS=249 +STORED=250 +STRATIFY=251 +STRUCT=252 +SUBSTR=253 +SUBSTRING=254 +SYNC=255 +SYSTEM_TIME=256 +SYSTEM_VERSION=257 +TABLE=258 +TABLES=259 +TABLESAMPLE=260 +TARGET=261 +TBLPROPERTIES=262 +TEMPORARY=263 +TERMINATED=264 +THEN=265 +TIME=266 +TIMESTAMP=267 +TIMESTAMPADD=268 +TIMESTAMPDIFF=269 +TO=270 +TOUCH=271 +TRAILING=272 +TRANSACTION=273 +TRANSACTIONS=274 +TRANSFORM=275 +TRIM=276 +TRUE=277 +TRUNCATE=278 +TRY_CAST=279 +TYPE=280 +UNARCHIVE=281 +UNBOUNDED=282 +UNCACHE=283 +UNION=284 +UNIQUE=285 +UNKNOWN=286 +UNLOCK=287 +UNPIVOT=288 +UNSET=289 +UPDATE=290 +USE=291 +USER=292 +USING=293 +VALUES=294 +VERSION=295 +VIEW=296 +VIEWS=297 +WEEK=298 +WEEKS=299 +WHEN=300 +WHERE=301 +WINDOW=302 +WITH=303 +WITHIN=304 +YEAR=305 +YEARS=306 +ZONE=307 +EQ=308 +NSEQ=309 +NEQ=310 +NEQJ=311 +LT=312 +LTE=313 +GT=314 +GTE=315 +PLUS=316 +MINUS=317 +ASTERISK=318 +SLASH=319 +PERCENT=320 +TILDE=321 +AMPERSAND=322 +PIPE=323 +CONCAT_PIPE=324 +HAT=325 +COLON=326 +ARROW=327 +HENT_START=328 +HENT_END=329 +STRING=330 +DOUBLEQUOTED_STRING=331 +BIGINT_LITERAL=332 +SMALLINT_LITERAL=333 +TINYINT_LITERAL=334 +INTEGER_VALUE=335 +EXPONENT_VALUE=336 +DECIMAL_VALUE=337 +FLOAT_LITERAL=338 +DOUBLE_LITERAL=339 +BIGDECIMAL_LITERAL=340 +IDENTIFIER=341 +BACKQUOTED_IDENTIFIER=342 +SIMPLE_COMMENT=343 +BRACKETED_COMMENT=344 +WS=345 +UNRECOGNIZED=346 +';'=1 +'('=2 +')'=3 +','=4 +'.'=5 +'['=6 +']'=7 +'ADD'=8 +'AFTER'=9 +'ALL'=10 +'ALTER'=11 +'ALWAYS'=12 +'ANALYZE'=13 +'AND'=14 +'ANTI'=15 +'ANY'=16 +'ANY_VALUE'=17 +'ARCHIVE'=18 +'ARRAY'=19 +'AS'=20 +'ASC'=21 +'AT'=22 +'AUTHORIZATION'=23 +'BETWEEN'=24 +'BOTH'=25 +'BUCKET'=26 +'BUCKETS'=27 +'BY'=28 +'CACHE'=29 +'CASCADE'=30 +'CASE'=31 +'CAST'=32 +'CATALOG'=33 +'CATALOGS'=34 +'CHANGE'=35 +'CHECK'=36 +'CLEAR'=37 +'CLUSTER'=38 +'CLUSTERED'=39 +'CODEGEN'=40 +'COLLATE'=41 +'COLLECTION'=42 +'COLUMN'=43 +'COLUMNS'=44 +'COMMENT'=45 +'COMMIT'=46 +'COMPACT'=47 +'COMPACTIONS'=48 +'COMPUTE'=49 +'CONCATENATE'=50 +'CONSTRAINT'=51 +'COST'=52 +'CREATE'=53 +'CROSS'=54 +'CUBE'=55 +'CURRENT'=56 +'CURRENT_DATE'=57 +'CURRENT_TIME'=58 +'CURRENT_TIMESTAMP'=59 +'CURRENT_USER'=60 +'DAY'=61 +'DAYS'=62 +'DAYOFYEAR'=63 +'DATA'=64 +'DATABASE'=65 +'DATABASES'=66 +'DATEADD'=67 +'DATEDIFF'=68 +'DBPROPERTIES'=69 +'DEFAULT'=70 +'DEFINED'=71 +'DELETE'=72 +'DELIMITED'=73 +'DESC'=74 +'DESCRIBE'=75 +'DFS'=76 +'DIRECTORIES'=77 +'DIRECTORY'=78 +'DISTINCT'=79 +'DISTRIBUTE'=80 +'DIV'=81 +'DROP'=82 +'ELSE'=83 +'END'=84 +'ESCAPE'=85 +'ESCAPED'=86 +'EXCEPT'=87 +'EXCHANGE'=88 +'EXCLUDE'=89 +'EXISTS'=90 +'EXPLAIN'=91 +'EXPORT'=92 +'EXTENDED'=93 +'EXTERNAL'=94 +'EXTRACT'=95 +'FALSE'=96 +'FETCH'=97 +'FIELDS'=98 +'FILTER'=99 +'FILEFORMAT'=100 +'FIRST'=101 +'FOLLOWING'=102 +'FOR'=103 +'FOREIGN'=104 +'FORMAT'=105 +'FORMATTED'=106 +'FROM'=107 +'FULL'=108 +'FUNCTION'=109 +'FUNCTIONS'=110 +'GENERATED'=111 +'GLOBAL'=112 +'GRANT'=113 +'GROUP'=114 +'GROUPING'=115 +'HAVING'=116 +'HOUR'=117 +'HOURS'=118 +'IF'=119 +'IGNORE'=120 +'IMPORT'=121 +'IN'=122 +'INCLUDE'=123 +'INDEX'=124 +'INDEXES'=125 +'INNER'=126 +'INPATH'=127 +'INPUTFORMAT'=128 +'INSERT'=129 +'INTERSECT'=130 +'INTERVAL'=131 +'INTO'=132 +'IS'=133 +'ITEMS'=134 +'JOIN'=135 +'KEYS'=136 +'LAST'=137 +'LATERAL'=138 +'LAZY'=139 +'LEADING'=140 +'LEFT'=141 +'LIKE'=142 +'ILIKE'=143 +'LIMIT'=144 +'LINES'=145 +'LIST'=146 +'LOAD'=147 +'LOCAL'=148 +'LOCATION'=149 +'LOCK'=150 +'LOCKS'=151 +'LOGICAL'=152 +'MACRO'=153 +'MAP'=154 +'MATCHED'=155 +'MERGE'=156 +'MICROSECOND'=157 +'MICROSECONDS'=158 +'MILLISECOND'=159 +'MILLISECONDS'=160 +'MINUTE'=161 +'MINUTES'=162 +'MONTH'=163 +'MONTHS'=164 +'MSCK'=165 +'NAMESPACE'=166 +'NAMESPACES'=167 +'NANOSECOND'=168 +'NANOSECONDS'=169 +'NATURAL'=170 +'NO'=171 +'NULL'=173 +'NULLS'=174 +'OF'=175 +'OFFSET'=176 +'ON'=177 +'ONLY'=178 +'OPTION'=179 +'OPTIONS'=180 +'OR'=181 +'ORDER'=182 +'OUT'=183 +'OUTER'=184 +'OUTPUTFORMAT'=185 +'OVER'=186 +'OVERLAPS'=187 +'OVERLAY'=188 +'OVERWRITE'=189 +'PARTITION'=190 +'PARTITIONED'=191 +'PARTITIONS'=192 +'PERCENTILE_CONT'=193 +'PERCENTILE_DISC'=194 +'PERCENT'=195 +'PIVOT'=196 +'PLACING'=197 +'POSITION'=198 +'PRECEDING'=199 +'PRIMARY'=200 +'PRINCIPALS'=201 +'PROPERTIES'=202 +'PURGE'=203 +'QUARTER'=204 +'QUERY'=205 +'RANGE'=206 +'RECORDREADER'=207 +'RECORDWRITER'=208 +'RECOVER'=209 +'REDUCE'=210 +'REFERENCES'=211 +'REFRESH'=212 +'RENAME'=213 +'REPAIR'=214 +'REPEATABLE'=215 +'REPLACE'=216 +'RESET'=217 +'RESPECT'=218 +'RESTRICT'=219 +'REVOKE'=220 +'RIGHT'=221 +'ROLE'=223 +'ROLES'=224 +'ROLLBACK'=225 +'ROLLUP'=226 +'ROW'=227 +'ROWS'=228 +'SECOND'=229 +'SECONDS'=230 +'SCHEMA'=231 +'SCHEMAS'=232 +'SELECT'=233 +'SEMI'=234 +'SEPARATED'=235 +'SERDE'=236 +'SERDEPROPERTIES'=237 +'SESSION_USER'=238 +'SET'=239 +'MINUS'=240 +'SETS'=241 +'SHOW'=242 +'SKEWED'=243 +'SOME'=244 +'SORT'=245 +'SORTED'=246 +'SOURCE'=247 +'START'=248 +'STATISTICS'=249 +'STORED'=250 +'STRATIFY'=251 +'STRUCT'=252 +'SUBSTR'=253 +'SUBSTRING'=254 +'SYNC'=255 +'SYSTEM_TIME'=256 +'SYSTEM_VERSION'=257 +'TABLE'=258 +'TABLES'=259 +'TABLESAMPLE'=260 +'TARGET'=261 +'TBLPROPERTIES'=262 +'TERMINATED'=264 +'THEN'=265 +'TIME'=266 +'TIMESTAMP'=267 +'TIMESTAMPADD'=268 +'TIMESTAMPDIFF'=269 +'TO'=270 +'TOUCH'=271 +'TRAILING'=272 +'TRANSACTION'=273 +'TRANSACTIONS'=274 +'TRANSFORM'=275 +'TRIM'=276 +'TRUE'=277 +'TRUNCATE'=278 +'TRY_CAST'=279 +'TYPE'=280 +'UNARCHIVE'=281 +'UNBOUNDED'=282 +'UNCACHE'=283 +'UNION'=284 +'UNIQUE'=285 +'UNKNOWN'=286 +'UNLOCK'=287 +'UNPIVOT'=288 +'UNSET'=289 +'UPDATE'=290 +'USE'=291 +'USER'=292 +'USING'=293 +'VALUES'=294 +'VERSION'=295 +'VIEW'=296 +'VIEWS'=297 +'WEEK'=298 +'WEEKS'=299 +'WHEN'=300 +'WHERE'=301 +'WINDOW'=302 +'WITH'=303 +'WITHIN'=304 +'YEAR'=305 +'YEARS'=306 +'ZONE'=307 +'<=>'=309 +'<>'=310 +'!='=311 +'<'=312 +'>'=314 +'+'=316 +'-'=317 +'*'=318 +'/'=319 +'%'=320 +'~'=321 +'&'=322 +'|'=323 +'||'=324 +'^'=325 +':'=326 +'->'=327 +'/*+'=328 +'*/'=329 diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParser.interp b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParser.interp new file mode 100644 index 000000000..ae31a6f53 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParser.interp @@ -0,0 +1,878 @@ +token literal names: +null +';' +'(' +')' +',' +'.' +'[' +']' +'ADD' +'AFTER' +'ALL' +'ALTER' +'ALWAYS' +'ANALYZE' +'AND' +'ANTI' +'ANY' +'ANY_VALUE' +'ARCHIVE' +'ARRAY' +'AS' +'ASC' +'AT' +'AUTHORIZATION' +'BETWEEN' +'BOTH' +'BUCKET' +'BUCKETS' +'BY' +'CACHE' +'CASCADE' +'CASE' +'CAST' +'CATALOG' +'CATALOGS' +'CHANGE' +'CHECK' +'CLEAR' +'CLUSTER' +'CLUSTERED' +'CODEGEN' +'COLLATE' +'COLLECTION' +'COLUMN' +'COLUMNS' +'COMMENT' +'COMMIT' +'COMPACT' +'COMPACTIONS' +'COMPUTE' +'CONCATENATE' +'CONSTRAINT' +'COST' +'CREATE' +'CROSS' +'CUBE' +'CURRENT' +'CURRENT_DATE' +'CURRENT_TIME' +'CURRENT_TIMESTAMP' +'CURRENT_USER' +'DAY' +'DAYS' +'DAYOFYEAR' +'DATA' +'DATABASE' +'DATABASES' +'DATEADD' +'DATEDIFF' +'DBPROPERTIES' +'DEFAULT' +'DEFINED' +'DELETE' +'DELIMITED' +'DESC' +'DESCRIBE' +'DFS' +'DIRECTORIES' +'DIRECTORY' +'DISTINCT' +'DISTRIBUTE' +'DIV' +'DROP' +'ELSE' +'END' +'ESCAPE' +'ESCAPED' +'EXCEPT' +'EXCHANGE' +'EXCLUDE' +'EXISTS' +'EXPLAIN' +'EXPORT' +'EXTENDED' +'EXTERNAL' +'EXTRACT' +'FALSE' +'FETCH' +'FIELDS' +'FILTER' +'FILEFORMAT' +'FIRST' +'FOLLOWING' +'FOR' +'FOREIGN' +'FORMAT' +'FORMATTED' +'FROM' +'FULL' +'FUNCTION' +'FUNCTIONS' +'GENERATED' +'GLOBAL' +'GRANT' +'GROUP' +'GROUPING' +'HAVING' +'HOUR' +'HOURS' +'IF' +'IGNORE' +'IMPORT' +'IN' +'INCLUDE' +'INDEX' +'INDEXES' +'INNER' +'INPATH' +'INPUTFORMAT' +'INSERT' +'INTERSECT' +'INTERVAL' +'INTO' +'IS' +'ITEMS' +'JOIN' +'KEYS' +'LAST' +'LATERAL' +'LAZY' +'LEADING' +'LEFT' +'LIKE' +'ILIKE' +'LIMIT' +'LINES' +'LIST' +'LOAD' +'LOCAL' +'LOCATION' +'LOCK' +'LOCKS' +'LOGICAL' +'MACRO' +'MAP' +'MATCHED' +'MERGE' +'MICROSECOND' +'MICROSECONDS' +'MILLISECOND' +'MILLISECONDS' +'MINUTE' +'MINUTES' +'MONTH' +'MONTHS' +'MSCK' +'NAMESPACE' +'NAMESPACES' +'NANOSECOND' +'NANOSECONDS' +'NATURAL' +'NO' +null +'NULL' +'NULLS' +'OF' +'OFFSET' +'ON' +'ONLY' +'OPTION' +'OPTIONS' +'OR' +'ORDER' +'OUT' +'OUTER' +'OUTPUTFORMAT' +'OVER' +'OVERLAPS' +'OVERLAY' +'OVERWRITE' +'PARTITION' +'PARTITIONED' +'PARTITIONS' +'PERCENTILE_CONT' +'PERCENTILE_DISC' +'PERCENT' +'PIVOT' +'PLACING' +'POSITION' +'PRECEDING' +'PRIMARY' +'PRINCIPALS' +'PROPERTIES' +'PURGE' +'QUARTER' +'QUERY' +'RANGE' +'RECORDREADER' +'RECORDWRITER' +'RECOVER' +'REDUCE' +'REFERENCES' +'REFRESH' +'RENAME' +'REPAIR' +'REPEATABLE' +'REPLACE' +'RESET' +'RESPECT' +'RESTRICT' +'REVOKE' +'RIGHT' +null +'ROLE' +'ROLES' +'ROLLBACK' +'ROLLUP' +'ROW' +'ROWS' +'SECOND' +'SECONDS' +'SCHEMA' +'SCHEMAS' +'SELECT' +'SEMI' +'SEPARATED' +'SERDE' +'SERDEPROPERTIES' +'SESSION_USER' +'SET' +'MINUS' +'SETS' +'SHOW' +'SKEWED' +'SOME' +'SORT' +'SORTED' +'SOURCE' +'START' +'STATISTICS' +'STORED' +'STRATIFY' +'STRUCT' +'SUBSTR' +'SUBSTRING' +'SYNC' +'SYSTEM_TIME' +'SYSTEM_VERSION' +'TABLE' +'TABLES' +'TABLESAMPLE' +'TARGET' +'TBLPROPERTIES' +null +'TERMINATED' +'THEN' +'TIME' +'TIMESTAMP' +'TIMESTAMPADD' +'TIMESTAMPDIFF' +'TO' +'TOUCH' +'TRAILING' +'TRANSACTION' +'TRANSACTIONS' +'TRANSFORM' +'TRIM' +'TRUE' +'TRUNCATE' +'TRY_CAST' +'TYPE' +'UNARCHIVE' +'UNBOUNDED' +'UNCACHE' +'UNION' +'UNIQUE' +'UNKNOWN' +'UNLOCK' +'UNPIVOT' +'UNSET' +'UPDATE' +'USE' +'USER' +'USING' +'VALUES' +'VERSION' +'VIEW' +'VIEWS' +'WEEK' +'WEEKS' +'WHEN' +'WHERE' +'WINDOW' +'WITH' +'WITHIN' +'YEAR' +'YEARS' +'ZONE' +null +'<=>' +'<>' +'!=' +'<' +null +'>' +null +'+' +'-' +'*' +'/' +'%' +'~' +'&' +'|' +'||' +'^' +':' +'->' +'/*+' +'*/' +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null +null + +token symbolic names: +null +SEMICOLON +LEFT_PAREN +RIGHT_PAREN +COMMA +DOT +LEFT_BRACKET +RIGHT_BRACKET +ADD +AFTER +ALL +ALTER +ALWAYS +ANALYZE +AND +ANTI +ANY +ANY_VALUE +ARCHIVE +ARRAY +AS +ASC +AT +AUTHORIZATION +BETWEEN +BOTH +BUCKET +BUCKETS +BY +CACHE +CASCADE +CASE +CAST +CATALOG +CATALOGS +CHANGE +CHECK +CLEAR +CLUSTER +CLUSTERED +CODEGEN +COLLATE +COLLECTION +COLUMN +COLUMNS +COMMENT +COMMIT +COMPACT +COMPACTIONS +COMPUTE +CONCATENATE +CONSTRAINT +COST +CREATE +CROSS +CUBE +CURRENT +CURRENT_DATE +CURRENT_TIME +CURRENT_TIMESTAMP +CURRENT_USER +DAY +DAYS +DAYOFYEAR +DATA +DATABASE +DATABASES +DATEADD +DATEDIFF +DBPROPERTIES +DEFAULT +DEFINED +DELETE +DELIMITED +DESC +DESCRIBE +DFS +DIRECTORIES +DIRECTORY +DISTINCT +DISTRIBUTE +DIV +DROP +ELSE +END +ESCAPE +ESCAPED +EXCEPT +EXCHANGE +EXCLUDE +EXISTS +EXPLAIN +EXPORT +EXTENDED +EXTERNAL +EXTRACT +FALSE +FETCH +FIELDS +FILTER +FILEFORMAT +FIRST +FOLLOWING +FOR +FOREIGN +FORMAT +FORMATTED +FROM +FULL +FUNCTION +FUNCTIONS +GENERATED +GLOBAL +GRANT +GROUP +GROUPING +HAVING +HOUR +HOURS +IF +IGNORE +IMPORT +IN +INCLUDE +INDEX +INDEXES +INNER +INPATH +INPUTFORMAT +INSERT +INTERSECT +INTERVAL +INTO +IS +ITEMS +JOIN +KEYS +LAST +LATERAL +LAZY +LEADING +LEFT +LIKE +ILIKE +LIMIT +LINES +LIST +LOAD +LOCAL +LOCATION +LOCK +LOCKS +LOGICAL +MACRO +MAP +MATCHED +MERGE +MICROSECOND +MICROSECONDS +MILLISECOND +MILLISECONDS +MINUTE +MINUTES +MONTH +MONTHS +MSCK +NAMESPACE +NAMESPACES +NANOSECOND +NANOSECONDS +NATURAL +NO +NOT +NULL +NULLS +OF +OFFSET +ON +ONLY +OPTION +OPTIONS +OR +ORDER +OUT +OUTER +OUTPUTFORMAT +OVER +OVERLAPS +OVERLAY +OVERWRITE +PARTITION +PARTITIONED +PARTITIONS +PERCENTILE_CONT +PERCENTILE_DISC +PERCENTLIT +PIVOT +PLACING +POSITION +PRECEDING +PRIMARY +PRINCIPALS +PROPERTIES +PURGE +QUARTER +QUERY +RANGE +RECORDREADER +RECORDWRITER +RECOVER +REDUCE +REFERENCES +REFRESH +RENAME +REPAIR +REPEATABLE +REPLACE +RESET +RESPECT +RESTRICT +REVOKE +RIGHT +RLIKE +ROLE +ROLES +ROLLBACK +ROLLUP +ROW +ROWS +SECOND +SECONDS +SCHEMA +SCHEMAS +SELECT +SEMI +SEPARATED +SERDE +SERDEPROPERTIES +SESSION_USER +SET +SETMINUS +SETS +SHOW +SKEWED +SOME +SORT +SORTED +SOURCE +START +STATISTICS +STORED +STRATIFY +STRUCT +SUBSTR +SUBSTRING +SYNC +SYSTEM_TIME +SYSTEM_VERSION +TABLE +TABLES +TABLESAMPLE +TARGET +TBLPROPERTIES +TEMPORARY +TERMINATED +THEN +TIME +TIMESTAMP +TIMESTAMPADD +TIMESTAMPDIFF +TO +TOUCH +TRAILING +TRANSACTION +TRANSACTIONS +TRANSFORM +TRIM +TRUE +TRUNCATE +TRY_CAST +TYPE +UNARCHIVE +UNBOUNDED +UNCACHE +UNION +UNIQUE +UNKNOWN +UNLOCK +UNPIVOT +UNSET +UPDATE +USE +USER +USING +VALUES +VERSION +VIEW +VIEWS +WEEK +WEEKS +WHEN +WHERE +WINDOW +WITH +WITHIN +YEAR +YEARS +ZONE +EQ +NSEQ +NEQ +NEQJ +LT +LTE +GT +GTE +PLUS +MINUS +ASTERISK +SLASH +PERCENT +TILDE +AMPERSAND +PIPE +CONCAT_PIPE +HAT +COLON +ARROW +HENT_START +HENT_END +STRING +DOUBLEQUOTED_STRING +BIGINT_LITERAL +SMALLINT_LITERAL +TINYINT_LITERAL +INTEGER_VALUE +EXPONENT_VALUE +DECIMAL_VALUE +FLOAT_LITERAL +DOUBLE_LITERAL +BIGDECIMAL_LITERAL +IDENTIFIER +BACKQUOTED_IDENTIFIER +SIMPLE_COMMENT +BRACKETED_COMMENT +WS +UNRECOGNIZED + +rule names: +singleStatement +singleExpression +singleTableIdentifier +singleMultipartIdentifier +singleFunctionIdentifier +singleDataType +singleTableSchema +statement +timezone +configKey +configValue +unsupportedHiveNativeCommands +createTableHeader +replaceTableHeader +bucketSpec +skewSpec +locationSpec +commentSpec +insertInto +partitionSpecLocation +partitionSpec +partitionVal +namespace +namespaces +describeFuncName +describeColName +ctes +query +namedQuery +queryTerm +querySpecification +queryPrimary +tableProvider +createTableClauses +propertyList +property +propertyKey +propertyValue +constantList +nestedConstantList +createFileFormat +fileFormat +storageHandler +resource +dmlStatementNoWith +queryOrganization +multiInsertQueryBody +sortItem +fromStatement +fromStatementBody +transformClause +selectClause +setClause +matchedClause +notMatchedClause +notMatchedBySourceClause +matchedAction +notMatchedAction +notMatchedBySourceAction +assignmentList +assignment +whereClause +havingClause +hint +hintStatement +fromClause +temporalClause +aggregationClause +groupByClause +groupingAnalytics +groupingElement +groupingSet +pivotClause +pivotColumn +pivotValue +unpivotClause +unpivotNullClause +unpivotOperator +unpivotSingleValueColumnClause +unpivotMultiValueColumnClause +unpivotColumnSet +unpivotValueColumn +unpivotNameColumn +unpivotColumnAndAlias +unpivotColumn +unpivotAlias +lateralView +setQuantifier +relation +relationExtension +joinRelation +joinType +joinCriteria +sample +sampleMethod +identifierList +identifierSeq +orderedIdentifierList +orderedIdentifier +identifierCommentList +identifierComment +relationPrimary +inlineTable +functionTable +tableAlias +rowFormat +multipartIdentifierList +multipartIdentifier +multipartIdentifierPropertyList +multipartIdentifierProperty +tableIdentifier +functionIdentifier +namedExpression +namedExpressionSeq +partitionFieldList +partitionField +transform +transformArgument +expression +expressionSeq +booleanExpression +predicate +valueExpression +datetimeUnit +primaryExpression +constant +comparisonOperator +arithmeticOperator +predicateOperator +booleanValue +interval +errorCapturingMultiUnitsInterval +multiUnitsInterval +errorCapturingUnitToUnitInterval +unitToUnitInterval +intervalValue +unitInMultiUnits +unitInUnitToUnit +colPosition +dataType +qualifiedColTypeWithPositionList +qualifiedColTypeWithPosition +colDefinitionDescriptorWithPosition +defaultExpression +colTypeList +colType +createOrReplaceTableColTypeList +createOrReplaceTableColType +colDefinitionOption +generationExpression +complexColTypeList +complexColType +whenClause +windowClause +namedWindow +windowSpec +windowFrame +frameBound +qualifiedNameList +functionName +qualifiedName +errorCapturingIdentifier +errorCapturingIdentifierExtra +identifier +strictIdentifier +quotedIdentifier +backQuotedIdentifier +number +alterColumnAction +stringLit +comment +version +ansiNonReserved +strictNonReserved +nonReserved + + +atn: +[4, 1, 346, 3549, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 2, 83, 7, 83, 2, 84, 7, 84, 2, 85, 7, 85, 2, 86, 7, 86, 2, 87, 7, 87, 2, 88, 7, 88, 2, 89, 7, 89, 2, 90, 7, 90, 2, 91, 7, 91, 2, 92, 7, 92, 2, 93, 7, 93, 2, 94, 7, 94, 2, 95, 7, 95, 2, 96, 7, 96, 2, 97, 7, 97, 2, 98, 7, 98, 2, 99, 7, 99, 2, 100, 7, 100, 2, 101, 7, 101, 2, 102, 7, 102, 2, 103, 7, 103, 2, 104, 7, 104, 2, 105, 7, 105, 2, 106, 7, 106, 2, 107, 7, 107, 2, 108, 7, 108, 2, 109, 7, 109, 2, 110, 7, 110, 2, 111, 7, 111, 2, 112, 7, 112, 2, 113, 7, 113, 2, 114, 7, 114, 2, 115, 7, 115, 2, 116, 7, 116, 2, 117, 7, 117, 2, 118, 7, 118, 2, 119, 7, 119, 2, 120, 7, 120, 2, 121, 7, 121, 2, 122, 7, 122, 2, 123, 7, 123, 2, 124, 7, 124, 2, 125, 7, 125, 2, 126, 7, 126, 2, 127, 7, 127, 2, 128, 7, 128, 2, 129, 7, 129, 2, 130, 7, 130, 2, 131, 7, 131, 2, 132, 7, 132, 2, 133, 7, 133, 2, 134, 7, 134, 2, 135, 7, 135, 2, 136, 7, 136, 2, 137, 7, 137, 2, 138, 7, 138, 2, 139, 7, 139, 2, 140, 7, 140, 2, 141, 7, 141, 2, 142, 7, 142, 2, 143, 7, 143, 2, 144, 7, 144, 2, 145, 7, 145, 2, 146, 7, 146, 2, 147, 7, 147, 2, 148, 7, 148, 2, 149, 7, 149, 2, 150, 7, 150, 2, 151, 7, 151, 2, 152, 7, 152, 2, 153, 7, 153, 2, 154, 7, 154, 2, 155, 7, 155, 2, 156, 7, 156, 2, 157, 7, 157, 2, 158, 7, 158, 2, 159, 7, 159, 2, 160, 7, 160, 2, 161, 7, 161, 2, 162, 7, 162, 2, 163, 7, 163, 2, 164, 7, 164, 2, 165, 7, 165, 2, 166, 7, 166, 2, 167, 7, 167, 2, 168, 7, 168, 2, 169, 7, 169, 2, 170, 7, 170, 2, 171, 7, 171, 2, 172, 7, 172, 2, 173, 7, 173, 2, 174, 7, 174, 1, 0, 1, 0, 5, 0, 353, 8, 0, 10, 0, 12, 0, 356, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 3, 7, 380, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 393, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 400, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 408, 8, 7, 10, 7, 12, 7, 411, 9, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 430, 8, 7, 1, 7, 1, 7, 3, 7, 434, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 440, 8, 7, 1, 7, 3, 7, 443, 8, 7, 1, 7, 3, 7, 446, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 453, 8, 7, 1, 7, 3, 7, 456, 8, 7, 1, 7, 1, 7, 3, 7, 460, 8, 7, 1, 7, 3, 7, 463, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 470, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 481, 8, 7, 10, 7, 12, 7, 484, 9, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 491, 8, 7, 1, 7, 3, 7, 494, 8, 7, 1, 7, 1, 7, 3, 7, 498, 8, 7, 1, 7, 3, 7, 501, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 507, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 518, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 524, 8, 7, 1, 7, 1, 7, 1, 7, 3, 7, 529, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 563, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 576, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 601, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 610, 8, 7, 1, 7, 1, 7, 3, 7, 614, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 620, 8, 7, 1, 7, 1, 7, 3, 7, 624, 8, 7, 1, 7, 1, 7, 1, 7, 3, 7, 629, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 635, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 647, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 655, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 661, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 674, 8, 7, 1, 7, 4, 7, 677, 8, 7, 11, 7, 12, 7, 678, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 695, 8, 7, 1, 7, 1, 7, 1, 7, 5, 7, 700, 8, 7, 10, 7, 12, 7, 703, 9, 7, 1, 7, 3, 7, 706, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 712, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 727, 8, 7, 1, 7, 1, 7, 3, 7, 731, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 737, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 743, 8, 7, 1, 7, 3, 7, 746, 8, 7, 1, 7, 3, 7, 749, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 755, 8, 7, 1, 7, 1, 7, 3, 7, 759, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 767, 8, 7, 10, 7, 12, 7, 770, 9, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 778, 8, 7, 1, 7, 3, 7, 781, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 790, 8, 7, 1, 7, 1, 7, 1, 7, 3, 7, 795, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 801, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 808, 8, 7, 1, 7, 3, 7, 811, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 817, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 826, 8, 7, 10, 7, 12, 7, 829, 9, 7, 3, 7, 831, 8, 7, 1, 7, 1, 7, 3, 7, 835, 8, 7, 1, 7, 1, 7, 1, 7, 3, 7, 840, 8, 7, 1, 7, 1, 7, 1, 7, 3, 7, 845, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 852, 8, 7, 1, 7, 3, 7, 855, 8, 7, 1, 7, 3, 7, 858, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 865, 8, 7, 1, 7, 1, 7, 1, 7, 3, 7, 870, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 879, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 887, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 893, 8, 7, 1, 7, 3, 7, 896, 8, 7, 1, 7, 3, 7, 899, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 905, 8, 7, 1, 7, 1, 7, 3, 7, 909, 8, 7, 1, 7, 1, 7, 1, 7, 3, 7, 914, 8, 7, 1, 7, 3, 7, 917, 8, 7, 1, 7, 1, 7, 3, 7, 921, 8, 7, 3, 7, 923, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 931, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 939, 8, 7, 1, 7, 3, 7, 942, 8, 7, 1, 7, 1, 7, 1, 7, 3, 7, 947, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 953, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 959, 8, 7, 1, 7, 3, 7, 962, 8, 7, 1, 7, 1, 7, 3, 7, 966, 8, 7, 1, 7, 3, 7, 969, 8, 7, 1, 7, 1, 7, 3, 7, 973, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 999, 8, 7, 10, 7, 12, 7, 1002, 9, 7, 3, 7, 1004, 8, 7, 1, 7, 1, 7, 3, 7, 1008, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1014, 8, 7, 1, 7, 3, 7, 1017, 8, 7, 1, 7, 3, 7, 1020, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1026, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1034, 8, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1039, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1045, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1051, 8, 7, 1, 7, 3, 7, 1054, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1061, 8, 7, 1, 7, 1, 7, 1, 7, 5, 7, 1066, 8, 7, 10, 7, 12, 7, 1069, 9, 7, 1, 7, 1, 7, 1, 7, 5, 7, 1074, 8, 7, 10, 7, 12, 7, 1077, 9, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 1091, 8, 7, 10, 7, 12, 7, 1094, 9, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 1105, 8, 7, 10, 7, 12, 7, 1108, 9, 7, 3, 7, 1110, 8, 7, 1, 7, 1, 7, 5, 7, 1114, 8, 7, 10, 7, 12, 7, 1117, 9, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 1123, 8, 7, 10, 7, 12, 7, 1126, 9, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 1132, 8, 7, 10, 7, 12, 7, 1135, 9, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1142, 8, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1147, 8, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1152, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1159, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1165, 8, 7, 1, 7, 1, 7, 1, 7, 3, 7, 1170, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 1176, 8, 7, 10, 7, 12, 7, 1179, 9, 7, 3, 7, 1181, 8, 7, 1, 8, 1, 8, 3, 8, 1185, 8, 8, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 1197, 8, 11, 1, 11, 1, 11, 3, 11, 1201, 8, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 1208, 8, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 1324, 8, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 1332, 8, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 1340, 8, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 1349, 8, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 1359, 8, 11, 1, 12, 1, 12, 3, 12, 1363, 8, 12, 1, 12, 3, 12, 1366, 8, 12, 1, 12, 1, 12, 1, 12, 1, 12, 3, 12, 1372, 8, 12, 1, 12, 1, 12, 1, 13, 1, 13, 3, 13, 1378, 8, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 3, 14, 1390, 8, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 1402, 8, 15, 1, 15, 1, 15, 1, 15, 3, 15, 1407, 8, 15, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 18, 1, 18, 1, 18, 3, 18, 1418, 8, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 1425, 8, 18, 3, 18, 1427, 8, 18, 1, 18, 3, 18, 1430, 8, 18, 1, 18, 1, 18, 1, 18, 3, 18, 1435, 8, 18, 1, 18, 1, 18, 3, 18, 1439, 8, 18, 1, 18, 1, 18, 1, 18, 3, 18, 1444, 8, 18, 1, 18, 3, 18, 1447, 8, 18, 1, 18, 1, 18, 1, 18, 3, 18, 1452, 8, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 1461, 8, 18, 1, 18, 1, 18, 1, 18, 3, 18, 1466, 8, 18, 1, 18, 3, 18, 1469, 8, 18, 1, 18, 1, 18, 1, 18, 3, 18, 1474, 8, 18, 1, 18, 1, 18, 3, 18, 1478, 8, 18, 1, 18, 1, 18, 1, 18, 3, 18, 1483, 8, 18, 3, 18, 1485, 8, 18, 1, 19, 1, 19, 3, 19, 1489, 8, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 5, 20, 1496, 8, 20, 10, 20, 12, 20, 1499, 9, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 3, 21, 1506, 8, 21, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 1512, 8, 21, 1, 22, 1, 22, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 3, 24, 1523, 8, 24, 1, 25, 1, 25, 1, 25, 5, 25, 1528, 8, 25, 10, 25, 12, 25, 1531, 9, 25, 1, 26, 1, 26, 1, 26, 1, 26, 5, 26, 1537, 8, 26, 10, 26, 12, 26, 1540, 9, 26, 1, 27, 3, 27, 1543, 8, 27, 1, 27, 1, 27, 1, 27, 1, 28, 1, 28, 3, 28, 1550, 8, 28, 1, 28, 3, 28, 1553, 8, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 3, 29, 1565, 8, 29, 1, 29, 5, 29, 1568, 8, 29, 10, 29, 12, 29, 1571, 9, 29, 1, 30, 1, 30, 3, 30, 1575, 8, 30, 1, 30, 5, 30, 1578, 8, 30, 10, 30, 12, 30, 1581, 9, 30, 1, 30, 3, 30, 1584, 8, 30, 1, 30, 3, 30, 1587, 8, 30, 1, 30, 3, 30, 1590, 8, 30, 1, 30, 3, 30, 1593, 8, 30, 1, 30, 1, 30, 3, 30, 1597, 8, 30, 1, 30, 5, 30, 1600, 8, 30, 10, 30, 12, 30, 1603, 9, 30, 1, 30, 3, 30, 1606, 8, 30, 1, 30, 3, 30, 1609, 8, 30, 1, 30, 3, 30, 1612, 8, 30, 1, 30, 3, 30, 1615, 8, 30, 3, 30, 1617, 8, 30, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 1628, 8, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 5, 33, 1646, 8, 33, 10, 33, 12, 33, 1649, 9, 33, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 1655, 8, 34, 10, 34, 12, 34, 1658, 9, 34, 1, 34, 1, 34, 1, 35, 1, 35, 3, 35, 1664, 8, 35, 1, 35, 3, 35, 1667, 8, 35, 1, 36, 1, 36, 1, 36, 5, 36, 1672, 8, 36, 10, 36, 12, 36, 1675, 9, 36, 1, 36, 3, 36, 1678, 8, 36, 1, 37, 1, 37, 1, 37, 1, 37, 3, 37, 1684, 8, 37, 1, 38, 1, 38, 1, 38, 1, 38, 5, 38, 1690, 8, 38, 10, 38, 12, 38, 1693, 9, 38, 1, 38, 1, 38, 1, 39, 1, 39, 1, 39, 1, 39, 5, 39, 1701, 8, 39, 10, 39, 12, 39, 1704, 9, 39, 1, 39, 1, 39, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 3, 40, 1714, 8, 40, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 1722, 8, 41, 1, 42, 1, 42, 1, 42, 1, 42, 3, 42, 1728, 8, 42, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 4, 44, 1738, 8, 44, 11, 44, 12, 44, 1739, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 3, 44, 1747, 8, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 3, 44, 1754, 8, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 3, 44, 1766, 8, 44, 1, 44, 1, 44, 1, 44, 1, 44, 5, 44, 1772, 8, 44, 10, 44, 12, 44, 1775, 9, 44, 1, 44, 5, 44, 1778, 8, 44, 10, 44, 12, 44, 1781, 9, 44, 1, 44, 5, 44, 1784, 8, 44, 10, 44, 12, 44, 1787, 9, 44, 3, 44, 1789, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 5, 45, 1796, 8, 45, 10, 45, 12, 45, 1799, 9, 45, 3, 45, 1801, 8, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 5, 45, 1808, 8, 45, 10, 45, 12, 45, 1811, 9, 45, 3, 45, 1813, 8, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 5, 45, 1820, 8, 45, 10, 45, 12, 45, 1823, 9, 45, 3, 45, 1825, 8, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 5, 45, 1832, 8, 45, 10, 45, 12, 45, 1835, 9, 45, 3, 45, 1837, 8, 45, 1, 45, 3, 45, 1840, 8, 45, 1, 45, 1, 45, 1, 45, 3, 45, 1845, 8, 45, 3, 45, 1847, 8, 45, 1, 45, 1, 45, 3, 45, 1851, 8, 45, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 3, 47, 1858, 8, 47, 1, 47, 1, 47, 3, 47, 1862, 8, 47, 1, 48, 1, 48, 4, 48, 1866, 8, 48, 11, 48, 12, 48, 1867, 1, 49, 1, 49, 3, 49, 1872, 8, 49, 1, 49, 1, 49, 1, 49, 1, 49, 5, 49, 1878, 8, 49, 10, 49, 12, 49, 1881, 9, 49, 1, 49, 3, 49, 1884, 8, 49, 1, 49, 3, 49, 1887, 8, 49, 1, 49, 3, 49, 1890, 8, 49, 1, 49, 3, 49, 1893, 8, 49, 1, 49, 1, 49, 3, 49, 1897, 8, 49, 1, 50, 1, 50, 1, 50, 1, 50, 3, 50, 1903, 8, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 3, 50, 1910, 8, 50, 1, 50, 1, 50, 1, 50, 3, 50, 1915, 8, 50, 1, 50, 3, 50, 1918, 8, 50, 1, 50, 3, 50, 1921, 8, 50, 1, 50, 1, 50, 3, 50, 1925, 8, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 3, 50, 1935, 8, 50, 1, 50, 1, 50, 3, 50, 1939, 8, 50, 3, 50, 1941, 8, 50, 1, 50, 3, 50, 1944, 8, 50, 1, 50, 1, 50, 3, 50, 1948, 8, 50, 1, 51, 1, 51, 5, 51, 1952, 8, 51, 10, 51, 12, 51, 1955, 9, 51, 1, 51, 3, 51, 1958, 8, 51, 1, 51, 1, 51, 1, 52, 1, 52, 1, 52, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 1969, 8, 53, 1, 53, 1, 53, 1, 53, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 3, 54, 1979, 8, 54, 1, 54, 1, 54, 3, 54, 1983, 8, 54, 1, 54, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 3, 55, 1995, 8, 55, 1, 55, 1, 55, 1, 55, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 3, 56, 2007, 8, 56, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 5, 57, 2020, 8, 57, 10, 57, 12, 57, 2023, 9, 57, 1, 57, 1, 57, 3, 57, 2027, 8, 57, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 2033, 8, 58, 1, 59, 1, 59, 1, 59, 5, 59, 2038, 8, 59, 10, 59, 12, 59, 2041, 9, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 3, 63, 2056, 8, 63, 1, 63, 5, 63, 2059, 8, 63, 10, 63, 12, 63, 2062, 9, 63, 1, 63, 1, 63, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 5, 64, 2072, 8, 64, 10, 64, 12, 64, 2075, 9, 64, 1, 64, 1, 64, 3, 64, 2079, 8, 64, 1, 65, 1, 65, 1, 65, 1, 65, 5, 65, 2085, 8, 65, 10, 65, 12, 65, 2088, 9, 65, 1, 65, 5, 65, 2091, 8, 65, 10, 65, 12, 65, 2094, 9, 65, 1, 65, 3, 65, 2097, 8, 65, 1, 65, 3, 65, 2100, 8, 65, 1, 66, 3, 66, 2103, 8, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 3, 66, 2110, 8, 66, 1, 66, 1, 66, 1, 66, 1, 66, 3, 66, 2116, 8, 66, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 5, 67, 2123, 8, 67, 10, 67, 12, 67, 2126, 9, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 5, 67, 2133, 8, 67, 10, 67, 12, 67, 2136, 9, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 5, 67, 2148, 8, 67, 10, 67, 12, 67, 2151, 9, 67, 1, 67, 1, 67, 3, 67, 2155, 8, 67, 3, 67, 2157, 8, 67, 1, 68, 1, 68, 3, 68, 2161, 8, 68, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 5, 69, 2168, 8, 69, 10, 69, 12, 69, 2171, 9, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 5, 69, 2181, 8, 69, 10, 69, 12, 69, 2184, 9, 69, 1, 69, 1, 69, 3, 69, 2188, 8, 69, 1, 70, 1, 70, 3, 70, 2192, 8, 70, 1, 71, 1, 71, 1, 71, 1, 71, 5, 71, 2198, 8, 71, 10, 71, 12, 71, 2201, 9, 71, 3, 71, 2203, 8, 71, 1, 71, 1, 71, 3, 71, 2207, 8, 71, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 5, 72, 2219, 8, 72, 10, 72, 12, 72, 2222, 9, 72, 1, 72, 1, 72, 1, 72, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 5, 73, 2232, 8, 73, 10, 73, 12, 73, 2235, 9, 73, 1, 73, 1, 73, 3, 73, 2239, 8, 73, 1, 74, 1, 74, 3, 74, 2243, 8, 74, 1, 74, 3, 74, 2246, 8, 74, 1, 75, 1, 75, 3, 75, 2250, 8, 75, 1, 75, 1, 75, 1, 75, 1, 75, 3, 75, 2256, 8, 75, 1, 75, 3, 75, 2259, 8, 75, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 3, 77, 2266, 8, 77, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 5, 78, 2276, 8, 78, 10, 78, 12, 78, 2279, 9, 78, 1, 78, 1, 78, 1, 79, 1, 79, 1, 79, 1, 79, 5, 79, 2287, 8, 79, 10, 79, 12, 79, 2290, 9, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 5, 79, 2300, 8, 79, 10, 79, 12, 79, 2303, 9, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 5, 80, 2311, 8, 80, 10, 80, 12, 80, 2314, 9, 80, 1, 80, 1, 80, 3, 80, 2318, 8, 80, 1, 81, 1, 81, 1, 82, 1, 82, 1, 83, 1, 83, 3, 83, 2326, 8, 83, 1, 84, 1, 84, 1, 85, 3, 85, 2331, 8, 85, 1, 85, 1, 85, 1, 86, 1, 86, 1, 86, 3, 86, 2338, 8, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 5, 86, 2345, 8, 86, 10, 86, 12, 86, 2348, 9, 86, 3, 86, 2350, 8, 86, 1, 86, 1, 86, 1, 86, 3, 86, 2355, 8, 86, 1, 86, 1, 86, 1, 86, 5, 86, 2360, 8, 86, 10, 86, 12, 86, 2363, 9, 86, 3, 86, 2365, 8, 86, 1, 87, 1, 87, 1, 88, 3, 88, 2370, 8, 88, 1, 88, 1, 88, 5, 88, 2374, 8, 88, 10, 88, 12, 88, 2377, 9, 88, 1, 89, 1, 89, 1, 89, 3, 89, 2382, 8, 89, 1, 90, 1, 90, 1, 90, 3, 90, 2387, 8, 90, 1, 90, 1, 90, 3, 90, 2391, 8, 90, 1, 90, 1, 90, 1, 90, 1, 90, 3, 90, 2397, 8, 90, 1, 90, 1, 90, 3, 90, 2401, 8, 90, 1, 91, 3, 91, 2404, 8, 91, 1, 91, 1, 91, 1, 91, 3, 91, 2409, 8, 91, 1, 91, 3, 91, 2412, 8, 91, 1, 91, 1, 91, 1, 91, 3, 91, 2417, 8, 91, 1, 91, 1, 91, 3, 91, 2421, 8, 91, 1, 91, 3, 91, 2424, 8, 91, 1, 91, 3, 91, 2427, 8, 91, 1, 92, 1, 92, 1, 92, 1, 92, 3, 92, 2433, 8, 92, 1, 93, 1, 93, 1, 93, 3, 93, 2438, 8, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 3, 93, 2445, 8, 93, 1, 94, 3, 94, 2448, 8, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 3, 94, 2466, 8, 94, 3, 94, 2468, 8, 94, 1, 94, 3, 94, 2471, 8, 94, 1, 95, 1, 95, 1, 95, 1, 95, 1, 96, 1, 96, 1, 96, 5, 96, 2480, 8, 96, 10, 96, 12, 96, 2483, 9, 96, 1, 97, 1, 97, 1, 97, 1, 97, 5, 97, 2489, 8, 97, 10, 97, 12, 97, 2492, 9, 97, 1, 97, 1, 97, 1, 98, 1, 98, 3, 98, 2498, 8, 98, 1, 99, 1, 99, 1, 99, 1, 99, 5, 99, 2504, 8, 99, 10, 99, 12, 99, 2507, 9, 99, 1, 99, 1, 99, 1, 100, 1, 100, 3, 100, 2513, 8, 100, 1, 101, 1, 101, 3, 101, 2517, 8, 101, 1, 101, 3, 101, 2520, 8, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 3, 101, 2528, 8, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 3, 101, 2536, 8, 101, 1, 101, 1, 101, 1, 101, 1, 101, 3, 101, 2542, 8, 101, 1, 102, 1, 102, 1, 102, 1, 102, 5, 102, 2548, 8, 102, 10, 102, 12, 102, 2551, 9, 102, 1, 102, 1, 102, 1, 103, 1, 103, 1, 103, 1, 103, 1, 103, 5, 103, 2560, 8, 103, 10, 103, 12, 103, 2563, 9, 103, 3, 103, 2565, 8, 103, 1, 103, 1, 103, 1, 103, 1, 104, 3, 104, 2571, 8, 104, 1, 104, 1, 104, 3, 104, 2575, 8, 104, 3, 104, 2577, 8, 104, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 3, 105, 2586, 8, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 3, 105, 2598, 8, 105, 3, 105, 2600, 8, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 3, 105, 2607, 8, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 3, 105, 2614, 8, 105, 1, 105, 1, 105, 1, 105, 1, 105, 3, 105, 2620, 8, 105, 1, 105, 1, 105, 1, 105, 1, 105, 3, 105, 2626, 8, 105, 3, 105, 2628, 8, 105, 1, 106, 1, 106, 1, 106, 5, 106, 2633, 8, 106, 10, 106, 12, 106, 2636, 9, 106, 1, 107, 1, 107, 1, 107, 5, 107, 2641, 8, 107, 10, 107, 12, 107, 2644, 9, 107, 1, 108, 1, 108, 1, 108, 5, 108, 2649, 8, 108, 10, 108, 12, 108, 2652, 9, 108, 1, 109, 1, 109, 1, 109, 3, 109, 2657, 8, 109, 1, 110, 1, 110, 1, 110, 3, 110, 2662, 8, 110, 1, 110, 1, 110, 1, 111, 1, 111, 1, 111, 3, 111, 2669, 8, 111, 1, 111, 1, 111, 1, 112, 1, 112, 3, 112, 2675, 8, 112, 1, 112, 1, 112, 3, 112, 2679, 8, 112, 3, 112, 2681, 8, 112, 1, 113, 1, 113, 1, 113, 5, 113, 2686, 8, 113, 10, 113, 12, 113, 2689, 9, 113, 1, 114, 1, 114, 1, 114, 1, 114, 5, 114, 2695, 8, 114, 10, 114, 12, 114, 2698, 9, 114, 1, 114, 1, 114, 1, 115, 1, 115, 3, 115, 2704, 8, 115, 1, 116, 1, 116, 1, 116, 1, 116, 1, 116, 1, 116, 5, 116, 2712, 8, 116, 10, 116, 12, 116, 2715, 9, 116, 1, 116, 1, 116, 3, 116, 2719, 8, 116, 1, 117, 1, 117, 3, 117, 2723, 8, 117, 1, 118, 1, 118, 1, 119, 1, 119, 1, 119, 5, 119, 2730, 8, 119, 10, 119, 12, 119, 2733, 9, 119, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 3, 120, 2745, 8, 120, 3, 120, 2747, 8, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 5, 120, 2755, 8, 120, 10, 120, 12, 120, 2758, 9, 120, 1, 121, 3, 121, 2761, 8, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 3, 121, 2769, 8, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 5, 121, 2776, 8, 121, 10, 121, 12, 121, 2779, 9, 121, 1, 121, 1, 121, 1, 121, 3, 121, 2784, 8, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 3, 121, 2792, 8, 121, 1, 121, 1, 121, 1, 121, 3, 121, 2797, 8, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 5, 121, 2807, 8, 121, 10, 121, 12, 121, 2810, 9, 121, 1, 121, 1, 121, 3, 121, 2814, 8, 121, 1, 121, 3, 121, 2817, 8, 121, 1, 121, 1, 121, 1, 121, 1, 121, 3, 121, 2823, 8, 121, 1, 121, 1, 121, 3, 121, 2827, 8, 121, 1, 121, 1, 121, 1, 121, 3, 121, 2832, 8, 121, 1, 121, 1, 121, 1, 121, 3, 121, 2837, 8, 121, 1, 121, 1, 121, 1, 121, 3, 121, 2842, 8, 121, 1, 122, 1, 122, 1, 122, 1, 122, 3, 122, 2848, 8, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 5, 122, 2869, 8, 122, 10, 122, 12, 122, 2872, 9, 122, 1, 123, 1, 123, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 4, 124, 2898, 8, 124, 11, 124, 12, 124, 2899, 1, 124, 1, 124, 3, 124, 2904, 8, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 4, 124, 2911, 8, 124, 11, 124, 12, 124, 2912, 1, 124, 1, 124, 3, 124, 2917, 8, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 5, 124, 2933, 8, 124, 10, 124, 12, 124, 2936, 9, 124, 3, 124, 2938, 8, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 3, 124, 2946, 8, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 3, 124, 2955, 8, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 3, 124, 2964, 8, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 4, 124, 2985, 8, 124, 11, 124, 12, 124, 2986, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 3, 124, 2998, 8, 124, 1, 124, 1, 124, 1, 124, 5, 124, 3003, 8, 124, 10, 124, 12, 124, 3006, 9, 124, 3, 124, 3008, 8, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 3, 124, 3017, 8, 124, 1, 124, 1, 124, 3, 124, 3021, 8, 124, 1, 124, 1, 124, 3, 124, 3025, 8, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 4, 124, 3035, 8, 124, 11, 124, 12, 124, 3036, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 3, 124, 3062, 8, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 3, 124, 3069, 8, 124, 1, 124, 3, 124, 3072, 8, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 3, 124, 3087, 8, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 3, 124, 3108, 8, 124, 1, 124, 1, 124, 3, 124, 3112, 8, 124, 3, 124, 3114, 8, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 5, 124, 3124, 8, 124, 10, 124, 12, 124, 3127, 9, 124, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 4, 125, 3139, 8, 125, 11, 125, 12, 125, 3140, 3, 125, 3143, 8, 125, 1, 126, 1, 126, 1, 127, 1, 127, 1, 128, 1, 128, 1, 129, 1, 129, 1, 130, 1, 130, 1, 130, 3, 130, 3156, 8, 130, 1, 131, 1, 131, 3, 131, 3160, 8, 131, 1, 132, 1, 132, 1, 132, 4, 132, 3165, 8, 132, 11, 132, 12, 132, 3166, 1, 133, 1, 133, 1, 133, 3, 133, 3172, 8, 133, 1, 134, 1, 134, 1, 134, 1, 134, 1, 134, 1, 135, 3, 135, 3180, 8, 135, 1, 135, 1, 135, 1, 135, 3, 135, 3185, 8, 135, 1, 136, 1, 136, 1, 137, 1, 137, 1, 138, 1, 138, 1, 138, 3, 138, 3194, 8, 138, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 3, 139, 3211, 8, 139, 1, 139, 1, 139, 3, 139, 3215, 8, 139, 1, 139, 1, 139, 1, 139, 1, 139, 3, 139, 3221, 8, 139, 1, 139, 1, 139, 1, 139, 1, 139, 3, 139, 3227, 8, 139, 1, 139, 1, 139, 1, 139, 1, 139, 1, 139, 5, 139, 3234, 8, 139, 10, 139, 12, 139, 3237, 9, 139, 1, 139, 3, 139, 3240, 8, 139, 3, 139, 3242, 8, 139, 1, 140, 1, 140, 1, 140, 5, 140, 3247, 8, 140, 10, 140, 12, 140, 3250, 9, 140, 1, 141, 1, 141, 1, 141, 5, 141, 3255, 8, 141, 10, 141, 12, 141, 3258, 9, 141, 1, 142, 1, 142, 1, 142, 1, 142, 1, 142, 3, 142, 3265, 8, 142, 1, 143, 1, 143, 1, 143, 1, 144, 1, 144, 1, 144, 5, 144, 3273, 8, 144, 10, 144, 12, 144, 3276, 9, 144, 1, 145, 1, 145, 1, 145, 1, 145, 3, 145, 3282, 8, 145, 1, 145, 3, 145, 3285, 8, 145, 1, 146, 1, 146, 1, 146, 5, 146, 3290, 8, 146, 10, 146, 12, 146, 3293, 9, 146, 1, 147, 1, 147, 1, 147, 5, 147, 3298, 8, 147, 10, 147, 12, 147, 3301, 9, 147, 1, 148, 1, 148, 1, 148, 1, 148, 1, 148, 3, 148, 3308, 8, 148, 1, 149, 1, 149, 1, 149, 1, 149, 1, 149, 1, 149, 1, 149, 1, 150, 1, 150, 1, 150, 5, 150, 3320, 8, 150, 10, 150, 12, 150, 3323, 9, 150, 1, 151, 1, 151, 3, 151, 3327, 8, 151, 1, 151, 1, 151, 1, 151, 3, 151, 3332, 8, 151, 1, 151, 3, 151, 3335, 8, 151, 1, 152, 1, 152, 1, 152, 1, 152, 1, 152, 1, 153, 1, 153, 1, 153, 1, 153, 5, 153, 3346, 8, 153, 10, 153, 12, 153, 3349, 9, 153, 1, 154, 1, 154, 1, 154, 1, 154, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 5, 155, 3366, 8, 155, 10, 155, 12, 155, 3369, 9, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 5, 155, 3376, 8, 155, 10, 155, 12, 155, 3379, 9, 155, 3, 155, 3381, 8, 155, 1, 155, 1, 155, 1, 155, 1, 155, 1, 155, 5, 155, 3388, 8, 155, 10, 155, 12, 155, 3391, 9, 155, 3, 155, 3393, 8, 155, 3, 155, 3395, 8, 155, 1, 155, 3, 155, 3398, 8, 155, 1, 155, 3, 155, 3401, 8, 155, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 1, 156, 3, 156, 3419, 8, 156, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 1, 157, 3, 157, 3428, 8, 157, 1, 158, 1, 158, 1, 158, 5, 158, 3433, 8, 158, 10, 158, 12, 158, 3436, 9, 158, 1, 159, 1, 159, 1, 159, 1, 159, 3, 159, 3442, 8, 159, 1, 160, 1, 160, 1, 160, 5, 160, 3447, 8, 160, 10, 160, 12, 160, 3450, 9, 160, 1, 161, 1, 161, 1, 161, 1, 162, 1, 162, 4, 162, 3457, 8, 162, 11, 162, 12, 162, 3458, 1, 162, 3, 162, 3462, 8, 162, 1, 163, 1, 163, 3, 163, 3466, 8, 163, 1, 164, 1, 164, 1, 164, 1, 164, 3, 164, 3472, 8, 164, 1, 165, 1, 165, 1, 166, 1, 166, 1, 167, 3, 167, 3479, 8, 167, 1, 167, 1, 167, 3, 167, 3483, 8, 167, 1, 167, 1, 167, 3, 167, 3487, 8, 167, 1, 167, 1, 167, 3, 167, 3491, 8, 167, 1, 167, 1, 167, 3, 167, 3495, 8, 167, 1, 167, 1, 167, 3, 167, 3499, 8, 167, 1, 167, 1, 167, 3, 167, 3503, 8, 167, 1, 167, 1, 167, 3, 167, 3507, 8, 167, 1, 167, 1, 167, 3, 167, 3511, 8, 167, 1, 167, 1, 167, 3, 167, 3515, 8, 167, 1, 167, 3, 167, 3518, 8, 167, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 1, 168, 3, 168, 3531, 8, 168, 1, 169, 1, 169, 1, 170, 1, 170, 3, 170, 3537, 8, 170, 1, 171, 1, 171, 3, 171, 3541, 8, 171, 1, 172, 1, 172, 1, 173, 1, 173, 1, 174, 1, 174, 1, 174, 9, 1000, 1067, 1075, 1092, 1106, 1115, 1124, 1133, 1177, 4, 58, 240, 244, 248, 175, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236, 238, 240, 242, 244, 246, 248, 250, 252, 254, 256, 258, 260, 262, 264, 266, 268, 270, 272, 274, 276, 278, 280, 282, 284, 286, 288, 290, 292, 294, 296, 298, 300, 302, 304, 306, 308, 310, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 332, 334, 336, 338, 340, 342, 344, 346, 348, 0, 59, 2, 0, 69, 69, 202, 202, 2, 0, 30, 30, 219, 219, 2, 0, 107, 107, 122, 122, 1, 0, 43, 44, 2, 0, 258, 258, 296, 296, 2, 0, 11, 11, 35, 35, 5, 0, 40, 40, 52, 52, 93, 93, 106, 106, 152, 152, 1, 0, 74, 75, 2, 0, 93, 93, 106, 106, 3, 0, 8, 8, 82, 82, 255, 255, 2, 0, 8, 8, 146, 146, 3, 0, 65, 65, 166, 166, 231, 231, 3, 0, 66, 66, 167, 167, 232, 232, 4, 0, 87, 87, 130, 130, 240, 240, 284, 284, 2, 0, 21, 21, 74, 74, 2, 0, 101, 101, 137, 137, 2, 0, 257, 257, 295, 295, 2, 0, 256, 256, 267, 267, 2, 0, 55, 55, 226, 226, 2, 0, 89, 89, 123, 123, 2, 0, 10, 10, 79, 79, 2, 0, 335, 335, 337, 337, 1, 0, 142, 143, 3, 0, 10, 10, 16, 16, 244, 244, 3, 0, 96, 96, 277, 277, 286, 286, 2, 0, 316, 317, 321, 321, 2, 0, 81, 81, 318, 320, 2, 0, 316, 317, 324, 324, 11, 0, 61, 61, 63, 63, 117, 117, 157, 157, 159, 159, 161, 161, 163, 163, 204, 204, 229, 229, 298, 298, 305, 305, 3, 0, 57, 57, 59, 60, 292, 292, 2, 0, 67, 67, 268, 268, 2, 0, 68, 68, 269, 269, 2, 0, 32, 32, 279, 279, 2, 0, 120, 120, 218, 218, 1, 0, 253, 254, 2, 0, 4, 4, 107, 107, 2, 0, 4, 4, 103, 103, 3, 0, 25, 25, 140, 140, 272, 272, 1, 0, 193, 194, 1, 0, 308, 315, 2, 0, 81, 81, 316, 325, 4, 0, 14, 14, 122, 122, 172, 172, 181, 181, 2, 0, 96, 96, 277, 277, 1, 0, 316, 317, 7, 0, 61, 62, 117, 118, 157, 164, 168, 169, 229, 230, 298, 299, 305, 306, 6, 0, 61, 61, 117, 117, 161, 161, 163, 163, 229, 229, 305, 305, 2, 0, 163, 163, 305, 305, 4, 0, 61, 61, 117, 117, 161, 161, 229, 229, 3, 0, 117, 117, 161, 161, 229, 229, 2, 0, 80, 80, 190, 190, 2, 0, 182, 182, 245, 245, 2, 0, 102, 102, 199, 199, 2, 0, 331, 331, 342, 342, 1, 0, 336, 337, 2, 0, 82, 82, 239, 239, 1, 0, 330, 331, 51, 0, 8, 9, 11, 13, 15, 15, 17, 19, 21, 22, 24, 24, 26, 30, 33, 35, 37, 40, 42, 42, 44, 50, 52, 52, 55, 56, 61, 78, 80, 82, 86, 86, 88, 95, 98, 98, 100, 102, 105, 106, 109, 112, 115, 115, 117, 121, 123, 125, 127, 129, 131, 131, 134, 134, 136, 137, 139, 139, 142, 169, 171, 171, 174, 175, 179, 180, 183, 183, 185, 186, 188, 192, 195, 199, 201, 210, 212, 220, 222, 232, 234, 237, 239, 243, 245, 257, 259, 264, 267, 269, 271, 271, 273, 283, 287, 291, 294, 299, 302, 302, 305, 307, 16, 0, 15, 15, 54, 54, 87, 87, 108, 108, 126, 126, 130, 130, 135, 135, 138, 138, 141, 141, 170, 170, 177, 177, 221, 221, 234, 234, 240, 240, 284, 284, 293, 293, 17, 0, 8, 14, 16, 53, 55, 86, 88, 107, 109, 125, 127, 129, 131, 134, 136, 137, 139, 140, 142, 169, 171, 176, 178, 220, 222, 233, 235, 239, 241, 283, 285, 292, 294, 307, 4068, 0, 350, 1, 0, 0, 0, 2, 359, 1, 0, 0, 0, 4, 362, 1, 0, 0, 0, 6, 365, 1, 0, 0, 0, 8, 368, 1, 0, 0, 0, 10, 371, 1, 0, 0, 0, 12, 374, 1, 0, 0, 0, 14, 1180, 1, 0, 0, 0, 16, 1184, 1, 0, 0, 0, 18, 1186, 1, 0, 0, 0, 20, 1188, 1, 0, 0, 0, 22, 1358, 1, 0, 0, 0, 24, 1360, 1, 0, 0, 0, 26, 1377, 1, 0, 0, 0, 28, 1383, 1, 0, 0, 0, 30, 1395, 1, 0, 0, 0, 32, 1408, 1, 0, 0, 0, 34, 1411, 1, 0, 0, 0, 36, 1484, 1, 0, 0, 0, 38, 1486, 1, 0, 0, 0, 40, 1490, 1, 0, 0, 0, 42, 1511, 1, 0, 0, 0, 44, 1513, 1, 0, 0, 0, 46, 1515, 1, 0, 0, 0, 48, 1522, 1, 0, 0, 0, 50, 1524, 1, 0, 0, 0, 52, 1532, 1, 0, 0, 0, 54, 1542, 1, 0, 0, 0, 56, 1547, 1, 0, 0, 0, 58, 1558, 1, 0, 0, 0, 60, 1616, 1, 0, 0, 0, 62, 1627, 1, 0, 0, 0, 64, 1629, 1, 0, 0, 0, 66, 1647, 1, 0, 0, 0, 68, 1650, 1, 0, 0, 0, 70, 1661, 1, 0, 0, 0, 72, 1677, 1, 0, 0, 0, 74, 1683, 1, 0, 0, 0, 76, 1685, 1, 0, 0, 0, 78, 1696, 1, 0, 0, 0, 80, 1713, 1, 0, 0, 0, 82, 1721, 1, 0, 0, 0, 84, 1723, 1, 0, 0, 0, 86, 1729, 1, 0, 0, 0, 88, 1788, 1, 0, 0, 0, 90, 1800, 1, 0, 0, 0, 92, 1852, 1, 0, 0, 0, 94, 1855, 1, 0, 0, 0, 96, 1863, 1, 0, 0, 0, 98, 1896, 1, 0, 0, 0, 100, 1917, 1, 0, 0, 0, 102, 1949, 1, 0, 0, 0, 104, 1961, 1, 0, 0, 0, 106, 1964, 1, 0, 0, 0, 108, 1973, 1, 0, 0, 0, 110, 1987, 1, 0, 0, 0, 112, 2006, 1, 0, 0, 0, 114, 2026, 1, 0, 0, 0, 116, 2032, 1, 0, 0, 0, 118, 2034, 1, 0, 0, 0, 120, 2042, 1, 0, 0, 0, 122, 2046, 1, 0, 0, 0, 124, 2049, 1, 0, 0, 0, 126, 2052, 1, 0, 0, 0, 128, 2078, 1, 0, 0, 0, 130, 2080, 1, 0, 0, 0, 132, 2115, 1, 0, 0, 0, 134, 2156, 1, 0, 0, 0, 136, 2160, 1, 0, 0, 0, 138, 2187, 1, 0, 0, 0, 140, 2191, 1, 0, 0, 0, 142, 2206, 1, 0, 0, 0, 144, 2208, 1, 0, 0, 0, 146, 2238, 1, 0, 0, 0, 148, 2240, 1, 0, 0, 0, 150, 2247, 1, 0, 0, 0, 152, 2260, 1, 0, 0, 0, 154, 2265, 1, 0, 0, 0, 156, 2267, 1, 0, 0, 0, 158, 2282, 1, 0, 0, 0, 160, 2306, 1, 0, 0, 0, 162, 2319, 1, 0, 0, 0, 164, 2321, 1, 0, 0, 0, 166, 2323, 1, 0, 0, 0, 168, 2327, 1, 0, 0, 0, 170, 2330, 1, 0, 0, 0, 172, 2334, 1, 0, 0, 0, 174, 2366, 1, 0, 0, 0, 176, 2369, 1, 0, 0, 0, 178, 2381, 1, 0, 0, 0, 180, 2400, 1, 0, 0, 0, 182, 2426, 1, 0, 0, 0, 184, 2432, 1, 0, 0, 0, 186, 2434, 1, 0, 0, 0, 188, 2470, 1, 0, 0, 0, 190, 2472, 1, 0, 0, 0, 192, 2476, 1, 0, 0, 0, 194, 2484, 1, 0, 0, 0, 196, 2495, 1, 0, 0, 0, 198, 2499, 1, 0, 0, 0, 200, 2510, 1, 0, 0, 0, 202, 2541, 1, 0, 0, 0, 204, 2543, 1, 0, 0, 0, 206, 2554, 1, 0, 0, 0, 208, 2576, 1, 0, 0, 0, 210, 2627, 1, 0, 0, 0, 212, 2629, 1, 0, 0, 0, 214, 2637, 1, 0, 0, 0, 216, 2645, 1, 0, 0, 0, 218, 2653, 1, 0, 0, 0, 220, 2661, 1, 0, 0, 0, 222, 2668, 1, 0, 0, 0, 224, 2672, 1, 0, 0, 0, 226, 2682, 1, 0, 0, 0, 228, 2690, 1, 0, 0, 0, 230, 2703, 1, 0, 0, 0, 232, 2718, 1, 0, 0, 0, 234, 2722, 1, 0, 0, 0, 236, 2724, 1, 0, 0, 0, 238, 2726, 1, 0, 0, 0, 240, 2746, 1, 0, 0, 0, 242, 2841, 1, 0, 0, 0, 244, 2847, 1, 0, 0, 0, 246, 2873, 1, 0, 0, 0, 248, 3113, 1, 0, 0, 0, 250, 3142, 1, 0, 0, 0, 252, 3144, 1, 0, 0, 0, 254, 3146, 1, 0, 0, 0, 256, 3148, 1, 0, 0, 0, 258, 3150, 1, 0, 0, 0, 260, 3152, 1, 0, 0, 0, 262, 3157, 1, 0, 0, 0, 264, 3164, 1, 0, 0, 0, 266, 3168, 1, 0, 0, 0, 268, 3173, 1, 0, 0, 0, 270, 3179, 1, 0, 0, 0, 272, 3186, 1, 0, 0, 0, 274, 3188, 1, 0, 0, 0, 276, 3193, 1, 0, 0, 0, 278, 3241, 1, 0, 0, 0, 280, 3243, 1, 0, 0, 0, 282, 3251, 1, 0, 0, 0, 284, 3264, 1, 0, 0, 0, 286, 3266, 1, 0, 0, 0, 288, 3269, 1, 0, 0, 0, 290, 3277, 1, 0, 0, 0, 292, 3286, 1, 0, 0, 0, 294, 3294, 1, 0, 0, 0, 296, 3307, 1, 0, 0, 0, 298, 3309, 1, 0, 0, 0, 300, 3316, 1, 0, 0, 0, 302, 3324, 1, 0, 0, 0, 304, 3336, 1, 0, 0, 0, 306, 3341, 1, 0, 0, 0, 308, 3350, 1, 0, 0, 0, 310, 3400, 1, 0, 0, 0, 312, 3418, 1, 0, 0, 0, 314, 3427, 1, 0, 0, 0, 316, 3429, 1, 0, 0, 0, 318, 3441, 1, 0, 0, 0, 320, 3443, 1, 0, 0, 0, 322, 3451, 1, 0, 0, 0, 324, 3461, 1, 0, 0, 0, 326, 3465, 1, 0, 0, 0, 328, 3471, 1, 0, 0, 0, 330, 3473, 1, 0, 0, 0, 332, 3475, 1, 0, 0, 0, 334, 3517, 1, 0, 0, 0, 336, 3530, 1, 0, 0, 0, 338, 3532, 1, 0, 0, 0, 340, 3536, 1, 0, 0, 0, 342, 3540, 1, 0, 0, 0, 344, 3542, 1, 0, 0, 0, 346, 3544, 1, 0, 0, 0, 348, 3546, 1, 0, 0, 0, 350, 354, 3, 14, 7, 0, 351, 353, 5, 1, 0, 0, 352, 351, 1, 0, 0, 0, 353, 356, 1, 0, 0, 0, 354, 352, 1, 0, 0, 0, 354, 355, 1, 0, 0, 0, 355, 357, 1, 0, 0, 0, 356, 354, 1, 0, 0, 0, 357, 358, 5, 0, 0, 1, 358, 1, 1, 0, 0, 0, 359, 360, 3, 224, 112, 0, 360, 361, 5, 0, 0, 1, 361, 3, 1, 0, 0, 0, 362, 363, 3, 220, 110, 0, 363, 364, 5, 0, 0, 1, 364, 5, 1, 0, 0, 0, 365, 366, 3, 214, 107, 0, 366, 367, 5, 0, 0, 1, 367, 7, 1, 0, 0, 0, 368, 369, 3, 222, 111, 0, 369, 370, 5, 0, 0, 1, 370, 9, 1, 0, 0, 0, 371, 372, 3, 278, 139, 0, 372, 373, 5, 0, 0, 1, 373, 11, 1, 0, 0, 0, 374, 375, 3, 288, 144, 0, 375, 376, 5, 0, 0, 1, 376, 13, 1, 0, 0, 0, 377, 1181, 3, 54, 27, 0, 378, 380, 3, 52, 26, 0, 379, 378, 1, 0, 0, 0, 379, 380, 1, 0, 0, 0, 380, 381, 1, 0, 0, 0, 381, 1181, 3, 88, 44, 0, 382, 383, 5, 291, 0, 0, 383, 1181, 3, 214, 107, 0, 384, 385, 5, 291, 0, 0, 385, 386, 3, 44, 22, 0, 386, 387, 3, 214, 107, 0, 387, 1181, 1, 0, 0, 0, 388, 389, 5, 239, 0, 0, 389, 392, 5, 33, 0, 0, 390, 393, 3, 326, 163, 0, 391, 393, 3, 338, 169, 0, 392, 390, 1, 0, 0, 0, 392, 391, 1, 0, 0, 0, 393, 1181, 1, 0, 0, 0, 394, 395, 5, 53, 0, 0, 395, 399, 3, 44, 22, 0, 396, 397, 5, 119, 0, 0, 397, 398, 5, 172, 0, 0, 398, 400, 5, 90, 0, 0, 399, 396, 1, 0, 0, 0, 399, 400, 1, 0, 0, 0, 400, 401, 1, 0, 0, 0, 401, 409, 3, 214, 107, 0, 402, 408, 3, 34, 17, 0, 403, 408, 3, 32, 16, 0, 404, 405, 5, 303, 0, 0, 405, 406, 7, 0, 0, 0, 406, 408, 3, 68, 34, 0, 407, 402, 1, 0, 0, 0, 407, 403, 1, 0, 0, 0, 407, 404, 1, 0, 0, 0, 408, 411, 1, 0, 0, 0, 409, 407, 1, 0, 0, 0, 409, 410, 1, 0, 0, 0, 410, 1181, 1, 0, 0, 0, 411, 409, 1, 0, 0, 0, 412, 413, 5, 11, 0, 0, 413, 414, 3, 44, 22, 0, 414, 415, 3, 214, 107, 0, 415, 416, 5, 239, 0, 0, 416, 417, 7, 0, 0, 0, 417, 418, 3, 68, 34, 0, 418, 1181, 1, 0, 0, 0, 419, 420, 5, 11, 0, 0, 420, 421, 3, 44, 22, 0, 421, 422, 3, 214, 107, 0, 422, 423, 5, 239, 0, 0, 423, 424, 3, 32, 16, 0, 424, 1181, 1, 0, 0, 0, 425, 426, 5, 82, 0, 0, 426, 429, 3, 44, 22, 0, 427, 428, 5, 119, 0, 0, 428, 430, 5, 90, 0, 0, 429, 427, 1, 0, 0, 0, 429, 430, 1, 0, 0, 0, 430, 431, 1, 0, 0, 0, 431, 433, 3, 214, 107, 0, 432, 434, 7, 1, 0, 0, 433, 432, 1, 0, 0, 0, 433, 434, 1, 0, 0, 0, 434, 1181, 1, 0, 0, 0, 435, 436, 5, 242, 0, 0, 436, 439, 3, 46, 23, 0, 437, 438, 7, 2, 0, 0, 438, 440, 3, 214, 107, 0, 439, 437, 1, 0, 0, 0, 439, 440, 1, 0, 0, 0, 440, 445, 1, 0, 0, 0, 441, 443, 5, 142, 0, 0, 442, 441, 1, 0, 0, 0, 442, 443, 1, 0, 0, 0, 443, 444, 1, 0, 0, 0, 444, 446, 3, 338, 169, 0, 445, 442, 1, 0, 0, 0, 445, 446, 1, 0, 0, 0, 446, 1181, 1, 0, 0, 0, 447, 452, 3, 24, 12, 0, 448, 449, 5, 2, 0, 0, 449, 450, 3, 292, 146, 0, 450, 451, 5, 3, 0, 0, 451, 453, 1, 0, 0, 0, 452, 448, 1, 0, 0, 0, 452, 453, 1, 0, 0, 0, 453, 455, 1, 0, 0, 0, 454, 456, 3, 64, 32, 0, 455, 454, 1, 0, 0, 0, 455, 456, 1, 0, 0, 0, 456, 457, 1, 0, 0, 0, 457, 462, 3, 66, 33, 0, 458, 460, 5, 20, 0, 0, 459, 458, 1, 0, 0, 0, 459, 460, 1, 0, 0, 0, 460, 461, 1, 0, 0, 0, 461, 463, 3, 54, 27, 0, 462, 459, 1, 0, 0, 0, 462, 463, 1, 0, 0, 0, 463, 1181, 1, 0, 0, 0, 464, 465, 5, 53, 0, 0, 465, 469, 5, 258, 0, 0, 466, 467, 5, 119, 0, 0, 467, 468, 5, 172, 0, 0, 468, 470, 5, 90, 0, 0, 469, 466, 1, 0, 0, 0, 469, 470, 1, 0, 0, 0, 470, 471, 1, 0, 0, 0, 471, 472, 3, 220, 110, 0, 472, 473, 5, 142, 0, 0, 473, 482, 3, 220, 110, 0, 474, 481, 3, 64, 32, 0, 475, 481, 3, 210, 105, 0, 476, 481, 3, 80, 40, 0, 477, 481, 3, 32, 16, 0, 478, 479, 5, 262, 0, 0, 479, 481, 3, 68, 34, 0, 480, 474, 1, 0, 0, 0, 480, 475, 1, 0, 0, 0, 480, 476, 1, 0, 0, 0, 480, 477, 1, 0, 0, 0, 480, 478, 1, 0, 0, 0, 481, 484, 1, 0, 0, 0, 482, 480, 1, 0, 0, 0, 482, 483, 1, 0, 0, 0, 483, 1181, 1, 0, 0, 0, 484, 482, 1, 0, 0, 0, 485, 490, 3, 26, 13, 0, 486, 487, 5, 2, 0, 0, 487, 488, 3, 292, 146, 0, 488, 489, 5, 3, 0, 0, 489, 491, 1, 0, 0, 0, 490, 486, 1, 0, 0, 0, 490, 491, 1, 0, 0, 0, 491, 493, 1, 0, 0, 0, 492, 494, 3, 64, 32, 0, 493, 492, 1, 0, 0, 0, 493, 494, 1, 0, 0, 0, 494, 495, 1, 0, 0, 0, 495, 500, 3, 66, 33, 0, 496, 498, 5, 20, 0, 0, 497, 496, 1, 0, 0, 0, 497, 498, 1, 0, 0, 0, 498, 499, 1, 0, 0, 0, 499, 501, 3, 54, 27, 0, 500, 497, 1, 0, 0, 0, 500, 501, 1, 0, 0, 0, 501, 1181, 1, 0, 0, 0, 502, 503, 5, 13, 0, 0, 503, 504, 5, 258, 0, 0, 504, 506, 3, 214, 107, 0, 505, 507, 3, 40, 20, 0, 506, 505, 1, 0, 0, 0, 506, 507, 1, 0, 0, 0, 507, 508, 1, 0, 0, 0, 508, 509, 5, 49, 0, 0, 509, 517, 5, 249, 0, 0, 510, 518, 3, 326, 163, 0, 511, 512, 5, 103, 0, 0, 512, 513, 5, 44, 0, 0, 513, 518, 3, 192, 96, 0, 514, 515, 5, 103, 0, 0, 515, 516, 5, 10, 0, 0, 516, 518, 5, 44, 0, 0, 517, 510, 1, 0, 0, 0, 517, 511, 1, 0, 0, 0, 517, 514, 1, 0, 0, 0, 517, 518, 1, 0, 0, 0, 518, 1181, 1, 0, 0, 0, 519, 520, 5, 13, 0, 0, 520, 523, 5, 259, 0, 0, 521, 522, 7, 2, 0, 0, 522, 524, 3, 214, 107, 0, 523, 521, 1, 0, 0, 0, 523, 524, 1, 0, 0, 0, 524, 525, 1, 0, 0, 0, 525, 526, 5, 49, 0, 0, 526, 528, 5, 249, 0, 0, 527, 529, 3, 326, 163, 0, 528, 527, 1, 0, 0, 0, 528, 529, 1, 0, 0, 0, 529, 1181, 1, 0, 0, 0, 530, 531, 5, 11, 0, 0, 531, 532, 5, 258, 0, 0, 532, 533, 3, 214, 107, 0, 533, 534, 5, 8, 0, 0, 534, 535, 7, 3, 0, 0, 535, 536, 3, 280, 140, 0, 536, 1181, 1, 0, 0, 0, 537, 538, 5, 11, 0, 0, 538, 539, 5, 258, 0, 0, 539, 540, 3, 214, 107, 0, 540, 541, 5, 8, 0, 0, 541, 542, 7, 3, 0, 0, 542, 543, 5, 2, 0, 0, 543, 544, 3, 280, 140, 0, 544, 545, 5, 3, 0, 0, 545, 1181, 1, 0, 0, 0, 546, 547, 5, 11, 0, 0, 547, 548, 5, 258, 0, 0, 548, 549, 3, 214, 107, 0, 549, 550, 5, 213, 0, 0, 550, 551, 5, 43, 0, 0, 551, 552, 3, 214, 107, 0, 552, 553, 5, 270, 0, 0, 553, 554, 3, 322, 161, 0, 554, 1181, 1, 0, 0, 0, 555, 556, 5, 11, 0, 0, 556, 557, 5, 258, 0, 0, 557, 558, 3, 214, 107, 0, 558, 559, 5, 82, 0, 0, 559, 562, 7, 3, 0, 0, 560, 561, 5, 119, 0, 0, 561, 563, 5, 90, 0, 0, 562, 560, 1, 0, 0, 0, 562, 563, 1, 0, 0, 0, 563, 564, 1, 0, 0, 0, 564, 565, 5, 2, 0, 0, 565, 566, 3, 212, 106, 0, 566, 567, 5, 3, 0, 0, 567, 1181, 1, 0, 0, 0, 568, 569, 5, 11, 0, 0, 569, 570, 5, 258, 0, 0, 570, 571, 3, 214, 107, 0, 571, 572, 5, 82, 0, 0, 572, 575, 7, 3, 0, 0, 573, 574, 5, 119, 0, 0, 574, 576, 5, 90, 0, 0, 575, 573, 1, 0, 0, 0, 575, 576, 1, 0, 0, 0, 576, 577, 1, 0, 0, 0, 577, 578, 3, 212, 106, 0, 578, 1181, 1, 0, 0, 0, 579, 580, 5, 11, 0, 0, 580, 581, 7, 4, 0, 0, 581, 582, 3, 214, 107, 0, 582, 583, 5, 213, 0, 0, 583, 584, 5, 270, 0, 0, 584, 585, 3, 214, 107, 0, 585, 1181, 1, 0, 0, 0, 586, 587, 5, 11, 0, 0, 587, 588, 7, 4, 0, 0, 588, 589, 3, 214, 107, 0, 589, 590, 5, 239, 0, 0, 590, 591, 5, 262, 0, 0, 591, 592, 3, 68, 34, 0, 592, 1181, 1, 0, 0, 0, 593, 594, 5, 11, 0, 0, 594, 595, 7, 4, 0, 0, 595, 596, 3, 214, 107, 0, 596, 597, 5, 289, 0, 0, 597, 600, 5, 262, 0, 0, 598, 599, 5, 119, 0, 0, 599, 601, 5, 90, 0, 0, 600, 598, 1, 0, 0, 0, 600, 601, 1, 0, 0, 0, 601, 602, 1, 0, 0, 0, 602, 603, 3, 68, 34, 0, 603, 1181, 1, 0, 0, 0, 604, 605, 5, 11, 0, 0, 605, 606, 5, 258, 0, 0, 606, 607, 3, 214, 107, 0, 607, 609, 7, 5, 0, 0, 608, 610, 5, 43, 0, 0, 609, 608, 1, 0, 0, 0, 609, 610, 1, 0, 0, 0, 610, 611, 1, 0, 0, 0, 611, 613, 3, 214, 107, 0, 612, 614, 3, 336, 168, 0, 613, 612, 1, 0, 0, 0, 613, 614, 1, 0, 0, 0, 614, 1181, 1, 0, 0, 0, 615, 616, 5, 11, 0, 0, 616, 617, 5, 258, 0, 0, 617, 619, 3, 214, 107, 0, 618, 620, 3, 40, 20, 0, 619, 618, 1, 0, 0, 0, 619, 620, 1, 0, 0, 0, 620, 621, 1, 0, 0, 0, 621, 623, 5, 35, 0, 0, 622, 624, 5, 43, 0, 0, 623, 622, 1, 0, 0, 0, 623, 624, 1, 0, 0, 0, 624, 625, 1, 0, 0, 0, 625, 626, 3, 214, 107, 0, 626, 628, 3, 290, 145, 0, 627, 629, 3, 276, 138, 0, 628, 627, 1, 0, 0, 0, 628, 629, 1, 0, 0, 0, 629, 1181, 1, 0, 0, 0, 630, 631, 5, 11, 0, 0, 631, 632, 5, 258, 0, 0, 632, 634, 3, 214, 107, 0, 633, 635, 3, 40, 20, 0, 634, 633, 1, 0, 0, 0, 634, 635, 1, 0, 0, 0, 635, 636, 1, 0, 0, 0, 636, 637, 5, 216, 0, 0, 637, 638, 5, 44, 0, 0, 638, 639, 5, 2, 0, 0, 639, 640, 3, 280, 140, 0, 640, 641, 5, 3, 0, 0, 641, 1181, 1, 0, 0, 0, 642, 643, 5, 11, 0, 0, 643, 644, 5, 258, 0, 0, 644, 646, 3, 214, 107, 0, 645, 647, 3, 40, 20, 0, 646, 645, 1, 0, 0, 0, 646, 647, 1, 0, 0, 0, 647, 648, 1, 0, 0, 0, 648, 649, 5, 239, 0, 0, 649, 650, 5, 236, 0, 0, 650, 654, 3, 338, 169, 0, 651, 652, 5, 303, 0, 0, 652, 653, 5, 237, 0, 0, 653, 655, 3, 68, 34, 0, 654, 651, 1, 0, 0, 0, 654, 655, 1, 0, 0, 0, 655, 1181, 1, 0, 0, 0, 656, 657, 5, 11, 0, 0, 657, 658, 5, 258, 0, 0, 658, 660, 3, 214, 107, 0, 659, 661, 3, 40, 20, 0, 660, 659, 1, 0, 0, 0, 660, 661, 1, 0, 0, 0, 661, 662, 1, 0, 0, 0, 662, 663, 5, 239, 0, 0, 663, 664, 5, 237, 0, 0, 664, 665, 3, 68, 34, 0, 665, 1181, 1, 0, 0, 0, 666, 667, 5, 11, 0, 0, 667, 668, 7, 4, 0, 0, 668, 669, 3, 214, 107, 0, 669, 673, 5, 8, 0, 0, 670, 671, 5, 119, 0, 0, 671, 672, 5, 172, 0, 0, 672, 674, 5, 90, 0, 0, 673, 670, 1, 0, 0, 0, 673, 674, 1, 0, 0, 0, 674, 676, 1, 0, 0, 0, 675, 677, 3, 38, 19, 0, 676, 675, 1, 0, 0, 0, 677, 678, 1, 0, 0, 0, 678, 676, 1, 0, 0, 0, 678, 679, 1, 0, 0, 0, 679, 1181, 1, 0, 0, 0, 680, 681, 5, 11, 0, 0, 681, 682, 5, 258, 0, 0, 682, 683, 3, 214, 107, 0, 683, 684, 3, 40, 20, 0, 684, 685, 5, 213, 0, 0, 685, 686, 5, 270, 0, 0, 686, 687, 3, 40, 20, 0, 687, 1181, 1, 0, 0, 0, 688, 689, 5, 11, 0, 0, 689, 690, 7, 4, 0, 0, 690, 691, 3, 214, 107, 0, 691, 694, 5, 82, 0, 0, 692, 693, 5, 119, 0, 0, 693, 695, 5, 90, 0, 0, 694, 692, 1, 0, 0, 0, 694, 695, 1, 0, 0, 0, 695, 696, 1, 0, 0, 0, 696, 701, 3, 40, 20, 0, 697, 698, 5, 4, 0, 0, 698, 700, 3, 40, 20, 0, 699, 697, 1, 0, 0, 0, 700, 703, 1, 0, 0, 0, 701, 699, 1, 0, 0, 0, 701, 702, 1, 0, 0, 0, 702, 705, 1, 0, 0, 0, 703, 701, 1, 0, 0, 0, 704, 706, 5, 203, 0, 0, 705, 704, 1, 0, 0, 0, 705, 706, 1, 0, 0, 0, 706, 1181, 1, 0, 0, 0, 707, 708, 5, 11, 0, 0, 708, 709, 5, 258, 0, 0, 709, 711, 3, 214, 107, 0, 710, 712, 3, 40, 20, 0, 711, 710, 1, 0, 0, 0, 711, 712, 1, 0, 0, 0, 712, 713, 1, 0, 0, 0, 713, 714, 5, 239, 0, 0, 714, 715, 3, 32, 16, 0, 715, 1181, 1, 0, 0, 0, 716, 717, 5, 11, 0, 0, 717, 718, 5, 258, 0, 0, 718, 719, 3, 214, 107, 0, 719, 720, 5, 209, 0, 0, 720, 721, 5, 192, 0, 0, 721, 1181, 1, 0, 0, 0, 722, 723, 5, 82, 0, 0, 723, 726, 5, 258, 0, 0, 724, 725, 5, 119, 0, 0, 725, 727, 5, 90, 0, 0, 726, 724, 1, 0, 0, 0, 726, 727, 1, 0, 0, 0, 727, 728, 1, 0, 0, 0, 728, 730, 3, 214, 107, 0, 729, 731, 5, 203, 0, 0, 730, 729, 1, 0, 0, 0, 730, 731, 1, 0, 0, 0, 731, 1181, 1, 0, 0, 0, 732, 733, 5, 82, 0, 0, 733, 736, 5, 296, 0, 0, 734, 735, 5, 119, 0, 0, 735, 737, 5, 90, 0, 0, 736, 734, 1, 0, 0, 0, 736, 737, 1, 0, 0, 0, 737, 738, 1, 0, 0, 0, 738, 1181, 3, 214, 107, 0, 739, 742, 5, 53, 0, 0, 740, 741, 5, 181, 0, 0, 741, 743, 5, 216, 0, 0, 742, 740, 1, 0, 0, 0, 742, 743, 1, 0, 0, 0, 743, 748, 1, 0, 0, 0, 744, 746, 5, 112, 0, 0, 745, 744, 1, 0, 0, 0, 745, 746, 1, 0, 0, 0, 746, 747, 1, 0, 0, 0, 747, 749, 5, 263, 0, 0, 748, 745, 1, 0, 0, 0, 748, 749, 1, 0, 0, 0, 749, 750, 1, 0, 0, 0, 750, 754, 5, 296, 0, 0, 751, 752, 5, 119, 0, 0, 752, 753, 5, 172, 0, 0, 753, 755, 5, 90, 0, 0, 754, 751, 1, 0, 0, 0, 754, 755, 1, 0, 0, 0, 755, 756, 1, 0, 0, 0, 756, 758, 3, 214, 107, 0, 757, 759, 3, 198, 99, 0, 758, 757, 1, 0, 0, 0, 758, 759, 1, 0, 0, 0, 759, 768, 1, 0, 0, 0, 760, 767, 3, 34, 17, 0, 761, 762, 5, 191, 0, 0, 762, 763, 5, 177, 0, 0, 763, 767, 3, 190, 95, 0, 764, 765, 5, 262, 0, 0, 765, 767, 3, 68, 34, 0, 766, 760, 1, 0, 0, 0, 766, 761, 1, 0, 0, 0, 766, 764, 1, 0, 0, 0, 767, 770, 1, 0, 0, 0, 768, 766, 1, 0, 0, 0, 768, 769, 1, 0, 0, 0, 769, 771, 1, 0, 0, 0, 770, 768, 1, 0, 0, 0, 771, 772, 5, 20, 0, 0, 772, 773, 3, 54, 27, 0, 773, 1181, 1, 0, 0, 0, 774, 777, 5, 53, 0, 0, 775, 776, 5, 181, 0, 0, 776, 778, 5, 216, 0, 0, 777, 775, 1, 0, 0, 0, 777, 778, 1, 0, 0, 0, 778, 780, 1, 0, 0, 0, 779, 781, 5, 112, 0, 0, 780, 779, 1, 0, 0, 0, 780, 781, 1, 0, 0, 0, 781, 782, 1, 0, 0, 0, 782, 783, 5, 263, 0, 0, 783, 784, 5, 296, 0, 0, 784, 789, 3, 220, 110, 0, 785, 786, 5, 2, 0, 0, 786, 787, 3, 288, 144, 0, 787, 788, 5, 3, 0, 0, 788, 790, 1, 0, 0, 0, 789, 785, 1, 0, 0, 0, 789, 790, 1, 0, 0, 0, 790, 791, 1, 0, 0, 0, 791, 794, 3, 64, 32, 0, 792, 793, 5, 180, 0, 0, 793, 795, 3, 68, 34, 0, 794, 792, 1, 0, 0, 0, 794, 795, 1, 0, 0, 0, 795, 1181, 1, 0, 0, 0, 796, 797, 5, 11, 0, 0, 797, 798, 5, 296, 0, 0, 798, 800, 3, 214, 107, 0, 799, 801, 5, 20, 0, 0, 800, 799, 1, 0, 0, 0, 800, 801, 1, 0, 0, 0, 801, 802, 1, 0, 0, 0, 802, 803, 3, 54, 27, 0, 803, 1181, 1, 0, 0, 0, 804, 807, 5, 53, 0, 0, 805, 806, 5, 181, 0, 0, 806, 808, 5, 216, 0, 0, 807, 805, 1, 0, 0, 0, 807, 808, 1, 0, 0, 0, 808, 810, 1, 0, 0, 0, 809, 811, 5, 263, 0, 0, 810, 809, 1, 0, 0, 0, 810, 811, 1, 0, 0, 0, 811, 812, 1, 0, 0, 0, 812, 816, 5, 109, 0, 0, 813, 814, 5, 119, 0, 0, 814, 815, 5, 172, 0, 0, 815, 817, 5, 90, 0, 0, 816, 813, 1, 0, 0, 0, 816, 817, 1, 0, 0, 0, 817, 818, 1, 0, 0, 0, 818, 819, 3, 214, 107, 0, 819, 820, 5, 20, 0, 0, 820, 830, 3, 338, 169, 0, 821, 822, 5, 293, 0, 0, 822, 827, 3, 86, 43, 0, 823, 824, 5, 4, 0, 0, 824, 826, 3, 86, 43, 0, 825, 823, 1, 0, 0, 0, 826, 829, 1, 0, 0, 0, 827, 825, 1, 0, 0, 0, 827, 828, 1, 0, 0, 0, 828, 831, 1, 0, 0, 0, 829, 827, 1, 0, 0, 0, 830, 821, 1, 0, 0, 0, 830, 831, 1, 0, 0, 0, 831, 1181, 1, 0, 0, 0, 832, 834, 5, 82, 0, 0, 833, 835, 5, 263, 0, 0, 834, 833, 1, 0, 0, 0, 834, 835, 1, 0, 0, 0, 835, 836, 1, 0, 0, 0, 836, 839, 5, 109, 0, 0, 837, 838, 5, 119, 0, 0, 838, 840, 5, 90, 0, 0, 839, 837, 1, 0, 0, 0, 839, 840, 1, 0, 0, 0, 840, 841, 1, 0, 0, 0, 841, 1181, 3, 214, 107, 0, 842, 844, 5, 91, 0, 0, 843, 845, 7, 6, 0, 0, 844, 843, 1, 0, 0, 0, 844, 845, 1, 0, 0, 0, 845, 846, 1, 0, 0, 0, 846, 1181, 3, 14, 7, 0, 847, 848, 5, 242, 0, 0, 848, 851, 5, 259, 0, 0, 849, 850, 7, 2, 0, 0, 850, 852, 3, 214, 107, 0, 851, 849, 1, 0, 0, 0, 851, 852, 1, 0, 0, 0, 852, 857, 1, 0, 0, 0, 853, 855, 5, 142, 0, 0, 854, 853, 1, 0, 0, 0, 854, 855, 1, 0, 0, 0, 855, 856, 1, 0, 0, 0, 856, 858, 3, 338, 169, 0, 857, 854, 1, 0, 0, 0, 857, 858, 1, 0, 0, 0, 858, 1181, 1, 0, 0, 0, 859, 860, 5, 242, 0, 0, 860, 861, 5, 258, 0, 0, 861, 864, 5, 93, 0, 0, 862, 863, 7, 2, 0, 0, 863, 865, 3, 214, 107, 0, 864, 862, 1, 0, 0, 0, 864, 865, 1, 0, 0, 0, 865, 866, 1, 0, 0, 0, 866, 867, 5, 142, 0, 0, 867, 869, 3, 338, 169, 0, 868, 870, 3, 40, 20, 0, 869, 868, 1, 0, 0, 0, 869, 870, 1, 0, 0, 0, 870, 1181, 1, 0, 0, 0, 871, 872, 5, 242, 0, 0, 872, 873, 5, 262, 0, 0, 873, 878, 3, 214, 107, 0, 874, 875, 5, 2, 0, 0, 875, 876, 3, 72, 36, 0, 876, 877, 5, 3, 0, 0, 877, 879, 1, 0, 0, 0, 878, 874, 1, 0, 0, 0, 878, 879, 1, 0, 0, 0, 879, 1181, 1, 0, 0, 0, 880, 881, 5, 242, 0, 0, 881, 882, 5, 44, 0, 0, 882, 883, 7, 2, 0, 0, 883, 886, 3, 214, 107, 0, 884, 885, 7, 2, 0, 0, 885, 887, 3, 214, 107, 0, 886, 884, 1, 0, 0, 0, 886, 887, 1, 0, 0, 0, 887, 1181, 1, 0, 0, 0, 888, 889, 5, 242, 0, 0, 889, 892, 5, 297, 0, 0, 890, 891, 7, 2, 0, 0, 891, 893, 3, 214, 107, 0, 892, 890, 1, 0, 0, 0, 892, 893, 1, 0, 0, 0, 893, 898, 1, 0, 0, 0, 894, 896, 5, 142, 0, 0, 895, 894, 1, 0, 0, 0, 895, 896, 1, 0, 0, 0, 896, 897, 1, 0, 0, 0, 897, 899, 3, 338, 169, 0, 898, 895, 1, 0, 0, 0, 898, 899, 1, 0, 0, 0, 899, 1181, 1, 0, 0, 0, 900, 901, 5, 242, 0, 0, 901, 902, 5, 192, 0, 0, 902, 904, 3, 214, 107, 0, 903, 905, 3, 40, 20, 0, 904, 903, 1, 0, 0, 0, 904, 905, 1, 0, 0, 0, 905, 1181, 1, 0, 0, 0, 906, 908, 5, 242, 0, 0, 907, 909, 3, 326, 163, 0, 908, 907, 1, 0, 0, 0, 908, 909, 1, 0, 0, 0, 909, 910, 1, 0, 0, 0, 910, 913, 5, 110, 0, 0, 911, 912, 7, 2, 0, 0, 912, 914, 3, 214, 107, 0, 913, 911, 1, 0, 0, 0, 913, 914, 1, 0, 0, 0, 914, 922, 1, 0, 0, 0, 915, 917, 5, 142, 0, 0, 916, 915, 1, 0, 0, 0, 916, 917, 1, 0, 0, 0, 917, 920, 1, 0, 0, 0, 918, 921, 3, 214, 107, 0, 919, 921, 3, 338, 169, 0, 920, 918, 1, 0, 0, 0, 920, 919, 1, 0, 0, 0, 921, 923, 1, 0, 0, 0, 922, 916, 1, 0, 0, 0, 922, 923, 1, 0, 0, 0, 923, 1181, 1, 0, 0, 0, 924, 925, 5, 242, 0, 0, 925, 926, 5, 53, 0, 0, 926, 927, 5, 258, 0, 0, 927, 930, 3, 214, 107, 0, 928, 929, 5, 20, 0, 0, 929, 931, 5, 236, 0, 0, 930, 928, 1, 0, 0, 0, 930, 931, 1, 0, 0, 0, 931, 1181, 1, 0, 0, 0, 932, 933, 5, 242, 0, 0, 933, 934, 5, 56, 0, 0, 934, 1181, 3, 44, 22, 0, 935, 936, 5, 242, 0, 0, 936, 941, 5, 34, 0, 0, 937, 939, 5, 142, 0, 0, 938, 937, 1, 0, 0, 0, 938, 939, 1, 0, 0, 0, 939, 940, 1, 0, 0, 0, 940, 942, 3, 338, 169, 0, 941, 938, 1, 0, 0, 0, 941, 942, 1, 0, 0, 0, 942, 1181, 1, 0, 0, 0, 943, 944, 7, 7, 0, 0, 944, 946, 5, 109, 0, 0, 945, 947, 5, 93, 0, 0, 946, 945, 1, 0, 0, 0, 946, 947, 1, 0, 0, 0, 947, 948, 1, 0, 0, 0, 948, 1181, 3, 48, 24, 0, 949, 950, 7, 7, 0, 0, 950, 952, 3, 44, 22, 0, 951, 953, 5, 93, 0, 0, 952, 951, 1, 0, 0, 0, 952, 953, 1, 0, 0, 0, 953, 954, 1, 0, 0, 0, 954, 955, 3, 214, 107, 0, 955, 1181, 1, 0, 0, 0, 956, 958, 7, 7, 0, 0, 957, 959, 5, 258, 0, 0, 958, 957, 1, 0, 0, 0, 958, 959, 1, 0, 0, 0, 959, 961, 1, 0, 0, 0, 960, 962, 7, 8, 0, 0, 961, 960, 1, 0, 0, 0, 961, 962, 1, 0, 0, 0, 962, 963, 1, 0, 0, 0, 963, 965, 3, 214, 107, 0, 964, 966, 3, 40, 20, 0, 965, 964, 1, 0, 0, 0, 965, 966, 1, 0, 0, 0, 966, 968, 1, 0, 0, 0, 967, 969, 3, 50, 25, 0, 968, 967, 1, 0, 0, 0, 968, 969, 1, 0, 0, 0, 969, 1181, 1, 0, 0, 0, 970, 972, 7, 7, 0, 0, 971, 973, 5, 205, 0, 0, 972, 971, 1, 0, 0, 0, 972, 973, 1, 0, 0, 0, 973, 974, 1, 0, 0, 0, 974, 1181, 3, 54, 27, 0, 975, 976, 5, 45, 0, 0, 976, 977, 5, 177, 0, 0, 977, 978, 3, 44, 22, 0, 978, 979, 3, 214, 107, 0, 979, 980, 5, 133, 0, 0, 980, 981, 3, 340, 170, 0, 981, 1181, 1, 0, 0, 0, 982, 983, 5, 45, 0, 0, 983, 984, 5, 177, 0, 0, 984, 985, 5, 258, 0, 0, 985, 986, 3, 214, 107, 0, 986, 987, 5, 133, 0, 0, 987, 988, 3, 340, 170, 0, 988, 1181, 1, 0, 0, 0, 989, 990, 5, 212, 0, 0, 990, 991, 5, 258, 0, 0, 991, 1181, 3, 214, 107, 0, 992, 993, 5, 212, 0, 0, 993, 994, 5, 109, 0, 0, 994, 1181, 3, 214, 107, 0, 995, 1003, 5, 212, 0, 0, 996, 1004, 3, 338, 169, 0, 997, 999, 9, 0, 0, 0, 998, 997, 1, 0, 0, 0, 999, 1002, 1, 0, 0, 0, 1000, 1001, 1, 0, 0, 0, 1000, 998, 1, 0, 0, 0, 1001, 1004, 1, 0, 0, 0, 1002, 1000, 1, 0, 0, 0, 1003, 996, 1, 0, 0, 0, 1003, 1000, 1, 0, 0, 0, 1004, 1181, 1, 0, 0, 0, 1005, 1007, 5, 29, 0, 0, 1006, 1008, 5, 139, 0, 0, 1007, 1006, 1, 0, 0, 0, 1007, 1008, 1, 0, 0, 0, 1008, 1009, 1, 0, 0, 0, 1009, 1010, 5, 258, 0, 0, 1010, 1013, 3, 214, 107, 0, 1011, 1012, 5, 180, 0, 0, 1012, 1014, 3, 68, 34, 0, 1013, 1011, 1, 0, 0, 0, 1013, 1014, 1, 0, 0, 0, 1014, 1019, 1, 0, 0, 0, 1015, 1017, 5, 20, 0, 0, 1016, 1015, 1, 0, 0, 0, 1016, 1017, 1, 0, 0, 0, 1017, 1018, 1, 0, 0, 0, 1018, 1020, 3, 54, 27, 0, 1019, 1016, 1, 0, 0, 0, 1019, 1020, 1, 0, 0, 0, 1020, 1181, 1, 0, 0, 0, 1021, 1022, 5, 283, 0, 0, 1022, 1025, 5, 258, 0, 0, 1023, 1024, 5, 119, 0, 0, 1024, 1026, 5, 90, 0, 0, 1025, 1023, 1, 0, 0, 0, 1025, 1026, 1, 0, 0, 0, 1026, 1027, 1, 0, 0, 0, 1027, 1181, 3, 214, 107, 0, 1028, 1029, 5, 37, 0, 0, 1029, 1181, 5, 29, 0, 0, 1030, 1031, 5, 147, 0, 0, 1031, 1033, 5, 64, 0, 0, 1032, 1034, 5, 148, 0, 0, 1033, 1032, 1, 0, 0, 0, 1033, 1034, 1, 0, 0, 0, 1034, 1035, 1, 0, 0, 0, 1035, 1036, 5, 127, 0, 0, 1036, 1038, 3, 338, 169, 0, 1037, 1039, 5, 189, 0, 0, 1038, 1037, 1, 0, 0, 0, 1038, 1039, 1, 0, 0, 0, 1039, 1040, 1, 0, 0, 0, 1040, 1041, 5, 132, 0, 0, 1041, 1042, 5, 258, 0, 0, 1042, 1044, 3, 214, 107, 0, 1043, 1045, 3, 40, 20, 0, 1044, 1043, 1, 0, 0, 0, 1044, 1045, 1, 0, 0, 0, 1045, 1181, 1, 0, 0, 0, 1046, 1047, 5, 278, 0, 0, 1047, 1048, 5, 258, 0, 0, 1048, 1050, 3, 214, 107, 0, 1049, 1051, 3, 40, 20, 0, 1050, 1049, 1, 0, 0, 0, 1050, 1051, 1, 0, 0, 0, 1051, 1181, 1, 0, 0, 0, 1052, 1054, 5, 165, 0, 0, 1053, 1052, 1, 0, 0, 0, 1053, 1054, 1, 0, 0, 0, 1054, 1055, 1, 0, 0, 0, 1055, 1056, 5, 214, 0, 0, 1056, 1057, 5, 258, 0, 0, 1057, 1060, 3, 214, 107, 0, 1058, 1059, 7, 9, 0, 0, 1059, 1061, 5, 192, 0, 0, 1060, 1058, 1, 0, 0, 0, 1060, 1061, 1, 0, 0, 0, 1061, 1181, 1, 0, 0, 0, 1062, 1063, 7, 10, 0, 0, 1063, 1067, 3, 326, 163, 0, 1064, 1066, 9, 0, 0, 0, 1065, 1064, 1, 0, 0, 0, 1066, 1069, 1, 0, 0, 0, 1067, 1068, 1, 0, 0, 0, 1067, 1065, 1, 0, 0, 0, 1068, 1181, 1, 0, 0, 0, 1069, 1067, 1, 0, 0, 0, 1070, 1071, 5, 239, 0, 0, 1071, 1075, 5, 223, 0, 0, 1072, 1074, 9, 0, 0, 0, 1073, 1072, 1, 0, 0, 0, 1074, 1077, 1, 0, 0, 0, 1075, 1076, 1, 0, 0, 0, 1075, 1073, 1, 0, 0, 0, 1076, 1181, 1, 0, 0, 0, 1077, 1075, 1, 0, 0, 0, 1078, 1079, 5, 239, 0, 0, 1079, 1080, 5, 266, 0, 0, 1080, 1081, 5, 307, 0, 0, 1081, 1181, 3, 260, 130, 0, 1082, 1083, 5, 239, 0, 0, 1083, 1084, 5, 266, 0, 0, 1084, 1085, 5, 307, 0, 0, 1085, 1181, 3, 16, 8, 0, 1086, 1087, 5, 239, 0, 0, 1087, 1088, 5, 266, 0, 0, 1088, 1092, 5, 307, 0, 0, 1089, 1091, 9, 0, 0, 0, 1090, 1089, 1, 0, 0, 0, 1091, 1094, 1, 0, 0, 0, 1092, 1093, 1, 0, 0, 0, 1092, 1090, 1, 0, 0, 0, 1093, 1181, 1, 0, 0, 0, 1094, 1092, 1, 0, 0, 0, 1095, 1096, 5, 239, 0, 0, 1096, 1097, 3, 18, 9, 0, 1097, 1098, 5, 308, 0, 0, 1098, 1099, 3, 20, 10, 0, 1099, 1181, 1, 0, 0, 0, 1100, 1101, 5, 239, 0, 0, 1101, 1109, 3, 18, 9, 0, 1102, 1106, 5, 308, 0, 0, 1103, 1105, 9, 0, 0, 0, 1104, 1103, 1, 0, 0, 0, 1105, 1108, 1, 0, 0, 0, 1106, 1107, 1, 0, 0, 0, 1106, 1104, 1, 0, 0, 0, 1107, 1110, 1, 0, 0, 0, 1108, 1106, 1, 0, 0, 0, 1109, 1102, 1, 0, 0, 0, 1109, 1110, 1, 0, 0, 0, 1110, 1181, 1, 0, 0, 0, 1111, 1115, 5, 239, 0, 0, 1112, 1114, 9, 0, 0, 0, 1113, 1112, 1, 0, 0, 0, 1114, 1117, 1, 0, 0, 0, 1115, 1116, 1, 0, 0, 0, 1115, 1113, 1, 0, 0, 0, 1116, 1118, 1, 0, 0, 0, 1117, 1115, 1, 0, 0, 0, 1118, 1119, 5, 308, 0, 0, 1119, 1181, 3, 20, 10, 0, 1120, 1124, 5, 239, 0, 0, 1121, 1123, 9, 0, 0, 0, 1122, 1121, 1, 0, 0, 0, 1123, 1126, 1, 0, 0, 0, 1124, 1125, 1, 0, 0, 0, 1124, 1122, 1, 0, 0, 0, 1125, 1181, 1, 0, 0, 0, 1126, 1124, 1, 0, 0, 0, 1127, 1128, 5, 217, 0, 0, 1128, 1181, 3, 18, 9, 0, 1129, 1133, 5, 217, 0, 0, 1130, 1132, 9, 0, 0, 0, 1131, 1130, 1, 0, 0, 0, 1132, 1135, 1, 0, 0, 0, 1133, 1134, 1, 0, 0, 0, 1133, 1131, 1, 0, 0, 0, 1134, 1181, 1, 0, 0, 0, 1135, 1133, 1, 0, 0, 0, 1136, 1137, 5, 53, 0, 0, 1137, 1141, 5, 124, 0, 0, 1138, 1139, 5, 119, 0, 0, 1139, 1140, 5, 172, 0, 0, 1140, 1142, 5, 90, 0, 0, 1141, 1138, 1, 0, 0, 0, 1141, 1142, 1, 0, 0, 0, 1142, 1143, 1, 0, 0, 0, 1143, 1144, 3, 326, 163, 0, 1144, 1146, 5, 177, 0, 0, 1145, 1147, 5, 258, 0, 0, 1146, 1145, 1, 0, 0, 0, 1146, 1147, 1, 0, 0, 0, 1147, 1148, 1, 0, 0, 0, 1148, 1151, 3, 214, 107, 0, 1149, 1150, 5, 293, 0, 0, 1150, 1152, 3, 326, 163, 0, 1151, 1149, 1, 0, 0, 0, 1151, 1152, 1, 0, 0, 0, 1152, 1153, 1, 0, 0, 0, 1153, 1154, 5, 2, 0, 0, 1154, 1155, 3, 216, 108, 0, 1155, 1158, 5, 3, 0, 0, 1156, 1157, 5, 180, 0, 0, 1157, 1159, 3, 68, 34, 0, 1158, 1156, 1, 0, 0, 0, 1158, 1159, 1, 0, 0, 0, 1159, 1181, 1, 0, 0, 0, 1160, 1161, 5, 82, 0, 0, 1161, 1164, 5, 124, 0, 0, 1162, 1163, 5, 119, 0, 0, 1163, 1165, 5, 90, 0, 0, 1164, 1162, 1, 0, 0, 0, 1164, 1165, 1, 0, 0, 0, 1165, 1166, 1, 0, 0, 0, 1166, 1167, 3, 326, 163, 0, 1167, 1169, 5, 177, 0, 0, 1168, 1170, 5, 258, 0, 0, 1169, 1168, 1, 0, 0, 0, 1169, 1170, 1, 0, 0, 0, 1170, 1171, 1, 0, 0, 0, 1171, 1172, 3, 214, 107, 0, 1172, 1181, 1, 0, 0, 0, 1173, 1177, 3, 22, 11, 0, 1174, 1176, 9, 0, 0, 0, 1175, 1174, 1, 0, 0, 0, 1176, 1179, 1, 0, 0, 0, 1177, 1178, 1, 0, 0, 0, 1177, 1175, 1, 0, 0, 0, 1178, 1181, 1, 0, 0, 0, 1179, 1177, 1, 0, 0, 0, 1180, 377, 1, 0, 0, 0, 1180, 379, 1, 0, 0, 0, 1180, 382, 1, 0, 0, 0, 1180, 384, 1, 0, 0, 0, 1180, 388, 1, 0, 0, 0, 1180, 394, 1, 0, 0, 0, 1180, 412, 1, 0, 0, 0, 1180, 419, 1, 0, 0, 0, 1180, 425, 1, 0, 0, 0, 1180, 435, 1, 0, 0, 0, 1180, 447, 1, 0, 0, 0, 1180, 464, 1, 0, 0, 0, 1180, 485, 1, 0, 0, 0, 1180, 502, 1, 0, 0, 0, 1180, 519, 1, 0, 0, 0, 1180, 530, 1, 0, 0, 0, 1180, 537, 1, 0, 0, 0, 1180, 546, 1, 0, 0, 0, 1180, 555, 1, 0, 0, 0, 1180, 568, 1, 0, 0, 0, 1180, 579, 1, 0, 0, 0, 1180, 586, 1, 0, 0, 0, 1180, 593, 1, 0, 0, 0, 1180, 604, 1, 0, 0, 0, 1180, 615, 1, 0, 0, 0, 1180, 630, 1, 0, 0, 0, 1180, 642, 1, 0, 0, 0, 1180, 656, 1, 0, 0, 0, 1180, 666, 1, 0, 0, 0, 1180, 680, 1, 0, 0, 0, 1180, 688, 1, 0, 0, 0, 1180, 707, 1, 0, 0, 0, 1180, 716, 1, 0, 0, 0, 1180, 722, 1, 0, 0, 0, 1180, 732, 1, 0, 0, 0, 1180, 739, 1, 0, 0, 0, 1180, 774, 1, 0, 0, 0, 1180, 796, 1, 0, 0, 0, 1180, 804, 1, 0, 0, 0, 1180, 832, 1, 0, 0, 0, 1180, 842, 1, 0, 0, 0, 1180, 847, 1, 0, 0, 0, 1180, 859, 1, 0, 0, 0, 1180, 871, 1, 0, 0, 0, 1180, 880, 1, 0, 0, 0, 1180, 888, 1, 0, 0, 0, 1180, 900, 1, 0, 0, 0, 1180, 906, 1, 0, 0, 0, 1180, 924, 1, 0, 0, 0, 1180, 932, 1, 0, 0, 0, 1180, 935, 1, 0, 0, 0, 1180, 943, 1, 0, 0, 0, 1180, 949, 1, 0, 0, 0, 1180, 956, 1, 0, 0, 0, 1180, 970, 1, 0, 0, 0, 1180, 975, 1, 0, 0, 0, 1180, 982, 1, 0, 0, 0, 1180, 989, 1, 0, 0, 0, 1180, 992, 1, 0, 0, 0, 1180, 995, 1, 0, 0, 0, 1180, 1005, 1, 0, 0, 0, 1180, 1021, 1, 0, 0, 0, 1180, 1028, 1, 0, 0, 0, 1180, 1030, 1, 0, 0, 0, 1180, 1046, 1, 0, 0, 0, 1180, 1053, 1, 0, 0, 0, 1180, 1062, 1, 0, 0, 0, 1180, 1070, 1, 0, 0, 0, 1180, 1078, 1, 0, 0, 0, 1180, 1082, 1, 0, 0, 0, 1180, 1086, 1, 0, 0, 0, 1180, 1095, 1, 0, 0, 0, 1180, 1100, 1, 0, 0, 0, 1180, 1111, 1, 0, 0, 0, 1180, 1120, 1, 0, 0, 0, 1180, 1127, 1, 0, 0, 0, 1180, 1129, 1, 0, 0, 0, 1180, 1136, 1, 0, 0, 0, 1180, 1160, 1, 0, 0, 0, 1180, 1173, 1, 0, 0, 0, 1181, 15, 1, 0, 0, 0, 1182, 1185, 3, 338, 169, 0, 1183, 1185, 5, 148, 0, 0, 1184, 1182, 1, 0, 0, 0, 1184, 1183, 1, 0, 0, 0, 1185, 17, 1, 0, 0, 0, 1186, 1187, 3, 330, 165, 0, 1187, 19, 1, 0, 0, 0, 1188, 1189, 3, 332, 166, 0, 1189, 21, 1, 0, 0, 0, 1190, 1191, 5, 53, 0, 0, 1191, 1359, 5, 223, 0, 0, 1192, 1193, 5, 82, 0, 0, 1193, 1359, 5, 223, 0, 0, 1194, 1196, 5, 113, 0, 0, 1195, 1197, 5, 223, 0, 0, 1196, 1195, 1, 0, 0, 0, 1196, 1197, 1, 0, 0, 0, 1197, 1359, 1, 0, 0, 0, 1198, 1200, 5, 220, 0, 0, 1199, 1201, 5, 223, 0, 0, 1200, 1199, 1, 0, 0, 0, 1200, 1201, 1, 0, 0, 0, 1201, 1359, 1, 0, 0, 0, 1202, 1203, 5, 242, 0, 0, 1203, 1359, 5, 113, 0, 0, 1204, 1205, 5, 242, 0, 0, 1205, 1207, 5, 223, 0, 0, 1206, 1208, 5, 113, 0, 0, 1207, 1206, 1, 0, 0, 0, 1207, 1208, 1, 0, 0, 0, 1208, 1359, 1, 0, 0, 0, 1209, 1210, 5, 242, 0, 0, 1210, 1359, 5, 201, 0, 0, 1211, 1212, 5, 242, 0, 0, 1212, 1359, 5, 224, 0, 0, 1213, 1214, 5, 242, 0, 0, 1214, 1215, 5, 56, 0, 0, 1215, 1359, 5, 224, 0, 0, 1216, 1217, 5, 92, 0, 0, 1217, 1359, 5, 258, 0, 0, 1218, 1219, 5, 121, 0, 0, 1219, 1359, 5, 258, 0, 0, 1220, 1221, 5, 242, 0, 0, 1221, 1359, 5, 48, 0, 0, 1222, 1223, 5, 242, 0, 0, 1223, 1224, 5, 53, 0, 0, 1224, 1359, 5, 258, 0, 0, 1225, 1226, 5, 242, 0, 0, 1226, 1359, 5, 274, 0, 0, 1227, 1228, 5, 242, 0, 0, 1228, 1359, 5, 125, 0, 0, 1229, 1230, 5, 242, 0, 0, 1230, 1359, 5, 151, 0, 0, 1231, 1232, 5, 53, 0, 0, 1232, 1359, 5, 124, 0, 0, 1233, 1234, 5, 82, 0, 0, 1234, 1359, 5, 124, 0, 0, 1235, 1236, 5, 11, 0, 0, 1236, 1359, 5, 124, 0, 0, 1237, 1238, 5, 150, 0, 0, 1238, 1359, 5, 258, 0, 0, 1239, 1240, 5, 150, 0, 0, 1240, 1359, 5, 65, 0, 0, 1241, 1242, 5, 287, 0, 0, 1242, 1359, 5, 258, 0, 0, 1243, 1244, 5, 287, 0, 0, 1244, 1359, 5, 65, 0, 0, 1245, 1246, 5, 53, 0, 0, 1246, 1247, 5, 263, 0, 0, 1247, 1359, 5, 153, 0, 0, 1248, 1249, 5, 82, 0, 0, 1249, 1250, 5, 263, 0, 0, 1250, 1359, 5, 153, 0, 0, 1251, 1252, 5, 11, 0, 0, 1252, 1253, 5, 258, 0, 0, 1253, 1254, 3, 220, 110, 0, 1254, 1255, 5, 172, 0, 0, 1255, 1256, 5, 39, 0, 0, 1256, 1359, 1, 0, 0, 0, 1257, 1258, 5, 11, 0, 0, 1258, 1259, 5, 258, 0, 0, 1259, 1260, 3, 220, 110, 0, 1260, 1261, 5, 39, 0, 0, 1261, 1262, 5, 28, 0, 0, 1262, 1359, 1, 0, 0, 0, 1263, 1264, 5, 11, 0, 0, 1264, 1265, 5, 258, 0, 0, 1265, 1266, 3, 220, 110, 0, 1266, 1267, 5, 172, 0, 0, 1267, 1268, 5, 246, 0, 0, 1268, 1359, 1, 0, 0, 0, 1269, 1270, 5, 11, 0, 0, 1270, 1271, 5, 258, 0, 0, 1271, 1272, 3, 220, 110, 0, 1272, 1273, 5, 243, 0, 0, 1273, 1274, 5, 28, 0, 0, 1274, 1359, 1, 0, 0, 0, 1275, 1276, 5, 11, 0, 0, 1276, 1277, 5, 258, 0, 0, 1277, 1278, 3, 220, 110, 0, 1278, 1279, 5, 172, 0, 0, 1279, 1280, 5, 243, 0, 0, 1280, 1359, 1, 0, 0, 0, 1281, 1282, 5, 11, 0, 0, 1282, 1283, 5, 258, 0, 0, 1283, 1284, 3, 220, 110, 0, 1284, 1285, 5, 172, 0, 0, 1285, 1286, 5, 250, 0, 0, 1286, 1287, 5, 20, 0, 0, 1287, 1288, 5, 77, 0, 0, 1288, 1359, 1, 0, 0, 0, 1289, 1290, 5, 11, 0, 0, 1290, 1291, 5, 258, 0, 0, 1291, 1292, 3, 220, 110, 0, 1292, 1293, 5, 239, 0, 0, 1293, 1294, 5, 243, 0, 0, 1294, 1295, 5, 149, 0, 0, 1295, 1359, 1, 0, 0, 0, 1296, 1297, 5, 11, 0, 0, 1297, 1298, 5, 258, 0, 0, 1298, 1299, 3, 220, 110, 0, 1299, 1300, 5, 88, 0, 0, 1300, 1301, 5, 190, 0, 0, 1301, 1359, 1, 0, 0, 0, 1302, 1303, 5, 11, 0, 0, 1303, 1304, 5, 258, 0, 0, 1304, 1305, 3, 220, 110, 0, 1305, 1306, 5, 18, 0, 0, 1306, 1307, 5, 190, 0, 0, 1307, 1359, 1, 0, 0, 0, 1308, 1309, 5, 11, 0, 0, 1309, 1310, 5, 258, 0, 0, 1310, 1311, 3, 220, 110, 0, 1311, 1312, 5, 281, 0, 0, 1312, 1313, 5, 190, 0, 0, 1313, 1359, 1, 0, 0, 0, 1314, 1315, 5, 11, 0, 0, 1315, 1316, 5, 258, 0, 0, 1316, 1317, 3, 220, 110, 0, 1317, 1318, 5, 271, 0, 0, 1318, 1359, 1, 0, 0, 0, 1319, 1320, 5, 11, 0, 0, 1320, 1321, 5, 258, 0, 0, 1321, 1323, 3, 220, 110, 0, 1322, 1324, 3, 40, 20, 0, 1323, 1322, 1, 0, 0, 0, 1323, 1324, 1, 0, 0, 0, 1324, 1325, 1, 0, 0, 0, 1325, 1326, 5, 47, 0, 0, 1326, 1359, 1, 0, 0, 0, 1327, 1328, 5, 11, 0, 0, 1328, 1329, 5, 258, 0, 0, 1329, 1331, 3, 220, 110, 0, 1330, 1332, 3, 40, 20, 0, 1331, 1330, 1, 0, 0, 0, 1331, 1332, 1, 0, 0, 0, 1332, 1333, 1, 0, 0, 0, 1333, 1334, 5, 50, 0, 0, 1334, 1359, 1, 0, 0, 0, 1335, 1336, 5, 11, 0, 0, 1336, 1337, 5, 258, 0, 0, 1337, 1339, 3, 220, 110, 0, 1338, 1340, 3, 40, 20, 0, 1339, 1338, 1, 0, 0, 0, 1339, 1340, 1, 0, 0, 0, 1340, 1341, 1, 0, 0, 0, 1341, 1342, 5, 239, 0, 0, 1342, 1343, 5, 100, 0, 0, 1343, 1359, 1, 0, 0, 0, 1344, 1345, 5, 11, 0, 0, 1345, 1346, 5, 258, 0, 0, 1346, 1348, 3, 220, 110, 0, 1347, 1349, 3, 40, 20, 0, 1348, 1347, 1, 0, 0, 0, 1348, 1349, 1, 0, 0, 0, 1349, 1350, 1, 0, 0, 0, 1350, 1351, 5, 216, 0, 0, 1351, 1352, 5, 44, 0, 0, 1352, 1359, 1, 0, 0, 0, 1353, 1354, 5, 248, 0, 0, 1354, 1359, 5, 273, 0, 0, 1355, 1359, 5, 46, 0, 0, 1356, 1359, 5, 225, 0, 0, 1357, 1359, 5, 76, 0, 0, 1358, 1190, 1, 0, 0, 0, 1358, 1192, 1, 0, 0, 0, 1358, 1194, 1, 0, 0, 0, 1358, 1198, 1, 0, 0, 0, 1358, 1202, 1, 0, 0, 0, 1358, 1204, 1, 0, 0, 0, 1358, 1209, 1, 0, 0, 0, 1358, 1211, 1, 0, 0, 0, 1358, 1213, 1, 0, 0, 0, 1358, 1216, 1, 0, 0, 0, 1358, 1218, 1, 0, 0, 0, 1358, 1220, 1, 0, 0, 0, 1358, 1222, 1, 0, 0, 0, 1358, 1225, 1, 0, 0, 0, 1358, 1227, 1, 0, 0, 0, 1358, 1229, 1, 0, 0, 0, 1358, 1231, 1, 0, 0, 0, 1358, 1233, 1, 0, 0, 0, 1358, 1235, 1, 0, 0, 0, 1358, 1237, 1, 0, 0, 0, 1358, 1239, 1, 0, 0, 0, 1358, 1241, 1, 0, 0, 0, 1358, 1243, 1, 0, 0, 0, 1358, 1245, 1, 0, 0, 0, 1358, 1248, 1, 0, 0, 0, 1358, 1251, 1, 0, 0, 0, 1358, 1257, 1, 0, 0, 0, 1358, 1263, 1, 0, 0, 0, 1358, 1269, 1, 0, 0, 0, 1358, 1275, 1, 0, 0, 0, 1358, 1281, 1, 0, 0, 0, 1358, 1289, 1, 0, 0, 0, 1358, 1296, 1, 0, 0, 0, 1358, 1302, 1, 0, 0, 0, 1358, 1308, 1, 0, 0, 0, 1358, 1314, 1, 0, 0, 0, 1358, 1319, 1, 0, 0, 0, 1358, 1327, 1, 0, 0, 0, 1358, 1335, 1, 0, 0, 0, 1358, 1344, 1, 0, 0, 0, 1358, 1353, 1, 0, 0, 0, 1358, 1355, 1, 0, 0, 0, 1358, 1356, 1, 0, 0, 0, 1358, 1357, 1, 0, 0, 0, 1359, 23, 1, 0, 0, 0, 1360, 1362, 5, 53, 0, 0, 1361, 1363, 5, 263, 0, 0, 1362, 1361, 1, 0, 0, 0, 1362, 1363, 1, 0, 0, 0, 1363, 1365, 1, 0, 0, 0, 1364, 1366, 5, 94, 0, 0, 1365, 1364, 1, 0, 0, 0, 1365, 1366, 1, 0, 0, 0, 1366, 1367, 1, 0, 0, 0, 1367, 1371, 5, 258, 0, 0, 1368, 1369, 5, 119, 0, 0, 1369, 1370, 5, 172, 0, 0, 1370, 1372, 5, 90, 0, 0, 1371, 1368, 1, 0, 0, 0, 1371, 1372, 1, 0, 0, 0, 1372, 1373, 1, 0, 0, 0, 1373, 1374, 3, 214, 107, 0, 1374, 25, 1, 0, 0, 0, 1375, 1376, 5, 53, 0, 0, 1376, 1378, 5, 181, 0, 0, 1377, 1375, 1, 0, 0, 0, 1377, 1378, 1, 0, 0, 0, 1378, 1379, 1, 0, 0, 0, 1379, 1380, 5, 216, 0, 0, 1380, 1381, 5, 258, 0, 0, 1381, 1382, 3, 214, 107, 0, 1382, 27, 1, 0, 0, 0, 1383, 1384, 5, 39, 0, 0, 1384, 1385, 5, 28, 0, 0, 1385, 1389, 3, 190, 95, 0, 1386, 1387, 5, 246, 0, 0, 1387, 1388, 5, 28, 0, 0, 1388, 1390, 3, 194, 97, 0, 1389, 1386, 1, 0, 0, 0, 1389, 1390, 1, 0, 0, 0, 1390, 1391, 1, 0, 0, 0, 1391, 1392, 5, 132, 0, 0, 1392, 1393, 5, 335, 0, 0, 1393, 1394, 5, 27, 0, 0, 1394, 29, 1, 0, 0, 0, 1395, 1396, 5, 243, 0, 0, 1396, 1397, 5, 28, 0, 0, 1397, 1398, 3, 190, 95, 0, 1398, 1401, 5, 177, 0, 0, 1399, 1402, 3, 76, 38, 0, 1400, 1402, 3, 78, 39, 0, 1401, 1399, 1, 0, 0, 0, 1401, 1400, 1, 0, 0, 0, 1402, 1406, 1, 0, 0, 0, 1403, 1404, 5, 250, 0, 0, 1404, 1405, 5, 20, 0, 0, 1405, 1407, 5, 77, 0, 0, 1406, 1403, 1, 0, 0, 0, 1406, 1407, 1, 0, 0, 0, 1407, 31, 1, 0, 0, 0, 1408, 1409, 5, 149, 0, 0, 1409, 1410, 3, 338, 169, 0, 1410, 33, 1, 0, 0, 0, 1411, 1412, 5, 45, 0, 0, 1412, 1413, 3, 338, 169, 0, 1413, 35, 1, 0, 0, 0, 1414, 1415, 5, 129, 0, 0, 1415, 1417, 5, 189, 0, 0, 1416, 1418, 5, 258, 0, 0, 1417, 1416, 1, 0, 0, 0, 1417, 1418, 1, 0, 0, 0, 1418, 1419, 1, 0, 0, 0, 1419, 1426, 3, 214, 107, 0, 1420, 1424, 3, 40, 20, 0, 1421, 1422, 5, 119, 0, 0, 1422, 1423, 5, 172, 0, 0, 1423, 1425, 5, 90, 0, 0, 1424, 1421, 1, 0, 0, 0, 1424, 1425, 1, 0, 0, 0, 1425, 1427, 1, 0, 0, 0, 1426, 1420, 1, 0, 0, 0, 1426, 1427, 1, 0, 0, 0, 1427, 1429, 1, 0, 0, 0, 1428, 1430, 3, 190, 95, 0, 1429, 1428, 1, 0, 0, 0, 1429, 1430, 1, 0, 0, 0, 1430, 1485, 1, 0, 0, 0, 1431, 1432, 5, 129, 0, 0, 1432, 1434, 5, 132, 0, 0, 1433, 1435, 5, 258, 0, 0, 1434, 1433, 1, 0, 0, 0, 1434, 1435, 1, 0, 0, 0, 1435, 1436, 1, 0, 0, 0, 1436, 1438, 3, 214, 107, 0, 1437, 1439, 3, 40, 20, 0, 1438, 1437, 1, 0, 0, 0, 1438, 1439, 1, 0, 0, 0, 1439, 1443, 1, 0, 0, 0, 1440, 1441, 5, 119, 0, 0, 1441, 1442, 5, 172, 0, 0, 1442, 1444, 5, 90, 0, 0, 1443, 1440, 1, 0, 0, 0, 1443, 1444, 1, 0, 0, 0, 1444, 1446, 1, 0, 0, 0, 1445, 1447, 3, 190, 95, 0, 1446, 1445, 1, 0, 0, 0, 1446, 1447, 1, 0, 0, 0, 1447, 1485, 1, 0, 0, 0, 1448, 1449, 5, 129, 0, 0, 1449, 1451, 5, 132, 0, 0, 1450, 1452, 5, 258, 0, 0, 1451, 1450, 1, 0, 0, 0, 1451, 1452, 1, 0, 0, 0, 1452, 1453, 1, 0, 0, 0, 1453, 1454, 3, 214, 107, 0, 1454, 1455, 5, 216, 0, 0, 1455, 1456, 3, 122, 61, 0, 1456, 1485, 1, 0, 0, 0, 1457, 1458, 5, 129, 0, 0, 1458, 1460, 5, 189, 0, 0, 1459, 1461, 5, 148, 0, 0, 1460, 1459, 1, 0, 0, 0, 1460, 1461, 1, 0, 0, 0, 1461, 1462, 1, 0, 0, 0, 1462, 1463, 5, 78, 0, 0, 1463, 1465, 3, 338, 169, 0, 1464, 1466, 3, 210, 105, 0, 1465, 1464, 1, 0, 0, 0, 1465, 1466, 1, 0, 0, 0, 1466, 1468, 1, 0, 0, 0, 1467, 1469, 3, 80, 40, 0, 1468, 1467, 1, 0, 0, 0, 1468, 1469, 1, 0, 0, 0, 1469, 1485, 1, 0, 0, 0, 1470, 1471, 5, 129, 0, 0, 1471, 1473, 5, 189, 0, 0, 1472, 1474, 5, 148, 0, 0, 1473, 1472, 1, 0, 0, 0, 1473, 1474, 1, 0, 0, 0, 1474, 1475, 1, 0, 0, 0, 1475, 1477, 5, 78, 0, 0, 1476, 1478, 3, 338, 169, 0, 1477, 1476, 1, 0, 0, 0, 1477, 1478, 1, 0, 0, 0, 1478, 1479, 1, 0, 0, 0, 1479, 1482, 3, 64, 32, 0, 1480, 1481, 5, 180, 0, 0, 1481, 1483, 3, 68, 34, 0, 1482, 1480, 1, 0, 0, 0, 1482, 1483, 1, 0, 0, 0, 1483, 1485, 1, 0, 0, 0, 1484, 1414, 1, 0, 0, 0, 1484, 1431, 1, 0, 0, 0, 1484, 1448, 1, 0, 0, 0, 1484, 1457, 1, 0, 0, 0, 1484, 1470, 1, 0, 0, 0, 1485, 37, 1, 0, 0, 0, 1486, 1488, 3, 40, 20, 0, 1487, 1489, 3, 32, 16, 0, 1488, 1487, 1, 0, 0, 0, 1488, 1489, 1, 0, 0, 0, 1489, 39, 1, 0, 0, 0, 1490, 1491, 5, 190, 0, 0, 1491, 1492, 5, 2, 0, 0, 1492, 1497, 3, 42, 21, 0, 1493, 1494, 5, 4, 0, 0, 1494, 1496, 3, 42, 21, 0, 1495, 1493, 1, 0, 0, 0, 1496, 1499, 1, 0, 0, 0, 1497, 1495, 1, 0, 0, 0, 1497, 1498, 1, 0, 0, 0, 1498, 1500, 1, 0, 0, 0, 1499, 1497, 1, 0, 0, 0, 1500, 1501, 5, 3, 0, 0, 1501, 41, 1, 0, 0, 0, 1502, 1505, 3, 326, 163, 0, 1503, 1504, 5, 308, 0, 0, 1504, 1506, 3, 250, 125, 0, 1505, 1503, 1, 0, 0, 0, 1505, 1506, 1, 0, 0, 0, 1506, 1512, 1, 0, 0, 0, 1507, 1508, 3, 326, 163, 0, 1508, 1509, 5, 308, 0, 0, 1509, 1510, 5, 70, 0, 0, 1510, 1512, 1, 0, 0, 0, 1511, 1502, 1, 0, 0, 0, 1511, 1507, 1, 0, 0, 0, 1512, 43, 1, 0, 0, 0, 1513, 1514, 7, 11, 0, 0, 1514, 45, 1, 0, 0, 0, 1515, 1516, 7, 12, 0, 0, 1516, 47, 1, 0, 0, 0, 1517, 1523, 3, 320, 160, 0, 1518, 1523, 3, 338, 169, 0, 1519, 1523, 3, 252, 126, 0, 1520, 1523, 3, 254, 127, 0, 1521, 1523, 3, 256, 128, 0, 1522, 1517, 1, 0, 0, 0, 1522, 1518, 1, 0, 0, 0, 1522, 1519, 1, 0, 0, 0, 1522, 1520, 1, 0, 0, 0, 1522, 1521, 1, 0, 0, 0, 1523, 49, 1, 0, 0, 0, 1524, 1529, 3, 326, 163, 0, 1525, 1526, 5, 5, 0, 0, 1526, 1528, 3, 326, 163, 0, 1527, 1525, 1, 0, 0, 0, 1528, 1531, 1, 0, 0, 0, 1529, 1527, 1, 0, 0, 0, 1529, 1530, 1, 0, 0, 0, 1530, 51, 1, 0, 0, 0, 1531, 1529, 1, 0, 0, 0, 1532, 1533, 5, 303, 0, 0, 1533, 1538, 3, 56, 28, 0, 1534, 1535, 5, 4, 0, 0, 1535, 1537, 3, 56, 28, 0, 1536, 1534, 1, 0, 0, 0, 1537, 1540, 1, 0, 0, 0, 1538, 1536, 1, 0, 0, 0, 1538, 1539, 1, 0, 0, 0, 1539, 53, 1, 0, 0, 0, 1540, 1538, 1, 0, 0, 0, 1541, 1543, 3, 52, 26, 0, 1542, 1541, 1, 0, 0, 0, 1542, 1543, 1, 0, 0, 0, 1543, 1544, 1, 0, 0, 0, 1544, 1545, 3, 58, 29, 0, 1545, 1546, 3, 90, 45, 0, 1546, 55, 1, 0, 0, 0, 1547, 1549, 3, 322, 161, 0, 1548, 1550, 3, 190, 95, 0, 1549, 1548, 1, 0, 0, 0, 1549, 1550, 1, 0, 0, 0, 1550, 1552, 1, 0, 0, 0, 1551, 1553, 5, 20, 0, 0, 1552, 1551, 1, 0, 0, 0, 1552, 1553, 1, 0, 0, 0, 1553, 1554, 1, 0, 0, 0, 1554, 1555, 5, 2, 0, 0, 1555, 1556, 3, 54, 27, 0, 1556, 1557, 5, 3, 0, 0, 1557, 57, 1, 0, 0, 0, 1558, 1559, 6, 29, -1, 0, 1559, 1560, 3, 62, 31, 0, 1560, 1569, 1, 0, 0, 0, 1561, 1562, 10, 1, 0, 0, 1562, 1564, 7, 13, 0, 0, 1563, 1565, 3, 174, 87, 0, 1564, 1563, 1, 0, 0, 0, 1564, 1565, 1, 0, 0, 0, 1565, 1566, 1, 0, 0, 0, 1566, 1568, 3, 58, 29, 2, 1567, 1561, 1, 0, 0, 0, 1568, 1571, 1, 0, 0, 0, 1569, 1567, 1, 0, 0, 0, 1569, 1570, 1, 0, 0, 0, 1570, 59, 1, 0, 0, 0, 1571, 1569, 1, 0, 0, 0, 1572, 1574, 3, 100, 50, 0, 1573, 1575, 3, 130, 65, 0, 1574, 1573, 1, 0, 0, 0, 1574, 1575, 1, 0, 0, 0, 1575, 1579, 1, 0, 0, 0, 1576, 1578, 3, 172, 86, 0, 1577, 1576, 1, 0, 0, 0, 1578, 1581, 1, 0, 0, 0, 1579, 1577, 1, 0, 0, 0, 1579, 1580, 1, 0, 0, 0, 1580, 1583, 1, 0, 0, 0, 1581, 1579, 1, 0, 0, 0, 1582, 1584, 3, 122, 61, 0, 1583, 1582, 1, 0, 0, 0, 1583, 1584, 1, 0, 0, 0, 1584, 1586, 1, 0, 0, 0, 1585, 1587, 3, 134, 67, 0, 1586, 1585, 1, 0, 0, 0, 1586, 1587, 1, 0, 0, 0, 1587, 1589, 1, 0, 0, 0, 1588, 1590, 3, 124, 62, 0, 1589, 1588, 1, 0, 0, 0, 1589, 1590, 1, 0, 0, 0, 1590, 1592, 1, 0, 0, 0, 1591, 1593, 3, 306, 153, 0, 1592, 1591, 1, 0, 0, 0, 1592, 1593, 1, 0, 0, 0, 1593, 1617, 1, 0, 0, 0, 1594, 1596, 3, 102, 51, 0, 1595, 1597, 3, 130, 65, 0, 1596, 1595, 1, 0, 0, 0, 1596, 1597, 1, 0, 0, 0, 1597, 1601, 1, 0, 0, 0, 1598, 1600, 3, 172, 86, 0, 1599, 1598, 1, 0, 0, 0, 1600, 1603, 1, 0, 0, 0, 1601, 1599, 1, 0, 0, 0, 1601, 1602, 1, 0, 0, 0, 1602, 1605, 1, 0, 0, 0, 1603, 1601, 1, 0, 0, 0, 1604, 1606, 3, 122, 61, 0, 1605, 1604, 1, 0, 0, 0, 1605, 1606, 1, 0, 0, 0, 1606, 1608, 1, 0, 0, 0, 1607, 1609, 3, 134, 67, 0, 1608, 1607, 1, 0, 0, 0, 1608, 1609, 1, 0, 0, 0, 1609, 1611, 1, 0, 0, 0, 1610, 1612, 3, 124, 62, 0, 1611, 1610, 1, 0, 0, 0, 1611, 1612, 1, 0, 0, 0, 1612, 1614, 1, 0, 0, 0, 1613, 1615, 3, 306, 153, 0, 1614, 1613, 1, 0, 0, 0, 1614, 1615, 1, 0, 0, 0, 1615, 1617, 1, 0, 0, 0, 1616, 1572, 1, 0, 0, 0, 1616, 1594, 1, 0, 0, 0, 1617, 61, 1, 0, 0, 0, 1618, 1628, 3, 60, 30, 0, 1619, 1628, 3, 96, 48, 0, 1620, 1621, 5, 258, 0, 0, 1621, 1628, 3, 214, 107, 0, 1622, 1628, 3, 204, 102, 0, 1623, 1624, 5, 2, 0, 0, 1624, 1625, 3, 54, 27, 0, 1625, 1626, 5, 3, 0, 0, 1626, 1628, 1, 0, 0, 0, 1627, 1618, 1, 0, 0, 0, 1627, 1619, 1, 0, 0, 0, 1627, 1620, 1, 0, 0, 0, 1627, 1622, 1, 0, 0, 0, 1627, 1623, 1, 0, 0, 0, 1628, 63, 1, 0, 0, 0, 1629, 1630, 5, 293, 0, 0, 1630, 1631, 3, 214, 107, 0, 1631, 65, 1, 0, 0, 0, 1632, 1633, 5, 180, 0, 0, 1633, 1646, 3, 68, 34, 0, 1634, 1635, 5, 191, 0, 0, 1635, 1636, 5, 28, 0, 0, 1636, 1646, 3, 228, 114, 0, 1637, 1646, 3, 30, 15, 0, 1638, 1646, 3, 28, 14, 0, 1639, 1646, 3, 210, 105, 0, 1640, 1646, 3, 80, 40, 0, 1641, 1646, 3, 32, 16, 0, 1642, 1646, 3, 34, 17, 0, 1643, 1644, 5, 262, 0, 0, 1644, 1646, 3, 68, 34, 0, 1645, 1632, 1, 0, 0, 0, 1645, 1634, 1, 0, 0, 0, 1645, 1637, 1, 0, 0, 0, 1645, 1638, 1, 0, 0, 0, 1645, 1639, 1, 0, 0, 0, 1645, 1640, 1, 0, 0, 0, 1645, 1641, 1, 0, 0, 0, 1645, 1642, 1, 0, 0, 0, 1645, 1643, 1, 0, 0, 0, 1646, 1649, 1, 0, 0, 0, 1647, 1645, 1, 0, 0, 0, 1647, 1648, 1, 0, 0, 0, 1648, 67, 1, 0, 0, 0, 1649, 1647, 1, 0, 0, 0, 1650, 1651, 5, 2, 0, 0, 1651, 1656, 3, 70, 35, 0, 1652, 1653, 5, 4, 0, 0, 1653, 1655, 3, 70, 35, 0, 1654, 1652, 1, 0, 0, 0, 1655, 1658, 1, 0, 0, 0, 1656, 1654, 1, 0, 0, 0, 1656, 1657, 1, 0, 0, 0, 1657, 1659, 1, 0, 0, 0, 1658, 1656, 1, 0, 0, 0, 1659, 1660, 5, 3, 0, 0, 1660, 69, 1, 0, 0, 0, 1661, 1666, 3, 72, 36, 0, 1662, 1664, 5, 308, 0, 0, 1663, 1662, 1, 0, 0, 0, 1663, 1664, 1, 0, 0, 0, 1664, 1665, 1, 0, 0, 0, 1665, 1667, 3, 74, 37, 0, 1666, 1663, 1, 0, 0, 0, 1666, 1667, 1, 0, 0, 0, 1667, 71, 1, 0, 0, 0, 1668, 1673, 3, 326, 163, 0, 1669, 1670, 5, 5, 0, 0, 1670, 1672, 3, 326, 163, 0, 1671, 1669, 1, 0, 0, 0, 1672, 1675, 1, 0, 0, 0, 1673, 1671, 1, 0, 0, 0, 1673, 1674, 1, 0, 0, 0, 1674, 1678, 1, 0, 0, 0, 1675, 1673, 1, 0, 0, 0, 1676, 1678, 3, 338, 169, 0, 1677, 1668, 1, 0, 0, 0, 1677, 1676, 1, 0, 0, 0, 1678, 73, 1, 0, 0, 0, 1679, 1684, 5, 335, 0, 0, 1680, 1684, 5, 337, 0, 0, 1681, 1684, 3, 258, 129, 0, 1682, 1684, 3, 338, 169, 0, 1683, 1679, 1, 0, 0, 0, 1683, 1680, 1, 0, 0, 0, 1683, 1681, 1, 0, 0, 0, 1683, 1682, 1, 0, 0, 0, 1684, 75, 1, 0, 0, 0, 1685, 1686, 5, 2, 0, 0, 1686, 1691, 3, 250, 125, 0, 1687, 1688, 5, 4, 0, 0, 1688, 1690, 3, 250, 125, 0, 1689, 1687, 1, 0, 0, 0, 1690, 1693, 1, 0, 0, 0, 1691, 1689, 1, 0, 0, 0, 1691, 1692, 1, 0, 0, 0, 1692, 1694, 1, 0, 0, 0, 1693, 1691, 1, 0, 0, 0, 1694, 1695, 5, 3, 0, 0, 1695, 77, 1, 0, 0, 0, 1696, 1697, 5, 2, 0, 0, 1697, 1702, 3, 76, 38, 0, 1698, 1699, 5, 4, 0, 0, 1699, 1701, 3, 76, 38, 0, 1700, 1698, 1, 0, 0, 0, 1701, 1704, 1, 0, 0, 0, 1702, 1700, 1, 0, 0, 0, 1702, 1703, 1, 0, 0, 0, 1703, 1705, 1, 0, 0, 0, 1704, 1702, 1, 0, 0, 0, 1705, 1706, 5, 3, 0, 0, 1706, 79, 1, 0, 0, 0, 1707, 1708, 5, 250, 0, 0, 1708, 1709, 5, 20, 0, 0, 1709, 1714, 3, 82, 41, 0, 1710, 1711, 5, 250, 0, 0, 1711, 1712, 5, 28, 0, 0, 1712, 1714, 3, 84, 42, 0, 1713, 1707, 1, 0, 0, 0, 1713, 1710, 1, 0, 0, 0, 1714, 81, 1, 0, 0, 0, 1715, 1716, 5, 128, 0, 0, 1716, 1717, 3, 338, 169, 0, 1717, 1718, 5, 185, 0, 0, 1718, 1719, 3, 338, 169, 0, 1719, 1722, 1, 0, 0, 0, 1720, 1722, 3, 326, 163, 0, 1721, 1715, 1, 0, 0, 0, 1721, 1720, 1, 0, 0, 0, 1722, 83, 1, 0, 0, 0, 1723, 1727, 3, 338, 169, 0, 1724, 1725, 5, 303, 0, 0, 1725, 1726, 5, 237, 0, 0, 1726, 1728, 3, 68, 34, 0, 1727, 1724, 1, 0, 0, 0, 1727, 1728, 1, 0, 0, 0, 1728, 85, 1, 0, 0, 0, 1729, 1730, 3, 326, 163, 0, 1730, 1731, 3, 338, 169, 0, 1731, 87, 1, 0, 0, 0, 1732, 1733, 3, 36, 18, 0, 1733, 1734, 3, 54, 27, 0, 1734, 1789, 1, 0, 0, 0, 1735, 1737, 3, 130, 65, 0, 1736, 1738, 3, 92, 46, 0, 1737, 1736, 1, 0, 0, 0, 1738, 1739, 1, 0, 0, 0, 1739, 1737, 1, 0, 0, 0, 1739, 1740, 1, 0, 0, 0, 1740, 1789, 1, 0, 0, 0, 1741, 1742, 5, 72, 0, 0, 1742, 1743, 5, 107, 0, 0, 1743, 1744, 3, 214, 107, 0, 1744, 1746, 3, 208, 104, 0, 1745, 1747, 3, 122, 61, 0, 1746, 1745, 1, 0, 0, 0, 1746, 1747, 1, 0, 0, 0, 1747, 1789, 1, 0, 0, 0, 1748, 1749, 5, 290, 0, 0, 1749, 1750, 3, 214, 107, 0, 1750, 1751, 3, 208, 104, 0, 1751, 1753, 3, 104, 52, 0, 1752, 1754, 3, 122, 61, 0, 1753, 1752, 1, 0, 0, 0, 1753, 1754, 1, 0, 0, 0, 1754, 1789, 1, 0, 0, 0, 1755, 1756, 5, 156, 0, 0, 1756, 1757, 5, 132, 0, 0, 1757, 1758, 3, 214, 107, 0, 1758, 1759, 3, 208, 104, 0, 1759, 1765, 5, 293, 0, 0, 1760, 1766, 3, 214, 107, 0, 1761, 1762, 5, 2, 0, 0, 1762, 1763, 3, 54, 27, 0, 1763, 1764, 5, 3, 0, 0, 1764, 1766, 1, 0, 0, 0, 1765, 1760, 1, 0, 0, 0, 1765, 1761, 1, 0, 0, 0, 1766, 1767, 1, 0, 0, 0, 1767, 1768, 3, 208, 104, 0, 1768, 1769, 5, 177, 0, 0, 1769, 1773, 3, 240, 120, 0, 1770, 1772, 3, 106, 53, 0, 1771, 1770, 1, 0, 0, 0, 1772, 1775, 1, 0, 0, 0, 1773, 1771, 1, 0, 0, 0, 1773, 1774, 1, 0, 0, 0, 1774, 1779, 1, 0, 0, 0, 1775, 1773, 1, 0, 0, 0, 1776, 1778, 3, 108, 54, 0, 1777, 1776, 1, 0, 0, 0, 1778, 1781, 1, 0, 0, 0, 1779, 1777, 1, 0, 0, 0, 1779, 1780, 1, 0, 0, 0, 1780, 1785, 1, 0, 0, 0, 1781, 1779, 1, 0, 0, 0, 1782, 1784, 3, 110, 55, 0, 1783, 1782, 1, 0, 0, 0, 1784, 1787, 1, 0, 0, 0, 1785, 1783, 1, 0, 0, 0, 1785, 1786, 1, 0, 0, 0, 1786, 1789, 1, 0, 0, 0, 1787, 1785, 1, 0, 0, 0, 1788, 1732, 1, 0, 0, 0, 1788, 1735, 1, 0, 0, 0, 1788, 1741, 1, 0, 0, 0, 1788, 1748, 1, 0, 0, 0, 1788, 1755, 1, 0, 0, 0, 1789, 89, 1, 0, 0, 0, 1790, 1791, 5, 182, 0, 0, 1791, 1792, 5, 28, 0, 0, 1792, 1797, 3, 94, 47, 0, 1793, 1794, 5, 4, 0, 0, 1794, 1796, 3, 94, 47, 0, 1795, 1793, 1, 0, 0, 0, 1796, 1799, 1, 0, 0, 0, 1797, 1795, 1, 0, 0, 0, 1797, 1798, 1, 0, 0, 0, 1798, 1801, 1, 0, 0, 0, 1799, 1797, 1, 0, 0, 0, 1800, 1790, 1, 0, 0, 0, 1800, 1801, 1, 0, 0, 0, 1801, 1812, 1, 0, 0, 0, 1802, 1803, 5, 38, 0, 0, 1803, 1804, 5, 28, 0, 0, 1804, 1809, 3, 236, 118, 0, 1805, 1806, 5, 4, 0, 0, 1806, 1808, 3, 236, 118, 0, 1807, 1805, 1, 0, 0, 0, 1808, 1811, 1, 0, 0, 0, 1809, 1807, 1, 0, 0, 0, 1809, 1810, 1, 0, 0, 0, 1810, 1813, 1, 0, 0, 0, 1811, 1809, 1, 0, 0, 0, 1812, 1802, 1, 0, 0, 0, 1812, 1813, 1, 0, 0, 0, 1813, 1824, 1, 0, 0, 0, 1814, 1815, 5, 80, 0, 0, 1815, 1816, 5, 28, 0, 0, 1816, 1821, 3, 236, 118, 0, 1817, 1818, 5, 4, 0, 0, 1818, 1820, 3, 236, 118, 0, 1819, 1817, 1, 0, 0, 0, 1820, 1823, 1, 0, 0, 0, 1821, 1819, 1, 0, 0, 0, 1821, 1822, 1, 0, 0, 0, 1822, 1825, 1, 0, 0, 0, 1823, 1821, 1, 0, 0, 0, 1824, 1814, 1, 0, 0, 0, 1824, 1825, 1, 0, 0, 0, 1825, 1836, 1, 0, 0, 0, 1826, 1827, 5, 245, 0, 0, 1827, 1828, 5, 28, 0, 0, 1828, 1833, 3, 94, 47, 0, 1829, 1830, 5, 4, 0, 0, 1830, 1832, 3, 94, 47, 0, 1831, 1829, 1, 0, 0, 0, 1832, 1835, 1, 0, 0, 0, 1833, 1831, 1, 0, 0, 0, 1833, 1834, 1, 0, 0, 0, 1834, 1837, 1, 0, 0, 0, 1835, 1833, 1, 0, 0, 0, 1836, 1826, 1, 0, 0, 0, 1836, 1837, 1, 0, 0, 0, 1837, 1839, 1, 0, 0, 0, 1838, 1840, 3, 306, 153, 0, 1839, 1838, 1, 0, 0, 0, 1839, 1840, 1, 0, 0, 0, 1840, 1846, 1, 0, 0, 0, 1841, 1844, 5, 144, 0, 0, 1842, 1845, 5, 10, 0, 0, 1843, 1845, 3, 236, 118, 0, 1844, 1842, 1, 0, 0, 0, 1844, 1843, 1, 0, 0, 0, 1845, 1847, 1, 0, 0, 0, 1846, 1841, 1, 0, 0, 0, 1846, 1847, 1, 0, 0, 0, 1847, 1850, 1, 0, 0, 0, 1848, 1849, 5, 176, 0, 0, 1849, 1851, 3, 236, 118, 0, 1850, 1848, 1, 0, 0, 0, 1850, 1851, 1, 0, 0, 0, 1851, 91, 1, 0, 0, 0, 1852, 1853, 3, 36, 18, 0, 1853, 1854, 3, 98, 49, 0, 1854, 93, 1, 0, 0, 0, 1855, 1857, 3, 236, 118, 0, 1856, 1858, 7, 14, 0, 0, 1857, 1856, 1, 0, 0, 0, 1857, 1858, 1, 0, 0, 0, 1858, 1861, 1, 0, 0, 0, 1859, 1860, 5, 174, 0, 0, 1860, 1862, 7, 15, 0, 0, 1861, 1859, 1, 0, 0, 0, 1861, 1862, 1, 0, 0, 0, 1862, 95, 1, 0, 0, 0, 1863, 1865, 3, 130, 65, 0, 1864, 1866, 3, 98, 49, 0, 1865, 1864, 1, 0, 0, 0, 1866, 1867, 1, 0, 0, 0, 1867, 1865, 1, 0, 0, 0, 1867, 1868, 1, 0, 0, 0, 1868, 97, 1, 0, 0, 0, 1869, 1871, 3, 100, 50, 0, 1870, 1872, 3, 122, 61, 0, 1871, 1870, 1, 0, 0, 0, 1871, 1872, 1, 0, 0, 0, 1872, 1873, 1, 0, 0, 0, 1873, 1874, 3, 90, 45, 0, 1874, 1897, 1, 0, 0, 0, 1875, 1879, 3, 102, 51, 0, 1876, 1878, 3, 172, 86, 0, 1877, 1876, 1, 0, 0, 0, 1878, 1881, 1, 0, 0, 0, 1879, 1877, 1, 0, 0, 0, 1879, 1880, 1, 0, 0, 0, 1880, 1883, 1, 0, 0, 0, 1881, 1879, 1, 0, 0, 0, 1882, 1884, 3, 122, 61, 0, 1883, 1882, 1, 0, 0, 0, 1883, 1884, 1, 0, 0, 0, 1884, 1886, 1, 0, 0, 0, 1885, 1887, 3, 134, 67, 0, 1886, 1885, 1, 0, 0, 0, 1886, 1887, 1, 0, 0, 0, 1887, 1889, 1, 0, 0, 0, 1888, 1890, 3, 124, 62, 0, 1889, 1888, 1, 0, 0, 0, 1889, 1890, 1, 0, 0, 0, 1890, 1892, 1, 0, 0, 0, 1891, 1893, 3, 306, 153, 0, 1892, 1891, 1, 0, 0, 0, 1892, 1893, 1, 0, 0, 0, 1893, 1894, 1, 0, 0, 0, 1894, 1895, 3, 90, 45, 0, 1895, 1897, 1, 0, 0, 0, 1896, 1869, 1, 0, 0, 0, 1896, 1875, 1, 0, 0, 0, 1897, 99, 1, 0, 0, 0, 1898, 1899, 5, 233, 0, 0, 1899, 1900, 5, 275, 0, 0, 1900, 1902, 5, 2, 0, 0, 1901, 1903, 3, 174, 87, 0, 1902, 1901, 1, 0, 0, 0, 1902, 1903, 1, 0, 0, 0, 1903, 1904, 1, 0, 0, 0, 1904, 1905, 3, 238, 119, 0, 1905, 1906, 5, 3, 0, 0, 1906, 1918, 1, 0, 0, 0, 1907, 1909, 5, 154, 0, 0, 1908, 1910, 3, 174, 87, 0, 1909, 1908, 1, 0, 0, 0, 1909, 1910, 1, 0, 0, 0, 1910, 1911, 1, 0, 0, 0, 1911, 1918, 3, 238, 119, 0, 1912, 1914, 5, 210, 0, 0, 1913, 1915, 3, 174, 87, 0, 1914, 1913, 1, 0, 0, 0, 1914, 1915, 1, 0, 0, 0, 1915, 1916, 1, 0, 0, 0, 1916, 1918, 3, 238, 119, 0, 1917, 1898, 1, 0, 0, 0, 1917, 1907, 1, 0, 0, 0, 1917, 1912, 1, 0, 0, 0, 1918, 1920, 1, 0, 0, 0, 1919, 1921, 3, 210, 105, 0, 1920, 1919, 1, 0, 0, 0, 1920, 1921, 1, 0, 0, 0, 1921, 1924, 1, 0, 0, 0, 1922, 1923, 5, 208, 0, 0, 1923, 1925, 3, 338, 169, 0, 1924, 1922, 1, 0, 0, 0, 1924, 1925, 1, 0, 0, 0, 1925, 1926, 1, 0, 0, 0, 1926, 1927, 5, 293, 0, 0, 1927, 1940, 3, 338, 169, 0, 1928, 1938, 5, 20, 0, 0, 1929, 1939, 3, 192, 96, 0, 1930, 1939, 3, 288, 144, 0, 1931, 1934, 5, 2, 0, 0, 1932, 1935, 3, 192, 96, 0, 1933, 1935, 3, 288, 144, 0, 1934, 1932, 1, 0, 0, 0, 1934, 1933, 1, 0, 0, 0, 1935, 1936, 1, 0, 0, 0, 1936, 1937, 5, 3, 0, 0, 1937, 1939, 1, 0, 0, 0, 1938, 1929, 1, 0, 0, 0, 1938, 1930, 1, 0, 0, 0, 1938, 1931, 1, 0, 0, 0, 1939, 1941, 1, 0, 0, 0, 1940, 1928, 1, 0, 0, 0, 1940, 1941, 1, 0, 0, 0, 1941, 1943, 1, 0, 0, 0, 1942, 1944, 3, 210, 105, 0, 1943, 1942, 1, 0, 0, 0, 1943, 1944, 1, 0, 0, 0, 1944, 1947, 1, 0, 0, 0, 1945, 1946, 5, 207, 0, 0, 1946, 1948, 3, 338, 169, 0, 1947, 1945, 1, 0, 0, 0, 1947, 1948, 1, 0, 0, 0, 1948, 101, 1, 0, 0, 0, 1949, 1953, 5, 233, 0, 0, 1950, 1952, 3, 126, 63, 0, 1951, 1950, 1, 0, 0, 0, 1952, 1955, 1, 0, 0, 0, 1953, 1951, 1, 0, 0, 0, 1953, 1954, 1, 0, 0, 0, 1954, 1957, 1, 0, 0, 0, 1955, 1953, 1, 0, 0, 0, 1956, 1958, 3, 174, 87, 0, 1957, 1956, 1, 0, 0, 0, 1957, 1958, 1, 0, 0, 0, 1958, 1959, 1, 0, 0, 0, 1959, 1960, 3, 226, 113, 0, 1960, 103, 1, 0, 0, 0, 1961, 1962, 5, 239, 0, 0, 1962, 1963, 3, 118, 59, 0, 1963, 105, 1, 0, 0, 0, 1964, 1965, 5, 300, 0, 0, 1965, 1968, 5, 155, 0, 0, 1966, 1967, 5, 14, 0, 0, 1967, 1969, 3, 240, 120, 0, 1968, 1966, 1, 0, 0, 0, 1968, 1969, 1, 0, 0, 0, 1969, 1970, 1, 0, 0, 0, 1970, 1971, 5, 265, 0, 0, 1971, 1972, 3, 112, 56, 0, 1972, 107, 1, 0, 0, 0, 1973, 1974, 5, 300, 0, 0, 1974, 1975, 5, 172, 0, 0, 1975, 1978, 5, 155, 0, 0, 1976, 1977, 5, 28, 0, 0, 1977, 1979, 5, 261, 0, 0, 1978, 1976, 1, 0, 0, 0, 1978, 1979, 1, 0, 0, 0, 1979, 1982, 1, 0, 0, 0, 1980, 1981, 5, 14, 0, 0, 1981, 1983, 3, 240, 120, 0, 1982, 1980, 1, 0, 0, 0, 1982, 1983, 1, 0, 0, 0, 1983, 1984, 1, 0, 0, 0, 1984, 1985, 5, 265, 0, 0, 1985, 1986, 3, 114, 57, 0, 1986, 109, 1, 0, 0, 0, 1987, 1988, 5, 300, 0, 0, 1988, 1989, 5, 172, 0, 0, 1989, 1990, 5, 155, 0, 0, 1990, 1991, 5, 28, 0, 0, 1991, 1994, 5, 247, 0, 0, 1992, 1993, 5, 14, 0, 0, 1993, 1995, 3, 240, 120, 0, 1994, 1992, 1, 0, 0, 0, 1994, 1995, 1, 0, 0, 0, 1995, 1996, 1, 0, 0, 0, 1996, 1997, 5, 265, 0, 0, 1997, 1998, 3, 116, 58, 0, 1998, 111, 1, 0, 0, 0, 1999, 2007, 5, 72, 0, 0, 2000, 2001, 5, 290, 0, 0, 2001, 2002, 5, 239, 0, 0, 2002, 2007, 5, 318, 0, 0, 2003, 2004, 5, 290, 0, 0, 2004, 2005, 5, 239, 0, 0, 2005, 2007, 3, 118, 59, 0, 2006, 1999, 1, 0, 0, 0, 2006, 2000, 1, 0, 0, 0, 2006, 2003, 1, 0, 0, 0, 2007, 113, 1, 0, 0, 0, 2008, 2009, 5, 129, 0, 0, 2009, 2027, 5, 318, 0, 0, 2010, 2011, 5, 129, 0, 0, 2011, 2012, 5, 2, 0, 0, 2012, 2013, 3, 212, 106, 0, 2013, 2014, 5, 3, 0, 0, 2014, 2015, 5, 294, 0, 0, 2015, 2016, 5, 2, 0, 0, 2016, 2021, 3, 236, 118, 0, 2017, 2018, 5, 4, 0, 0, 2018, 2020, 3, 236, 118, 0, 2019, 2017, 1, 0, 0, 0, 2020, 2023, 1, 0, 0, 0, 2021, 2019, 1, 0, 0, 0, 2021, 2022, 1, 0, 0, 0, 2022, 2024, 1, 0, 0, 0, 2023, 2021, 1, 0, 0, 0, 2024, 2025, 5, 3, 0, 0, 2025, 2027, 1, 0, 0, 0, 2026, 2008, 1, 0, 0, 0, 2026, 2010, 1, 0, 0, 0, 2027, 115, 1, 0, 0, 0, 2028, 2033, 5, 72, 0, 0, 2029, 2030, 5, 290, 0, 0, 2030, 2031, 5, 239, 0, 0, 2031, 2033, 3, 118, 59, 0, 2032, 2028, 1, 0, 0, 0, 2032, 2029, 1, 0, 0, 0, 2033, 117, 1, 0, 0, 0, 2034, 2039, 3, 120, 60, 0, 2035, 2036, 5, 4, 0, 0, 2036, 2038, 3, 120, 60, 0, 2037, 2035, 1, 0, 0, 0, 2038, 2041, 1, 0, 0, 0, 2039, 2037, 1, 0, 0, 0, 2039, 2040, 1, 0, 0, 0, 2040, 119, 1, 0, 0, 0, 2041, 2039, 1, 0, 0, 0, 2042, 2043, 3, 214, 107, 0, 2043, 2044, 5, 308, 0, 0, 2044, 2045, 3, 236, 118, 0, 2045, 121, 1, 0, 0, 0, 2046, 2047, 5, 301, 0, 0, 2047, 2048, 3, 240, 120, 0, 2048, 123, 1, 0, 0, 0, 2049, 2050, 5, 116, 0, 0, 2050, 2051, 3, 240, 120, 0, 2051, 125, 1, 0, 0, 0, 2052, 2053, 5, 328, 0, 0, 2053, 2060, 3, 128, 64, 0, 2054, 2056, 5, 4, 0, 0, 2055, 2054, 1, 0, 0, 0, 2055, 2056, 1, 0, 0, 0, 2056, 2057, 1, 0, 0, 0, 2057, 2059, 3, 128, 64, 0, 2058, 2055, 1, 0, 0, 0, 2059, 2062, 1, 0, 0, 0, 2060, 2058, 1, 0, 0, 0, 2060, 2061, 1, 0, 0, 0, 2061, 2063, 1, 0, 0, 0, 2062, 2060, 1, 0, 0, 0, 2063, 2064, 5, 329, 0, 0, 2064, 127, 1, 0, 0, 0, 2065, 2079, 3, 326, 163, 0, 2066, 2067, 3, 326, 163, 0, 2067, 2068, 5, 2, 0, 0, 2068, 2073, 3, 248, 124, 0, 2069, 2070, 5, 4, 0, 0, 2070, 2072, 3, 248, 124, 0, 2071, 2069, 1, 0, 0, 0, 2072, 2075, 1, 0, 0, 0, 2073, 2071, 1, 0, 0, 0, 2073, 2074, 1, 0, 0, 0, 2074, 2076, 1, 0, 0, 0, 2075, 2073, 1, 0, 0, 0, 2076, 2077, 5, 3, 0, 0, 2077, 2079, 1, 0, 0, 0, 2078, 2065, 1, 0, 0, 0, 2078, 2066, 1, 0, 0, 0, 2079, 129, 1, 0, 0, 0, 2080, 2081, 5, 107, 0, 0, 2081, 2086, 3, 176, 88, 0, 2082, 2083, 5, 4, 0, 0, 2083, 2085, 3, 176, 88, 0, 2084, 2082, 1, 0, 0, 0, 2085, 2088, 1, 0, 0, 0, 2086, 2084, 1, 0, 0, 0, 2086, 2087, 1, 0, 0, 0, 2087, 2092, 1, 0, 0, 0, 2088, 2086, 1, 0, 0, 0, 2089, 2091, 3, 172, 86, 0, 2090, 2089, 1, 0, 0, 0, 2091, 2094, 1, 0, 0, 0, 2092, 2090, 1, 0, 0, 0, 2092, 2093, 1, 0, 0, 0, 2093, 2096, 1, 0, 0, 0, 2094, 2092, 1, 0, 0, 0, 2095, 2097, 3, 144, 72, 0, 2096, 2095, 1, 0, 0, 0, 2096, 2097, 1, 0, 0, 0, 2097, 2099, 1, 0, 0, 0, 2098, 2100, 3, 150, 75, 0, 2099, 2098, 1, 0, 0, 0, 2099, 2100, 1, 0, 0, 0, 2100, 131, 1, 0, 0, 0, 2101, 2103, 5, 103, 0, 0, 2102, 2101, 1, 0, 0, 0, 2102, 2103, 1, 0, 0, 0, 2103, 2104, 1, 0, 0, 0, 2104, 2105, 7, 16, 0, 0, 2105, 2106, 5, 20, 0, 0, 2106, 2107, 5, 175, 0, 0, 2107, 2116, 3, 342, 171, 0, 2108, 2110, 5, 103, 0, 0, 2109, 2108, 1, 0, 0, 0, 2109, 2110, 1, 0, 0, 0, 2110, 2111, 1, 0, 0, 0, 2111, 2112, 7, 17, 0, 0, 2112, 2113, 5, 20, 0, 0, 2113, 2114, 5, 175, 0, 0, 2114, 2116, 3, 244, 122, 0, 2115, 2102, 1, 0, 0, 0, 2115, 2109, 1, 0, 0, 0, 2116, 133, 1, 0, 0, 0, 2117, 2118, 5, 114, 0, 0, 2118, 2119, 5, 28, 0, 0, 2119, 2124, 3, 136, 68, 0, 2120, 2121, 5, 4, 0, 0, 2121, 2123, 3, 136, 68, 0, 2122, 2120, 1, 0, 0, 0, 2123, 2126, 1, 0, 0, 0, 2124, 2122, 1, 0, 0, 0, 2124, 2125, 1, 0, 0, 0, 2125, 2157, 1, 0, 0, 0, 2126, 2124, 1, 0, 0, 0, 2127, 2128, 5, 114, 0, 0, 2128, 2129, 5, 28, 0, 0, 2129, 2134, 3, 236, 118, 0, 2130, 2131, 5, 4, 0, 0, 2131, 2133, 3, 236, 118, 0, 2132, 2130, 1, 0, 0, 0, 2133, 2136, 1, 0, 0, 0, 2134, 2132, 1, 0, 0, 0, 2134, 2135, 1, 0, 0, 0, 2135, 2154, 1, 0, 0, 0, 2136, 2134, 1, 0, 0, 0, 2137, 2138, 5, 303, 0, 0, 2138, 2155, 5, 226, 0, 0, 2139, 2140, 5, 303, 0, 0, 2140, 2155, 5, 55, 0, 0, 2141, 2142, 5, 115, 0, 0, 2142, 2143, 5, 241, 0, 0, 2143, 2144, 5, 2, 0, 0, 2144, 2149, 3, 142, 71, 0, 2145, 2146, 5, 4, 0, 0, 2146, 2148, 3, 142, 71, 0, 2147, 2145, 1, 0, 0, 0, 2148, 2151, 1, 0, 0, 0, 2149, 2147, 1, 0, 0, 0, 2149, 2150, 1, 0, 0, 0, 2150, 2152, 1, 0, 0, 0, 2151, 2149, 1, 0, 0, 0, 2152, 2153, 5, 3, 0, 0, 2153, 2155, 1, 0, 0, 0, 2154, 2137, 1, 0, 0, 0, 2154, 2139, 1, 0, 0, 0, 2154, 2141, 1, 0, 0, 0, 2154, 2155, 1, 0, 0, 0, 2155, 2157, 1, 0, 0, 0, 2156, 2117, 1, 0, 0, 0, 2156, 2127, 1, 0, 0, 0, 2157, 135, 1, 0, 0, 0, 2158, 2161, 3, 138, 69, 0, 2159, 2161, 3, 236, 118, 0, 2160, 2158, 1, 0, 0, 0, 2160, 2159, 1, 0, 0, 0, 2161, 137, 1, 0, 0, 0, 2162, 2163, 7, 18, 0, 0, 2163, 2164, 5, 2, 0, 0, 2164, 2169, 3, 142, 71, 0, 2165, 2166, 5, 4, 0, 0, 2166, 2168, 3, 142, 71, 0, 2167, 2165, 1, 0, 0, 0, 2168, 2171, 1, 0, 0, 0, 2169, 2167, 1, 0, 0, 0, 2169, 2170, 1, 0, 0, 0, 2170, 2172, 1, 0, 0, 0, 2171, 2169, 1, 0, 0, 0, 2172, 2173, 5, 3, 0, 0, 2173, 2188, 1, 0, 0, 0, 2174, 2175, 5, 115, 0, 0, 2175, 2176, 5, 241, 0, 0, 2176, 2177, 5, 2, 0, 0, 2177, 2182, 3, 140, 70, 0, 2178, 2179, 5, 4, 0, 0, 2179, 2181, 3, 140, 70, 0, 2180, 2178, 1, 0, 0, 0, 2181, 2184, 1, 0, 0, 0, 2182, 2180, 1, 0, 0, 0, 2182, 2183, 1, 0, 0, 0, 2183, 2185, 1, 0, 0, 0, 2184, 2182, 1, 0, 0, 0, 2185, 2186, 5, 3, 0, 0, 2186, 2188, 1, 0, 0, 0, 2187, 2162, 1, 0, 0, 0, 2187, 2174, 1, 0, 0, 0, 2188, 139, 1, 0, 0, 0, 2189, 2192, 3, 138, 69, 0, 2190, 2192, 3, 142, 71, 0, 2191, 2189, 1, 0, 0, 0, 2191, 2190, 1, 0, 0, 0, 2192, 141, 1, 0, 0, 0, 2193, 2202, 5, 2, 0, 0, 2194, 2199, 3, 236, 118, 0, 2195, 2196, 5, 4, 0, 0, 2196, 2198, 3, 236, 118, 0, 2197, 2195, 1, 0, 0, 0, 2198, 2201, 1, 0, 0, 0, 2199, 2197, 1, 0, 0, 0, 2199, 2200, 1, 0, 0, 0, 2200, 2203, 1, 0, 0, 0, 2201, 2199, 1, 0, 0, 0, 2202, 2194, 1, 0, 0, 0, 2202, 2203, 1, 0, 0, 0, 2203, 2204, 1, 0, 0, 0, 2204, 2207, 5, 3, 0, 0, 2205, 2207, 3, 236, 118, 0, 2206, 2193, 1, 0, 0, 0, 2206, 2205, 1, 0, 0, 0, 2207, 143, 1, 0, 0, 0, 2208, 2209, 5, 196, 0, 0, 2209, 2210, 5, 2, 0, 0, 2210, 2211, 3, 226, 113, 0, 2211, 2212, 5, 103, 0, 0, 2212, 2213, 3, 146, 73, 0, 2213, 2214, 5, 122, 0, 0, 2214, 2215, 5, 2, 0, 0, 2215, 2220, 3, 148, 74, 0, 2216, 2217, 5, 4, 0, 0, 2217, 2219, 3, 148, 74, 0, 2218, 2216, 1, 0, 0, 0, 2219, 2222, 1, 0, 0, 0, 2220, 2218, 1, 0, 0, 0, 2220, 2221, 1, 0, 0, 0, 2221, 2223, 1, 0, 0, 0, 2222, 2220, 1, 0, 0, 0, 2223, 2224, 5, 3, 0, 0, 2224, 2225, 5, 3, 0, 0, 2225, 145, 1, 0, 0, 0, 2226, 2239, 3, 326, 163, 0, 2227, 2228, 5, 2, 0, 0, 2228, 2233, 3, 326, 163, 0, 2229, 2230, 5, 4, 0, 0, 2230, 2232, 3, 326, 163, 0, 2231, 2229, 1, 0, 0, 0, 2232, 2235, 1, 0, 0, 0, 2233, 2231, 1, 0, 0, 0, 2233, 2234, 1, 0, 0, 0, 2234, 2236, 1, 0, 0, 0, 2235, 2233, 1, 0, 0, 0, 2236, 2237, 5, 3, 0, 0, 2237, 2239, 1, 0, 0, 0, 2238, 2226, 1, 0, 0, 0, 2238, 2227, 1, 0, 0, 0, 2239, 147, 1, 0, 0, 0, 2240, 2245, 3, 236, 118, 0, 2241, 2243, 5, 20, 0, 0, 2242, 2241, 1, 0, 0, 0, 2242, 2243, 1, 0, 0, 0, 2243, 2244, 1, 0, 0, 0, 2244, 2246, 3, 326, 163, 0, 2245, 2242, 1, 0, 0, 0, 2245, 2246, 1, 0, 0, 0, 2246, 149, 1, 0, 0, 0, 2247, 2249, 5, 288, 0, 0, 2248, 2250, 3, 152, 76, 0, 2249, 2248, 1, 0, 0, 0, 2249, 2250, 1, 0, 0, 0, 2250, 2251, 1, 0, 0, 0, 2251, 2252, 5, 2, 0, 0, 2252, 2253, 3, 154, 77, 0, 2253, 2258, 5, 3, 0, 0, 2254, 2256, 5, 20, 0, 0, 2255, 2254, 1, 0, 0, 0, 2255, 2256, 1, 0, 0, 0, 2256, 2257, 1, 0, 0, 0, 2257, 2259, 3, 326, 163, 0, 2258, 2255, 1, 0, 0, 0, 2258, 2259, 1, 0, 0, 0, 2259, 151, 1, 0, 0, 0, 2260, 2261, 7, 19, 0, 0, 2261, 2262, 5, 174, 0, 0, 2262, 153, 1, 0, 0, 0, 2263, 2266, 3, 156, 78, 0, 2264, 2266, 3, 158, 79, 0, 2265, 2263, 1, 0, 0, 0, 2265, 2264, 1, 0, 0, 0, 2266, 155, 1, 0, 0, 0, 2267, 2268, 3, 162, 81, 0, 2268, 2269, 5, 103, 0, 0, 2269, 2270, 3, 164, 82, 0, 2270, 2271, 5, 122, 0, 0, 2271, 2272, 5, 2, 0, 0, 2272, 2277, 3, 166, 83, 0, 2273, 2274, 5, 4, 0, 0, 2274, 2276, 3, 166, 83, 0, 2275, 2273, 1, 0, 0, 0, 2276, 2279, 1, 0, 0, 0, 2277, 2275, 1, 0, 0, 0, 2277, 2278, 1, 0, 0, 0, 2278, 2280, 1, 0, 0, 0, 2279, 2277, 1, 0, 0, 0, 2280, 2281, 5, 3, 0, 0, 2281, 157, 1, 0, 0, 0, 2282, 2283, 5, 2, 0, 0, 2283, 2288, 3, 162, 81, 0, 2284, 2285, 5, 4, 0, 0, 2285, 2287, 3, 162, 81, 0, 2286, 2284, 1, 0, 0, 0, 2287, 2290, 1, 0, 0, 0, 2288, 2286, 1, 0, 0, 0, 2288, 2289, 1, 0, 0, 0, 2289, 2291, 1, 0, 0, 0, 2290, 2288, 1, 0, 0, 0, 2291, 2292, 5, 3, 0, 0, 2292, 2293, 5, 103, 0, 0, 2293, 2294, 3, 164, 82, 0, 2294, 2295, 5, 122, 0, 0, 2295, 2296, 5, 2, 0, 0, 2296, 2301, 3, 160, 80, 0, 2297, 2298, 5, 4, 0, 0, 2298, 2300, 3, 160, 80, 0, 2299, 2297, 1, 0, 0, 0, 2300, 2303, 1, 0, 0, 0, 2301, 2299, 1, 0, 0, 0, 2301, 2302, 1, 0, 0, 0, 2302, 2304, 1, 0, 0, 0, 2303, 2301, 1, 0, 0, 0, 2304, 2305, 5, 3, 0, 0, 2305, 159, 1, 0, 0, 0, 2306, 2307, 5, 2, 0, 0, 2307, 2312, 3, 168, 84, 0, 2308, 2309, 5, 4, 0, 0, 2309, 2311, 3, 168, 84, 0, 2310, 2308, 1, 0, 0, 0, 2311, 2314, 1, 0, 0, 0, 2312, 2310, 1, 0, 0, 0, 2312, 2313, 1, 0, 0, 0, 2313, 2315, 1, 0, 0, 0, 2314, 2312, 1, 0, 0, 0, 2315, 2317, 5, 3, 0, 0, 2316, 2318, 3, 170, 85, 0, 2317, 2316, 1, 0, 0, 0, 2317, 2318, 1, 0, 0, 0, 2318, 161, 1, 0, 0, 0, 2319, 2320, 3, 326, 163, 0, 2320, 163, 1, 0, 0, 0, 2321, 2322, 3, 326, 163, 0, 2322, 165, 1, 0, 0, 0, 2323, 2325, 3, 168, 84, 0, 2324, 2326, 3, 170, 85, 0, 2325, 2324, 1, 0, 0, 0, 2325, 2326, 1, 0, 0, 0, 2326, 167, 1, 0, 0, 0, 2327, 2328, 3, 214, 107, 0, 2328, 169, 1, 0, 0, 0, 2329, 2331, 5, 20, 0, 0, 2330, 2329, 1, 0, 0, 0, 2330, 2331, 1, 0, 0, 0, 2331, 2332, 1, 0, 0, 0, 2332, 2333, 3, 326, 163, 0, 2333, 171, 1, 0, 0, 0, 2334, 2335, 5, 138, 0, 0, 2335, 2337, 5, 296, 0, 0, 2336, 2338, 5, 184, 0, 0, 2337, 2336, 1, 0, 0, 0, 2337, 2338, 1, 0, 0, 0, 2338, 2339, 1, 0, 0, 0, 2339, 2340, 3, 320, 160, 0, 2340, 2349, 5, 2, 0, 0, 2341, 2346, 3, 236, 118, 0, 2342, 2343, 5, 4, 0, 0, 2343, 2345, 3, 236, 118, 0, 2344, 2342, 1, 0, 0, 0, 2345, 2348, 1, 0, 0, 0, 2346, 2344, 1, 0, 0, 0, 2346, 2347, 1, 0, 0, 0, 2347, 2350, 1, 0, 0, 0, 2348, 2346, 1, 0, 0, 0, 2349, 2341, 1, 0, 0, 0, 2349, 2350, 1, 0, 0, 0, 2350, 2351, 1, 0, 0, 0, 2351, 2352, 5, 3, 0, 0, 2352, 2364, 3, 326, 163, 0, 2353, 2355, 5, 20, 0, 0, 2354, 2353, 1, 0, 0, 0, 2354, 2355, 1, 0, 0, 0, 2355, 2356, 1, 0, 0, 0, 2356, 2361, 3, 326, 163, 0, 2357, 2358, 5, 4, 0, 0, 2358, 2360, 3, 326, 163, 0, 2359, 2357, 1, 0, 0, 0, 2360, 2363, 1, 0, 0, 0, 2361, 2359, 1, 0, 0, 0, 2361, 2362, 1, 0, 0, 0, 2362, 2365, 1, 0, 0, 0, 2363, 2361, 1, 0, 0, 0, 2364, 2354, 1, 0, 0, 0, 2364, 2365, 1, 0, 0, 0, 2365, 173, 1, 0, 0, 0, 2366, 2367, 7, 20, 0, 0, 2367, 175, 1, 0, 0, 0, 2368, 2370, 5, 138, 0, 0, 2369, 2368, 1, 0, 0, 0, 2369, 2370, 1, 0, 0, 0, 2370, 2371, 1, 0, 0, 0, 2371, 2375, 3, 202, 101, 0, 2372, 2374, 3, 178, 89, 0, 2373, 2372, 1, 0, 0, 0, 2374, 2377, 1, 0, 0, 0, 2375, 2373, 1, 0, 0, 0, 2375, 2376, 1, 0, 0, 0, 2376, 177, 1, 0, 0, 0, 2377, 2375, 1, 0, 0, 0, 2378, 2382, 3, 180, 90, 0, 2379, 2382, 3, 144, 72, 0, 2380, 2382, 3, 150, 75, 0, 2381, 2378, 1, 0, 0, 0, 2381, 2379, 1, 0, 0, 0, 2381, 2380, 1, 0, 0, 0, 2382, 179, 1, 0, 0, 0, 2383, 2384, 3, 182, 91, 0, 2384, 2386, 5, 135, 0, 0, 2385, 2387, 5, 138, 0, 0, 2386, 2385, 1, 0, 0, 0, 2386, 2387, 1, 0, 0, 0, 2387, 2388, 1, 0, 0, 0, 2388, 2390, 3, 202, 101, 0, 2389, 2391, 3, 184, 92, 0, 2390, 2389, 1, 0, 0, 0, 2390, 2391, 1, 0, 0, 0, 2391, 2401, 1, 0, 0, 0, 2392, 2393, 5, 170, 0, 0, 2393, 2394, 3, 182, 91, 0, 2394, 2396, 5, 135, 0, 0, 2395, 2397, 5, 138, 0, 0, 2396, 2395, 1, 0, 0, 0, 2396, 2397, 1, 0, 0, 0, 2397, 2398, 1, 0, 0, 0, 2398, 2399, 3, 202, 101, 0, 2399, 2401, 1, 0, 0, 0, 2400, 2383, 1, 0, 0, 0, 2400, 2392, 1, 0, 0, 0, 2401, 181, 1, 0, 0, 0, 2402, 2404, 5, 126, 0, 0, 2403, 2402, 1, 0, 0, 0, 2403, 2404, 1, 0, 0, 0, 2404, 2427, 1, 0, 0, 0, 2405, 2427, 5, 54, 0, 0, 2406, 2408, 5, 141, 0, 0, 2407, 2409, 5, 184, 0, 0, 2408, 2407, 1, 0, 0, 0, 2408, 2409, 1, 0, 0, 0, 2409, 2427, 1, 0, 0, 0, 2410, 2412, 5, 141, 0, 0, 2411, 2410, 1, 0, 0, 0, 2411, 2412, 1, 0, 0, 0, 2412, 2413, 1, 0, 0, 0, 2413, 2427, 5, 234, 0, 0, 2414, 2416, 5, 221, 0, 0, 2415, 2417, 5, 184, 0, 0, 2416, 2415, 1, 0, 0, 0, 2416, 2417, 1, 0, 0, 0, 2417, 2427, 1, 0, 0, 0, 2418, 2420, 5, 108, 0, 0, 2419, 2421, 5, 184, 0, 0, 2420, 2419, 1, 0, 0, 0, 2420, 2421, 1, 0, 0, 0, 2421, 2427, 1, 0, 0, 0, 2422, 2424, 5, 141, 0, 0, 2423, 2422, 1, 0, 0, 0, 2423, 2424, 1, 0, 0, 0, 2424, 2425, 1, 0, 0, 0, 2425, 2427, 5, 15, 0, 0, 2426, 2403, 1, 0, 0, 0, 2426, 2405, 1, 0, 0, 0, 2426, 2406, 1, 0, 0, 0, 2426, 2411, 1, 0, 0, 0, 2426, 2414, 1, 0, 0, 0, 2426, 2418, 1, 0, 0, 0, 2426, 2423, 1, 0, 0, 0, 2427, 183, 1, 0, 0, 0, 2428, 2429, 5, 177, 0, 0, 2429, 2433, 3, 240, 120, 0, 2430, 2431, 5, 293, 0, 0, 2431, 2433, 3, 190, 95, 0, 2432, 2428, 1, 0, 0, 0, 2432, 2430, 1, 0, 0, 0, 2433, 185, 1, 0, 0, 0, 2434, 2435, 5, 260, 0, 0, 2435, 2437, 5, 2, 0, 0, 2436, 2438, 3, 188, 94, 0, 2437, 2436, 1, 0, 0, 0, 2437, 2438, 1, 0, 0, 0, 2438, 2439, 1, 0, 0, 0, 2439, 2444, 5, 3, 0, 0, 2440, 2441, 5, 215, 0, 0, 2441, 2442, 5, 2, 0, 0, 2442, 2443, 5, 335, 0, 0, 2443, 2445, 5, 3, 0, 0, 2444, 2440, 1, 0, 0, 0, 2444, 2445, 1, 0, 0, 0, 2445, 187, 1, 0, 0, 0, 2446, 2448, 5, 317, 0, 0, 2447, 2446, 1, 0, 0, 0, 2447, 2448, 1, 0, 0, 0, 2448, 2449, 1, 0, 0, 0, 2449, 2450, 7, 21, 0, 0, 2450, 2471, 5, 195, 0, 0, 2451, 2452, 3, 236, 118, 0, 2452, 2453, 5, 228, 0, 0, 2453, 2471, 1, 0, 0, 0, 2454, 2455, 5, 26, 0, 0, 2455, 2456, 5, 335, 0, 0, 2456, 2457, 5, 183, 0, 0, 2457, 2458, 5, 175, 0, 0, 2458, 2467, 5, 335, 0, 0, 2459, 2465, 5, 177, 0, 0, 2460, 2466, 3, 326, 163, 0, 2461, 2462, 3, 320, 160, 0, 2462, 2463, 5, 2, 0, 0, 2463, 2464, 5, 3, 0, 0, 2464, 2466, 1, 0, 0, 0, 2465, 2460, 1, 0, 0, 0, 2465, 2461, 1, 0, 0, 0, 2466, 2468, 1, 0, 0, 0, 2467, 2459, 1, 0, 0, 0, 2467, 2468, 1, 0, 0, 0, 2468, 2471, 1, 0, 0, 0, 2469, 2471, 3, 236, 118, 0, 2470, 2447, 1, 0, 0, 0, 2470, 2451, 1, 0, 0, 0, 2470, 2454, 1, 0, 0, 0, 2470, 2469, 1, 0, 0, 0, 2471, 189, 1, 0, 0, 0, 2472, 2473, 5, 2, 0, 0, 2473, 2474, 3, 192, 96, 0, 2474, 2475, 5, 3, 0, 0, 2475, 191, 1, 0, 0, 0, 2476, 2481, 3, 322, 161, 0, 2477, 2478, 5, 4, 0, 0, 2478, 2480, 3, 322, 161, 0, 2479, 2477, 1, 0, 0, 0, 2480, 2483, 1, 0, 0, 0, 2481, 2479, 1, 0, 0, 0, 2481, 2482, 1, 0, 0, 0, 2482, 193, 1, 0, 0, 0, 2483, 2481, 1, 0, 0, 0, 2484, 2485, 5, 2, 0, 0, 2485, 2490, 3, 196, 98, 0, 2486, 2487, 5, 4, 0, 0, 2487, 2489, 3, 196, 98, 0, 2488, 2486, 1, 0, 0, 0, 2489, 2492, 1, 0, 0, 0, 2490, 2488, 1, 0, 0, 0, 2490, 2491, 1, 0, 0, 0, 2491, 2493, 1, 0, 0, 0, 2492, 2490, 1, 0, 0, 0, 2493, 2494, 5, 3, 0, 0, 2494, 195, 1, 0, 0, 0, 2495, 2497, 3, 322, 161, 0, 2496, 2498, 7, 14, 0, 0, 2497, 2496, 1, 0, 0, 0, 2497, 2498, 1, 0, 0, 0, 2498, 197, 1, 0, 0, 0, 2499, 2500, 5, 2, 0, 0, 2500, 2505, 3, 200, 100, 0, 2501, 2502, 5, 4, 0, 0, 2502, 2504, 3, 200, 100, 0, 2503, 2501, 1, 0, 0, 0, 2504, 2507, 1, 0, 0, 0, 2505, 2503, 1, 0, 0, 0, 2505, 2506, 1, 0, 0, 0, 2506, 2508, 1, 0, 0, 0, 2507, 2505, 1, 0, 0, 0, 2508, 2509, 5, 3, 0, 0, 2509, 199, 1, 0, 0, 0, 2510, 2512, 3, 326, 163, 0, 2511, 2513, 3, 34, 17, 0, 2512, 2511, 1, 0, 0, 0, 2512, 2513, 1, 0, 0, 0, 2513, 201, 1, 0, 0, 0, 2514, 2516, 3, 214, 107, 0, 2515, 2517, 3, 132, 66, 0, 2516, 2515, 1, 0, 0, 0, 2516, 2517, 1, 0, 0, 0, 2517, 2519, 1, 0, 0, 0, 2518, 2520, 3, 186, 93, 0, 2519, 2518, 1, 0, 0, 0, 2519, 2520, 1, 0, 0, 0, 2520, 2521, 1, 0, 0, 0, 2521, 2522, 3, 208, 104, 0, 2522, 2542, 1, 0, 0, 0, 2523, 2524, 5, 2, 0, 0, 2524, 2525, 3, 54, 27, 0, 2525, 2527, 5, 3, 0, 0, 2526, 2528, 3, 186, 93, 0, 2527, 2526, 1, 0, 0, 0, 2527, 2528, 1, 0, 0, 0, 2528, 2529, 1, 0, 0, 0, 2529, 2530, 3, 208, 104, 0, 2530, 2542, 1, 0, 0, 0, 2531, 2532, 5, 2, 0, 0, 2532, 2533, 3, 176, 88, 0, 2533, 2535, 5, 3, 0, 0, 2534, 2536, 3, 186, 93, 0, 2535, 2534, 1, 0, 0, 0, 2535, 2536, 1, 0, 0, 0, 2536, 2537, 1, 0, 0, 0, 2537, 2538, 3, 208, 104, 0, 2538, 2542, 1, 0, 0, 0, 2539, 2542, 3, 204, 102, 0, 2540, 2542, 3, 206, 103, 0, 2541, 2514, 1, 0, 0, 0, 2541, 2523, 1, 0, 0, 0, 2541, 2531, 1, 0, 0, 0, 2541, 2539, 1, 0, 0, 0, 2541, 2540, 1, 0, 0, 0, 2542, 203, 1, 0, 0, 0, 2543, 2544, 5, 294, 0, 0, 2544, 2549, 3, 236, 118, 0, 2545, 2546, 5, 4, 0, 0, 2546, 2548, 3, 236, 118, 0, 2547, 2545, 1, 0, 0, 0, 2548, 2551, 1, 0, 0, 0, 2549, 2547, 1, 0, 0, 0, 2549, 2550, 1, 0, 0, 0, 2550, 2552, 1, 0, 0, 0, 2551, 2549, 1, 0, 0, 0, 2552, 2553, 3, 208, 104, 0, 2553, 205, 1, 0, 0, 0, 2554, 2555, 3, 318, 159, 0, 2555, 2564, 5, 2, 0, 0, 2556, 2561, 3, 236, 118, 0, 2557, 2558, 5, 4, 0, 0, 2558, 2560, 3, 236, 118, 0, 2559, 2557, 1, 0, 0, 0, 2560, 2563, 1, 0, 0, 0, 2561, 2559, 1, 0, 0, 0, 2561, 2562, 1, 0, 0, 0, 2562, 2565, 1, 0, 0, 0, 2563, 2561, 1, 0, 0, 0, 2564, 2556, 1, 0, 0, 0, 2564, 2565, 1, 0, 0, 0, 2565, 2566, 1, 0, 0, 0, 2566, 2567, 5, 3, 0, 0, 2567, 2568, 3, 208, 104, 0, 2568, 207, 1, 0, 0, 0, 2569, 2571, 5, 20, 0, 0, 2570, 2569, 1, 0, 0, 0, 2570, 2571, 1, 0, 0, 0, 2571, 2572, 1, 0, 0, 0, 2572, 2574, 3, 328, 164, 0, 2573, 2575, 3, 190, 95, 0, 2574, 2573, 1, 0, 0, 0, 2574, 2575, 1, 0, 0, 0, 2575, 2577, 1, 0, 0, 0, 2576, 2570, 1, 0, 0, 0, 2576, 2577, 1, 0, 0, 0, 2577, 209, 1, 0, 0, 0, 2578, 2579, 5, 227, 0, 0, 2579, 2580, 5, 105, 0, 0, 2580, 2581, 5, 236, 0, 0, 2581, 2585, 3, 338, 169, 0, 2582, 2583, 5, 303, 0, 0, 2583, 2584, 5, 237, 0, 0, 2584, 2586, 3, 68, 34, 0, 2585, 2582, 1, 0, 0, 0, 2585, 2586, 1, 0, 0, 0, 2586, 2628, 1, 0, 0, 0, 2587, 2588, 5, 227, 0, 0, 2588, 2589, 5, 105, 0, 0, 2589, 2599, 5, 73, 0, 0, 2590, 2591, 5, 98, 0, 0, 2591, 2592, 5, 264, 0, 0, 2592, 2593, 5, 28, 0, 0, 2593, 2597, 3, 338, 169, 0, 2594, 2595, 5, 86, 0, 0, 2595, 2596, 5, 28, 0, 0, 2596, 2598, 3, 338, 169, 0, 2597, 2594, 1, 0, 0, 0, 2597, 2598, 1, 0, 0, 0, 2598, 2600, 1, 0, 0, 0, 2599, 2590, 1, 0, 0, 0, 2599, 2600, 1, 0, 0, 0, 2600, 2606, 1, 0, 0, 0, 2601, 2602, 5, 42, 0, 0, 2602, 2603, 5, 134, 0, 0, 2603, 2604, 5, 264, 0, 0, 2604, 2605, 5, 28, 0, 0, 2605, 2607, 3, 338, 169, 0, 2606, 2601, 1, 0, 0, 0, 2606, 2607, 1, 0, 0, 0, 2607, 2613, 1, 0, 0, 0, 2608, 2609, 5, 154, 0, 0, 2609, 2610, 5, 136, 0, 0, 2610, 2611, 5, 264, 0, 0, 2611, 2612, 5, 28, 0, 0, 2612, 2614, 3, 338, 169, 0, 2613, 2608, 1, 0, 0, 0, 2613, 2614, 1, 0, 0, 0, 2614, 2619, 1, 0, 0, 0, 2615, 2616, 5, 145, 0, 0, 2616, 2617, 5, 264, 0, 0, 2617, 2618, 5, 28, 0, 0, 2618, 2620, 3, 338, 169, 0, 2619, 2615, 1, 0, 0, 0, 2619, 2620, 1, 0, 0, 0, 2620, 2625, 1, 0, 0, 0, 2621, 2622, 5, 173, 0, 0, 2622, 2623, 5, 71, 0, 0, 2623, 2624, 5, 20, 0, 0, 2624, 2626, 3, 338, 169, 0, 2625, 2621, 1, 0, 0, 0, 2625, 2626, 1, 0, 0, 0, 2626, 2628, 1, 0, 0, 0, 2627, 2578, 1, 0, 0, 0, 2627, 2587, 1, 0, 0, 0, 2628, 211, 1, 0, 0, 0, 2629, 2634, 3, 214, 107, 0, 2630, 2631, 5, 4, 0, 0, 2631, 2633, 3, 214, 107, 0, 2632, 2630, 1, 0, 0, 0, 2633, 2636, 1, 0, 0, 0, 2634, 2632, 1, 0, 0, 0, 2634, 2635, 1, 0, 0, 0, 2635, 213, 1, 0, 0, 0, 2636, 2634, 1, 0, 0, 0, 2637, 2642, 3, 322, 161, 0, 2638, 2639, 5, 5, 0, 0, 2639, 2641, 3, 322, 161, 0, 2640, 2638, 1, 0, 0, 0, 2641, 2644, 1, 0, 0, 0, 2642, 2640, 1, 0, 0, 0, 2642, 2643, 1, 0, 0, 0, 2643, 215, 1, 0, 0, 0, 2644, 2642, 1, 0, 0, 0, 2645, 2650, 3, 218, 109, 0, 2646, 2647, 5, 4, 0, 0, 2647, 2649, 3, 218, 109, 0, 2648, 2646, 1, 0, 0, 0, 2649, 2652, 1, 0, 0, 0, 2650, 2648, 1, 0, 0, 0, 2650, 2651, 1, 0, 0, 0, 2651, 217, 1, 0, 0, 0, 2652, 2650, 1, 0, 0, 0, 2653, 2656, 3, 214, 107, 0, 2654, 2655, 5, 180, 0, 0, 2655, 2657, 3, 68, 34, 0, 2656, 2654, 1, 0, 0, 0, 2656, 2657, 1, 0, 0, 0, 2657, 219, 1, 0, 0, 0, 2658, 2659, 3, 322, 161, 0, 2659, 2660, 5, 5, 0, 0, 2660, 2662, 1, 0, 0, 0, 2661, 2658, 1, 0, 0, 0, 2661, 2662, 1, 0, 0, 0, 2662, 2663, 1, 0, 0, 0, 2663, 2664, 3, 322, 161, 0, 2664, 221, 1, 0, 0, 0, 2665, 2666, 3, 322, 161, 0, 2666, 2667, 5, 5, 0, 0, 2667, 2669, 1, 0, 0, 0, 2668, 2665, 1, 0, 0, 0, 2668, 2669, 1, 0, 0, 0, 2669, 2670, 1, 0, 0, 0, 2670, 2671, 3, 322, 161, 0, 2671, 223, 1, 0, 0, 0, 2672, 2680, 3, 236, 118, 0, 2673, 2675, 5, 20, 0, 0, 2674, 2673, 1, 0, 0, 0, 2674, 2675, 1, 0, 0, 0, 2675, 2678, 1, 0, 0, 0, 2676, 2679, 3, 322, 161, 0, 2677, 2679, 3, 190, 95, 0, 2678, 2676, 1, 0, 0, 0, 2678, 2677, 1, 0, 0, 0, 2679, 2681, 1, 0, 0, 0, 2680, 2674, 1, 0, 0, 0, 2680, 2681, 1, 0, 0, 0, 2681, 225, 1, 0, 0, 0, 2682, 2687, 3, 224, 112, 0, 2683, 2684, 5, 4, 0, 0, 2684, 2686, 3, 224, 112, 0, 2685, 2683, 1, 0, 0, 0, 2686, 2689, 1, 0, 0, 0, 2687, 2685, 1, 0, 0, 0, 2687, 2688, 1, 0, 0, 0, 2688, 227, 1, 0, 0, 0, 2689, 2687, 1, 0, 0, 0, 2690, 2691, 5, 2, 0, 0, 2691, 2696, 3, 230, 115, 0, 2692, 2693, 5, 4, 0, 0, 2693, 2695, 3, 230, 115, 0, 2694, 2692, 1, 0, 0, 0, 2695, 2698, 1, 0, 0, 0, 2696, 2694, 1, 0, 0, 0, 2696, 2697, 1, 0, 0, 0, 2697, 2699, 1, 0, 0, 0, 2698, 2696, 1, 0, 0, 0, 2699, 2700, 5, 3, 0, 0, 2700, 229, 1, 0, 0, 0, 2701, 2704, 3, 232, 116, 0, 2702, 2704, 3, 290, 145, 0, 2703, 2701, 1, 0, 0, 0, 2703, 2702, 1, 0, 0, 0, 2704, 231, 1, 0, 0, 0, 2705, 2719, 3, 320, 160, 0, 2706, 2707, 3, 326, 163, 0, 2707, 2708, 5, 2, 0, 0, 2708, 2713, 3, 234, 117, 0, 2709, 2710, 5, 4, 0, 0, 2710, 2712, 3, 234, 117, 0, 2711, 2709, 1, 0, 0, 0, 2712, 2715, 1, 0, 0, 0, 2713, 2711, 1, 0, 0, 0, 2713, 2714, 1, 0, 0, 0, 2714, 2716, 1, 0, 0, 0, 2715, 2713, 1, 0, 0, 0, 2716, 2717, 5, 3, 0, 0, 2717, 2719, 1, 0, 0, 0, 2718, 2705, 1, 0, 0, 0, 2718, 2706, 1, 0, 0, 0, 2719, 233, 1, 0, 0, 0, 2720, 2723, 3, 320, 160, 0, 2721, 2723, 3, 250, 125, 0, 2722, 2720, 1, 0, 0, 0, 2722, 2721, 1, 0, 0, 0, 2723, 235, 1, 0, 0, 0, 2724, 2725, 3, 240, 120, 0, 2725, 237, 1, 0, 0, 0, 2726, 2731, 3, 236, 118, 0, 2727, 2728, 5, 4, 0, 0, 2728, 2730, 3, 236, 118, 0, 2729, 2727, 1, 0, 0, 0, 2730, 2733, 1, 0, 0, 0, 2731, 2729, 1, 0, 0, 0, 2731, 2732, 1, 0, 0, 0, 2732, 239, 1, 0, 0, 0, 2733, 2731, 1, 0, 0, 0, 2734, 2735, 6, 120, -1, 0, 2735, 2736, 5, 172, 0, 0, 2736, 2747, 3, 240, 120, 5, 2737, 2738, 5, 90, 0, 0, 2738, 2739, 5, 2, 0, 0, 2739, 2740, 3, 54, 27, 0, 2740, 2741, 5, 3, 0, 0, 2741, 2747, 1, 0, 0, 0, 2742, 2744, 3, 244, 122, 0, 2743, 2745, 3, 242, 121, 0, 2744, 2743, 1, 0, 0, 0, 2744, 2745, 1, 0, 0, 0, 2745, 2747, 1, 0, 0, 0, 2746, 2734, 1, 0, 0, 0, 2746, 2737, 1, 0, 0, 0, 2746, 2742, 1, 0, 0, 0, 2747, 2756, 1, 0, 0, 0, 2748, 2749, 10, 2, 0, 0, 2749, 2750, 5, 14, 0, 0, 2750, 2755, 3, 240, 120, 3, 2751, 2752, 10, 1, 0, 0, 2752, 2753, 5, 181, 0, 0, 2753, 2755, 3, 240, 120, 2, 2754, 2748, 1, 0, 0, 0, 2754, 2751, 1, 0, 0, 0, 2755, 2758, 1, 0, 0, 0, 2756, 2754, 1, 0, 0, 0, 2756, 2757, 1, 0, 0, 0, 2757, 241, 1, 0, 0, 0, 2758, 2756, 1, 0, 0, 0, 2759, 2761, 5, 172, 0, 0, 2760, 2759, 1, 0, 0, 0, 2760, 2761, 1, 0, 0, 0, 2761, 2762, 1, 0, 0, 0, 2762, 2763, 5, 24, 0, 0, 2763, 2764, 3, 244, 122, 0, 2764, 2765, 5, 14, 0, 0, 2765, 2766, 3, 244, 122, 0, 2766, 2842, 1, 0, 0, 0, 2767, 2769, 5, 172, 0, 0, 2768, 2767, 1, 0, 0, 0, 2768, 2769, 1, 0, 0, 0, 2769, 2770, 1, 0, 0, 0, 2770, 2771, 5, 122, 0, 0, 2771, 2772, 5, 2, 0, 0, 2772, 2777, 3, 236, 118, 0, 2773, 2774, 5, 4, 0, 0, 2774, 2776, 3, 236, 118, 0, 2775, 2773, 1, 0, 0, 0, 2776, 2779, 1, 0, 0, 0, 2777, 2775, 1, 0, 0, 0, 2777, 2778, 1, 0, 0, 0, 2778, 2780, 1, 0, 0, 0, 2779, 2777, 1, 0, 0, 0, 2780, 2781, 5, 3, 0, 0, 2781, 2842, 1, 0, 0, 0, 2782, 2784, 5, 172, 0, 0, 2783, 2782, 1, 0, 0, 0, 2783, 2784, 1, 0, 0, 0, 2784, 2785, 1, 0, 0, 0, 2785, 2786, 5, 122, 0, 0, 2786, 2787, 5, 2, 0, 0, 2787, 2788, 3, 54, 27, 0, 2788, 2789, 5, 3, 0, 0, 2789, 2842, 1, 0, 0, 0, 2790, 2792, 5, 172, 0, 0, 2791, 2790, 1, 0, 0, 0, 2791, 2792, 1, 0, 0, 0, 2792, 2793, 1, 0, 0, 0, 2793, 2794, 5, 222, 0, 0, 2794, 2842, 3, 244, 122, 0, 2795, 2797, 5, 172, 0, 0, 2796, 2795, 1, 0, 0, 0, 2796, 2797, 1, 0, 0, 0, 2797, 2798, 1, 0, 0, 0, 2798, 2799, 7, 22, 0, 0, 2799, 2813, 7, 23, 0, 0, 2800, 2801, 5, 2, 0, 0, 2801, 2814, 5, 3, 0, 0, 2802, 2803, 5, 2, 0, 0, 2803, 2808, 3, 236, 118, 0, 2804, 2805, 5, 4, 0, 0, 2805, 2807, 3, 236, 118, 0, 2806, 2804, 1, 0, 0, 0, 2807, 2810, 1, 0, 0, 0, 2808, 2806, 1, 0, 0, 0, 2808, 2809, 1, 0, 0, 0, 2809, 2811, 1, 0, 0, 0, 2810, 2808, 1, 0, 0, 0, 2811, 2812, 5, 3, 0, 0, 2812, 2814, 1, 0, 0, 0, 2813, 2800, 1, 0, 0, 0, 2813, 2802, 1, 0, 0, 0, 2814, 2842, 1, 0, 0, 0, 2815, 2817, 5, 172, 0, 0, 2816, 2815, 1, 0, 0, 0, 2816, 2817, 1, 0, 0, 0, 2817, 2818, 1, 0, 0, 0, 2818, 2819, 7, 22, 0, 0, 2819, 2822, 3, 244, 122, 0, 2820, 2821, 5, 85, 0, 0, 2821, 2823, 3, 338, 169, 0, 2822, 2820, 1, 0, 0, 0, 2822, 2823, 1, 0, 0, 0, 2823, 2842, 1, 0, 0, 0, 2824, 2826, 5, 133, 0, 0, 2825, 2827, 5, 172, 0, 0, 2826, 2825, 1, 0, 0, 0, 2826, 2827, 1, 0, 0, 0, 2827, 2828, 1, 0, 0, 0, 2828, 2842, 5, 173, 0, 0, 2829, 2831, 5, 133, 0, 0, 2830, 2832, 5, 172, 0, 0, 2831, 2830, 1, 0, 0, 0, 2831, 2832, 1, 0, 0, 0, 2832, 2833, 1, 0, 0, 0, 2833, 2842, 7, 24, 0, 0, 2834, 2836, 5, 133, 0, 0, 2835, 2837, 5, 172, 0, 0, 2836, 2835, 1, 0, 0, 0, 2836, 2837, 1, 0, 0, 0, 2837, 2838, 1, 0, 0, 0, 2838, 2839, 5, 79, 0, 0, 2839, 2840, 5, 107, 0, 0, 2840, 2842, 3, 244, 122, 0, 2841, 2760, 1, 0, 0, 0, 2841, 2768, 1, 0, 0, 0, 2841, 2783, 1, 0, 0, 0, 2841, 2791, 1, 0, 0, 0, 2841, 2796, 1, 0, 0, 0, 2841, 2816, 1, 0, 0, 0, 2841, 2824, 1, 0, 0, 0, 2841, 2829, 1, 0, 0, 0, 2841, 2834, 1, 0, 0, 0, 2842, 243, 1, 0, 0, 0, 2843, 2844, 6, 122, -1, 0, 2844, 2848, 3, 248, 124, 0, 2845, 2846, 7, 25, 0, 0, 2846, 2848, 3, 244, 122, 7, 2847, 2843, 1, 0, 0, 0, 2847, 2845, 1, 0, 0, 0, 2848, 2870, 1, 0, 0, 0, 2849, 2850, 10, 6, 0, 0, 2850, 2851, 7, 26, 0, 0, 2851, 2869, 3, 244, 122, 7, 2852, 2853, 10, 5, 0, 0, 2853, 2854, 7, 27, 0, 0, 2854, 2869, 3, 244, 122, 6, 2855, 2856, 10, 4, 0, 0, 2856, 2857, 5, 322, 0, 0, 2857, 2869, 3, 244, 122, 5, 2858, 2859, 10, 3, 0, 0, 2859, 2860, 5, 325, 0, 0, 2860, 2869, 3, 244, 122, 4, 2861, 2862, 10, 2, 0, 0, 2862, 2863, 5, 323, 0, 0, 2863, 2869, 3, 244, 122, 3, 2864, 2865, 10, 1, 0, 0, 2865, 2866, 3, 252, 126, 0, 2866, 2867, 3, 244, 122, 2, 2867, 2869, 1, 0, 0, 0, 2868, 2849, 1, 0, 0, 0, 2868, 2852, 1, 0, 0, 0, 2868, 2855, 1, 0, 0, 0, 2868, 2858, 1, 0, 0, 0, 2868, 2861, 1, 0, 0, 0, 2868, 2864, 1, 0, 0, 0, 2869, 2872, 1, 0, 0, 0, 2870, 2868, 1, 0, 0, 0, 2870, 2871, 1, 0, 0, 0, 2871, 245, 1, 0, 0, 0, 2872, 2870, 1, 0, 0, 0, 2873, 2874, 7, 28, 0, 0, 2874, 247, 1, 0, 0, 0, 2875, 2876, 6, 124, -1, 0, 2876, 3114, 7, 29, 0, 0, 2877, 2878, 7, 30, 0, 0, 2878, 2879, 5, 2, 0, 0, 2879, 2880, 3, 246, 123, 0, 2880, 2881, 5, 4, 0, 0, 2881, 2882, 3, 244, 122, 0, 2882, 2883, 5, 4, 0, 0, 2883, 2884, 3, 244, 122, 0, 2884, 2885, 5, 3, 0, 0, 2885, 3114, 1, 0, 0, 0, 2886, 2887, 7, 31, 0, 0, 2887, 2888, 5, 2, 0, 0, 2888, 2889, 3, 246, 123, 0, 2889, 2890, 5, 4, 0, 0, 2890, 2891, 3, 244, 122, 0, 2891, 2892, 5, 4, 0, 0, 2892, 2893, 3, 244, 122, 0, 2893, 2894, 5, 3, 0, 0, 2894, 3114, 1, 0, 0, 0, 2895, 2897, 5, 31, 0, 0, 2896, 2898, 3, 304, 152, 0, 2897, 2896, 1, 0, 0, 0, 2898, 2899, 1, 0, 0, 0, 2899, 2897, 1, 0, 0, 0, 2899, 2900, 1, 0, 0, 0, 2900, 2903, 1, 0, 0, 0, 2901, 2902, 5, 83, 0, 0, 2902, 2904, 3, 236, 118, 0, 2903, 2901, 1, 0, 0, 0, 2903, 2904, 1, 0, 0, 0, 2904, 2905, 1, 0, 0, 0, 2905, 2906, 5, 84, 0, 0, 2906, 3114, 1, 0, 0, 0, 2907, 2908, 5, 31, 0, 0, 2908, 2910, 3, 236, 118, 0, 2909, 2911, 3, 304, 152, 0, 2910, 2909, 1, 0, 0, 0, 2911, 2912, 1, 0, 0, 0, 2912, 2910, 1, 0, 0, 0, 2912, 2913, 1, 0, 0, 0, 2913, 2916, 1, 0, 0, 0, 2914, 2915, 5, 83, 0, 0, 2915, 2917, 3, 236, 118, 0, 2916, 2914, 1, 0, 0, 0, 2916, 2917, 1, 0, 0, 0, 2917, 2918, 1, 0, 0, 0, 2918, 2919, 5, 84, 0, 0, 2919, 3114, 1, 0, 0, 0, 2920, 2921, 7, 32, 0, 0, 2921, 2922, 5, 2, 0, 0, 2922, 2923, 3, 236, 118, 0, 2923, 2924, 5, 20, 0, 0, 2924, 2925, 3, 278, 139, 0, 2925, 2926, 5, 3, 0, 0, 2926, 3114, 1, 0, 0, 0, 2927, 2928, 5, 252, 0, 0, 2928, 2937, 5, 2, 0, 0, 2929, 2934, 3, 224, 112, 0, 2930, 2931, 5, 4, 0, 0, 2931, 2933, 3, 224, 112, 0, 2932, 2930, 1, 0, 0, 0, 2933, 2936, 1, 0, 0, 0, 2934, 2932, 1, 0, 0, 0, 2934, 2935, 1, 0, 0, 0, 2935, 2938, 1, 0, 0, 0, 2936, 2934, 1, 0, 0, 0, 2937, 2929, 1, 0, 0, 0, 2937, 2938, 1, 0, 0, 0, 2938, 2939, 1, 0, 0, 0, 2939, 3114, 5, 3, 0, 0, 2940, 2941, 5, 101, 0, 0, 2941, 2942, 5, 2, 0, 0, 2942, 2945, 3, 236, 118, 0, 2943, 2944, 5, 120, 0, 0, 2944, 2946, 5, 174, 0, 0, 2945, 2943, 1, 0, 0, 0, 2945, 2946, 1, 0, 0, 0, 2946, 2947, 1, 0, 0, 0, 2947, 2948, 5, 3, 0, 0, 2948, 3114, 1, 0, 0, 0, 2949, 2950, 5, 17, 0, 0, 2950, 2951, 5, 2, 0, 0, 2951, 2954, 3, 236, 118, 0, 2952, 2953, 5, 120, 0, 0, 2953, 2955, 5, 174, 0, 0, 2954, 2952, 1, 0, 0, 0, 2954, 2955, 1, 0, 0, 0, 2955, 2956, 1, 0, 0, 0, 2956, 2957, 5, 3, 0, 0, 2957, 3114, 1, 0, 0, 0, 2958, 2959, 5, 137, 0, 0, 2959, 2960, 5, 2, 0, 0, 2960, 2963, 3, 236, 118, 0, 2961, 2962, 5, 120, 0, 0, 2962, 2964, 5, 174, 0, 0, 2963, 2961, 1, 0, 0, 0, 2963, 2964, 1, 0, 0, 0, 2964, 2965, 1, 0, 0, 0, 2965, 2966, 5, 3, 0, 0, 2966, 3114, 1, 0, 0, 0, 2967, 2968, 5, 198, 0, 0, 2968, 2969, 5, 2, 0, 0, 2969, 2970, 3, 244, 122, 0, 2970, 2971, 5, 122, 0, 0, 2971, 2972, 3, 244, 122, 0, 2972, 2973, 5, 3, 0, 0, 2973, 3114, 1, 0, 0, 0, 2974, 3114, 3, 250, 125, 0, 2975, 3114, 5, 318, 0, 0, 2976, 2977, 3, 320, 160, 0, 2977, 2978, 5, 5, 0, 0, 2978, 2979, 5, 318, 0, 0, 2979, 3114, 1, 0, 0, 0, 2980, 2981, 5, 2, 0, 0, 2981, 2984, 3, 224, 112, 0, 2982, 2983, 5, 4, 0, 0, 2983, 2985, 3, 224, 112, 0, 2984, 2982, 1, 0, 0, 0, 2985, 2986, 1, 0, 0, 0, 2986, 2984, 1, 0, 0, 0, 2986, 2987, 1, 0, 0, 0, 2987, 2988, 1, 0, 0, 0, 2988, 2989, 5, 3, 0, 0, 2989, 3114, 1, 0, 0, 0, 2990, 2991, 5, 2, 0, 0, 2991, 2992, 3, 54, 27, 0, 2992, 2993, 5, 3, 0, 0, 2993, 3114, 1, 0, 0, 0, 2994, 2995, 3, 318, 159, 0, 2995, 3007, 5, 2, 0, 0, 2996, 2998, 3, 174, 87, 0, 2997, 2996, 1, 0, 0, 0, 2997, 2998, 1, 0, 0, 0, 2998, 2999, 1, 0, 0, 0, 2999, 3004, 3, 236, 118, 0, 3000, 3001, 5, 4, 0, 0, 3001, 3003, 3, 236, 118, 0, 3002, 3000, 1, 0, 0, 0, 3003, 3006, 1, 0, 0, 0, 3004, 3002, 1, 0, 0, 0, 3004, 3005, 1, 0, 0, 0, 3005, 3008, 1, 0, 0, 0, 3006, 3004, 1, 0, 0, 0, 3007, 2997, 1, 0, 0, 0, 3007, 3008, 1, 0, 0, 0, 3008, 3009, 1, 0, 0, 0, 3009, 3016, 5, 3, 0, 0, 3010, 3011, 5, 99, 0, 0, 3011, 3012, 5, 2, 0, 0, 3012, 3013, 5, 301, 0, 0, 3013, 3014, 3, 240, 120, 0, 3014, 3015, 5, 3, 0, 0, 3015, 3017, 1, 0, 0, 0, 3016, 3010, 1, 0, 0, 0, 3016, 3017, 1, 0, 0, 0, 3017, 3020, 1, 0, 0, 0, 3018, 3019, 7, 33, 0, 0, 3019, 3021, 5, 174, 0, 0, 3020, 3018, 1, 0, 0, 0, 3020, 3021, 1, 0, 0, 0, 3021, 3024, 1, 0, 0, 0, 3022, 3023, 5, 186, 0, 0, 3023, 3025, 3, 310, 155, 0, 3024, 3022, 1, 0, 0, 0, 3024, 3025, 1, 0, 0, 0, 3025, 3114, 1, 0, 0, 0, 3026, 3027, 3, 326, 163, 0, 3027, 3028, 5, 327, 0, 0, 3028, 3029, 3, 236, 118, 0, 3029, 3114, 1, 0, 0, 0, 3030, 3031, 5, 2, 0, 0, 3031, 3034, 3, 326, 163, 0, 3032, 3033, 5, 4, 0, 0, 3033, 3035, 3, 326, 163, 0, 3034, 3032, 1, 0, 0, 0, 3035, 3036, 1, 0, 0, 0, 3036, 3034, 1, 0, 0, 0, 3036, 3037, 1, 0, 0, 0, 3037, 3038, 1, 0, 0, 0, 3038, 3039, 5, 3, 0, 0, 3039, 3040, 5, 327, 0, 0, 3040, 3041, 3, 236, 118, 0, 3041, 3114, 1, 0, 0, 0, 3042, 3114, 3, 326, 163, 0, 3043, 3044, 5, 2, 0, 0, 3044, 3045, 3, 236, 118, 0, 3045, 3046, 5, 3, 0, 0, 3046, 3114, 1, 0, 0, 0, 3047, 3048, 5, 95, 0, 0, 3048, 3049, 5, 2, 0, 0, 3049, 3050, 3, 326, 163, 0, 3050, 3051, 5, 107, 0, 0, 3051, 3052, 3, 244, 122, 0, 3052, 3053, 5, 3, 0, 0, 3053, 3114, 1, 0, 0, 0, 3054, 3055, 7, 34, 0, 0, 3055, 3056, 5, 2, 0, 0, 3056, 3057, 3, 244, 122, 0, 3057, 3058, 7, 35, 0, 0, 3058, 3061, 3, 244, 122, 0, 3059, 3060, 7, 36, 0, 0, 3060, 3062, 3, 244, 122, 0, 3061, 3059, 1, 0, 0, 0, 3061, 3062, 1, 0, 0, 0, 3062, 3063, 1, 0, 0, 0, 3063, 3064, 5, 3, 0, 0, 3064, 3114, 1, 0, 0, 0, 3065, 3066, 5, 276, 0, 0, 3066, 3068, 5, 2, 0, 0, 3067, 3069, 7, 37, 0, 0, 3068, 3067, 1, 0, 0, 0, 3068, 3069, 1, 0, 0, 0, 3069, 3071, 1, 0, 0, 0, 3070, 3072, 3, 244, 122, 0, 3071, 3070, 1, 0, 0, 0, 3071, 3072, 1, 0, 0, 0, 3072, 3073, 1, 0, 0, 0, 3073, 3074, 5, 107, 0, 0, 3074, 3075, 3, 244, 122, 0, 3075, 3076, 5, 3, 0, 0, 3076, 3114, 1, 0, 0, 0, 3077, 3078, 5, 188, 0, 0, 3078, 3079, 5, 2, 0, 0, 3079, 3080, 3, 244, 122, 0, 3080, 3081, 5, 197, 0, 0, 3081, 3082, 3, 244, 122, 0, 3082, 3083, 5, 107, 0, 0, 3083, 3086, 3, 244, 122, 0, 3084, 3085, 5, 103, 0, 0, 3085, 3087, 3, 244, 122, 0, 3086, 3084, 1, 0, 0, 0, 3086, 3087, 1, 0, 0, 0, 3087, 3088, 1, 0, 0, 0, 3088, 3089, 5, 3, 0, 0, 3089, 3114, 1, 0, 0, 0, 3090, 3091, 7, 38, 0, 0, 3091, 3092, 5, 2, 0, 0, 3092, 3093, 3, 244, 122, 0, 3093, 3094, 5, 3, 0, 0, 3094, 3095, 5, 304, 0, 0, 3095, 3096, 5, 114, 0, 0, 3096, 3097, 5, 2, 0, 0, 3097, 3098, 5, 182, 0, 0, 3098, 3099, 5, 28, 0, 0, 3099, 3100, 3, 94, 47, 0, 3100, 3107, 5, 3, 0, 0, 3101, 3102, 5, 99, 0, 0, 3102, 3103, 5, 2, 0, 0, 3103, 3104, 5, 301, 0, 0, 3104, 3105, 3, 240, 120, 0, 3105, 3106, 5, 3, 0, 0, 3106, 3108, 1, 0, 0, 0, 3107, 3101, 1, 0, 0, 0, 3107, 3108, 1, 0, 0, 0, 3108, 3111, 1, 0, 0, 0, 3109, 3110, 5, 186, 0, 0, 3110, 3112, 3, 310, 155, 0, 3111, 3109, 1, 0, 0, 0, 3111, 3112, 1, 0, 0, 0, 3112, 3114, 1, 0, 0, 0, 3113, 2875, 1, 0, 0, 0, 3113, 2877, 1, 0, 0, 0, 3113, 2886, 1, 0, 0, 0, 3113, 2895, 1, 0, 0, 0, 3113, 2907, 1, 0, 0, 0, 3113, 2920, 1, 0, 0, 0, 3113, 2927, 1, 0, 0, 0, 3113, 2940, 1, 0, 0, 0, 3113, 2949, 1, 0, 0, 0, 3113, 2958, 1, 0, 0, 0, 3113, 2967, 1, 0, 0, 0, 3113, 2974, 1, 0, 0, 0, 3113, 2975, 1, 0, 0, 0, 3113, 2976, 1, 0, 0, 0, 3113, 2980, 1, 0, 0, 0, 3113, 2990, 1, 0, 0, 0, 3113, 2994, 1, 0, 0, 0, 3113, 3026, 1, 0, 0, 0, 3113, 3030, 1, 0, 0, 0, 3113, 3042, 1, 0, 0, 0, 3113, 3043, 1, 0, 0, 0, 3113, 3047, 1, 0, 0, 0, 3113, 3054, 1, 0, 0, 0, 3113, 3065, 1, 0, 0, 0, 3113, 3077, 1, 0, 0, 0, 3113, 3090, 1, 0, 0, 0, 3114, 3125, 1, 0, 0, 0, 3115, 3116, 10, 9, 0, 0, 3116, 3117, 5, 6, 0, 0, 3117, 3118, 3, 244, 122, 0, 3118, 3119, 5, 7, 0, 0, 3119, 3124, 1, 0, 0, 0, 3120, 3121, 10, 7, 0, 0, 3121, 3122, 5, 5, 0, 0, 3122, 3124, 3, 326, 163, 0, 3123, 3115, 1, 0, 0, 0, 3123, 3120, 1, 0, 0, 0, 3124, 3127, 1, 0, 0, 0, 3125, 3123, 1, 0, 0, 0, 3125, 3126, 1, 0, 0, 0, 3126, 249, 1, 0, 0, 0, 3127, 3125, 1, 0, 0, 0, 3128, 3143, 5, 173, 0, 0, 3129, 3130, 5, 326, 0, 0, 3130, 3143, 3, 326, 163, 0, 3131, 3143, 3, 260, 130, 0, 3132, 3133, 3, 326, 163, 0, 3133, 3134, 3, 338, 169, 0, 3134, 3143, 1, 0, 0, 0, 3135, 3143, 3, 334, 167, 0, 3136, 3143, 3, 258, 129, 0, 3137, 3139, 3, 338, 169, 0, 3138, 3137, 1, 0, 0, 0, 3139, 3140, 1, 0, 0, 0, 3140, 3138, 1, 0, 0, 0, 3140, 3141, 1, 0, 0, 0, 3141, 3143, 1, 0, 0, 0, 3142, 3128, 1, 0, 0, 0, 3142, 3129, 1, 0, 0, 0, 3142, 3131, 1, 0, 0, 0, 3142, 3132, 1, 0, 0, 0, 3142, 3135, 1, 0, 0, 0, 3142, 3136, 1, 0, 0, 0, 3142, 3138, 1, 0, 0, 0, 3143, 251, 1, 0, 0, 0, 3144, 3145, 7, 39, 0, 0, 3145, 253, 1, 0, 0, 0, 3146, 3147, 7, 40, 0, 0, 3147, 255, 1, 0, 0, 0, 3148, 3149, 7, 41, 0, 0, 3149, 257, 1, 0, 0, 0, 3150, 3151, 7, 42, 0, 0, 3151, 259, 1, 0, 0, 0, 3152, 3155, 5, 131, 0, 0, 3153, 3156, 3, 262, 131, 0, 3154, 3156, 3, 266, 133, 0, 3155, 3153, 1, 0, 0, 0, 3155, 3154, 1, 0, 0, 0, 3156, 261, 1, 0, 0, 0, 3157, 3159, 3, 264, 132, 0, 3158, 3160, 3, 268, 134, 0, 3159, 3158, 1, 0, 0, 0, 3159, 3160, 1, 0, 0, 0, 3160, 263, 1, 0, 0, 0, 3161, 3162, 3, 270, 135, 0, 3162, 3163, 3, 272, 136, 0, 3163, 3165, 1, 0, 0, 0, 3164, 3161, 1, 0, 0, 0, 3165, 3166, 1, 0, 0, 0, 3166, 3164, 1, 0, 0, 0, 3166, 3167, 1, 0, 0, 0, 3167, 265, 1, 0, 0, 0, 3168, 3171, 3, 268, 134, 0, 3169, 3172, 3, 264, 132, 0, 3170, 3172, 3, 268, 134, 0, 3171, 3169, 1, 0, 0, 0, 3171, 3170, 1, 0, 0, 0, 3171, 3172, 1, 0, 0, 0, 3172, 267, 1, 0, 0, 0, 3173, 3174, 3, 270, 135, 0, 3174, 3175, 3, 274, 137, 0, 3175, 3176, 5, 270, 0, 0, 3176, 3177, 3, 274, 137, 0, 3177, 269, 1, 0, 0, 0, 3178, 3180, 7, 43, 0, 0, 3179, 3178, 1, 0, 0, 0, 3179, 3180, 1, 0, 0, 0, 3180, 3184, 1, 0, 0, 0, 3181, 3185, 5, 335, 0, 0, 3182, 3185, 5, 337, 0, 0, 3183, 3185, 3, 338, 169, 0, 3184, 3181, 1, 0, 0, 0, 3184, 3182, 1, 0, 0, 0, 3184, 3183, 1, 0, 0, 0, 3185, 271, 1, 0, 0, 0, 3186, 3187, 7, 44, 0, 0, 3187, 273, 1, 0, 0, 0, 3188, 3189, 7, 45, 0, 0, 3189, 275, 1, 0, 0, 0, 3190, 3194, 5, 101, 0, 0, 3191, 3192, 5, 9, 0, 0, 3192, 3194, 3, 322, 161, 0, 3193, 3190, 1, 0, 0, 0, 3193, 3191, 1, 0, 0, 0, 3194, 277, 1, 0, 0, 0, 3195, 3196, 5, 19, 0, 0, 3196, 3197, 5, 312, 0, 0, 3197, 3198, 3, 278, 139, 0, 3198, 3199, 5, 314, 0, 0, 3199, 3242, 1, 0, 0, 0, 3200, 3201, 5, 154, 0, 0, 3201, 3202, 5, 312, 0, 0, 3202, 3203, 3, 278, 139, 0, 3203, 3204, 5, 4, 0, 0, 3204, 3205, 3, 278, 139, 0, 3205, 3206, 5, 314, 0, 0, 3206, 3242, 1, 0, 0, 0, 3207, 3214, 5, 252, 0, 0, 3208, 3210, 5, 312, 0, 0, 3209, 3211, 3, 300, 150, 0, 3210, 3209, 1, 0, 0, 0, 3210, 3211, 1, 0, 0, 0, 3211, 3212, 1, 0, 0, 0, 3212, 3215, 5, 314, 0, 0, 3213, 3215, 5, 310, 0, 0, 3214, 3208, 1, 0, 0, 0, 3214, 3213, 1, 0, 0, 0, 3215, 3242, 1, 0, 0, 0, 3216, 3217, 5, 131, 0, 0, 3217, 3220, 7, 46, 0, 0, 3218, 3219, 5, 270, 0, 0, 3219, 3221, 5, 163, 0, 0, 3220, 3218, 1, 0, 0, 0, 3220, 3221, 1, 0, 0, 0, 3221, 3242, 1, 0, 0, 0, 3222, 3223, 5, 131, 0, 0, 3223, 3226, 7, 47, 0, 0, 3224, 3225, 5, 270, 0, 0, 3225, 3227, 7, 48, 0, 0, 3226, 3224, 1, 0, 0, 0, 3226, 3227, 1, 0, 0, 0, 3227, 3242, 1, 0, 0, 0, 3228, 3239, 3, 326, 163, 0, 3229, 3230, 5, 2, 0, 0, 3230, 3235, 5, 335, 0, 0, 3231, 3232, 5, 4, 0, 0, 3232, 3234, 5, 335, 0, 0, 3233, 3231, 1, 0, 0, 0, 3234, 3237, 1, 0, 0, 0, 3235, 3233, 1, 0, 0, 0, 3235, 3236, 1, 0, 0, 0, 3236, 3238, 1, 0, 0, 0, 3237, 3235, 1, 0, 0, 0, 3238, 3240, 5, 3, 0, 0, 3239, 3229, 1, 0, 0, 0, 3239, 3240, 1, 0, 0, 0, 3240, 3242, 1, 0, 0, 0, 3241, 3195, 1, 0, 0, 0, 3241, 3200, 1, 0, 0, 0, 3241, 3207, 1, 0, 0, 0, 3241, 3216, 1, 0, 0, 0, 3241, 3222, 1, 0, 0, 0, 3241, 3228, 1, 0, 0, 0, 3242, 279, 1, 0, 0, 0, 3243, 3248, 3, 282, 141, 0, 3244, 3245, 5, 4, 0, 0, 3245, 3247, 3, 282, 141, 0, 3246, 3244, 1, 0, 0, 0, 3247, 3250, 1, 0, 0, 0, 3248, 3246, 1, 0, 0, 0, 3248, 3249, 1, 0, 0, 0, 3249, 281, 1, 0, 0, 0, 3250, 3248, 1, 0, 0, 0, 3251, 3252, 3, 214, 107, 0, 3252, 3256, 3, 278, 139, 0, 3253, 3255, 3, 284, 142, 0, 3254, 3253, 1, 0, 0, 0, 3255, 3258, 1, 0, 0, 0, 3256, 3254, 1, 0, 0, 0, 3256, 3257, 1, 0, 0, 0, 3257, 283, 1, 0, 0, 0, 3258, 3256, 1, 0, 0, 0, 3259, 3260, 5, 172, 0, 0, 3260, 3265, 5, 173, 0, 0, 3261, 3265, 3, 286, 143, 0, 3262, 3265, 3, 34, 17, 0, 3263, 3265, 3, 276, 138, 0, 3264, 3259, 1, 0, 0, 0, 3264, 3261, 1, 0, 0, 0, 3264, 3262, 1, 0, 0, 0, 3264, 3263, 1, 0, 0, 0, 3265, 285, 1, 0, 0, 0, 3266, 3267, 5, 70, 0, 0, 3267, 3268, 3, 236, 118, 0, 3268, 287, 1, 0, 0, 0, 3269, 3274, 3, 290, 145, 0, 3270, 3271, 5, 4, 0, 0, 3271, 3273, 3, 290, 145, 0, 3272, 3270, 1, 0, 0, 0, 3273, 3276, 1, 0, 0, 0, 3274, 3272, 1, 0, 0, 0, 3274, 3275, 1, 0, 0, 0, 3275, 289, 1, 0, 0, 0, 3276, 3274, 1, 0, 0, 0, 3277, 3278, 3, 322, 161, 0, 3278, 3281, 3, 278, 139, 0, 3279, 3280, 5, 172, 0, 0, 3280, 3282, 5, 173, 0, 0, 3281, 3279, 1, 0, 0, 0, 3281, 3282, 1, 0, 0, 0, 3282, 3284, 1, 0, 0, 0, 3283, 3285, 3, 34, 17, 0, 3284, 3283, 1, 0, 0, 0, 3284, 3285, 1, 0, 0, 0, 3285, 291, 1, 0, 0, 0, 3286, 3291, 3, 294, 147, 0, 3287, 3288, 5, 4, 0, 0, 3288, 3290, 3, 294, 147, 0, 3289, 3287, 1, 0, 0, 0, 3290, 3293, 1, 0, 0, 0, 3291, 3289, 1, 0, 0, 0, 3291, 3292, 1, 0, 0, 0, 3292, 293, 1, 0, 0, 0, 3293, 3291, 1, 0, 0, 0, 3294, 3295, 3, 322, 161, 0, 3295, 3299, 3, 278, 139, 0, 3296, 3298, 3, 296, 148, 0, 3297, 3296, 1, 0, 0, 0, 3298, 3301, 1, 0, 0, 0, 3299, 3297, 1, 0, 0, 0, 3299, 3300, 1, 0, 0, 0, 3300, 295, 1, 0, 0, 0, 3301, 3299, 1, 0, 0, 0, 3302, 3303, 5, 172, 0, 0, 3303, 3308, 5, 173, 0, 0, 3304, 3308, 3, 286, 143, 0, 3305, 3308, 3, 298, 149, 0, 3306, 3308, 3, 34, 17, 0, 3307, 3302, 1, 0, 0, 0, 3307, 3304, 1, 0, 0, 0, 3307, 3305, 1, 0, 0, 0, 3307, 3306, 1, 0, 0, 0, 3308, 297, 1, 0, 0, 0, 3309, 3310, 5, 111, 0, 0, 3310, 3311, 5, 12, 0, 0, 3311, 3312, 5, 20, 0, 0, 3312, 3313, 5, 2, 0, 0, 3313, 3314, 3, 236, 118, 0, 3314, 3315, 5, 3, 0, 0, 3315, 299, 1, 0, 0, 0, 3316, 3321, 3, 302, 151, 0, 3317, 3318, 5, 4, 0, 0, 3318, 3320, 3, 302, 151, 0, 3319, 3317, 1, 0, 0, 0, 3320, 3323, 1, 0, 0, 0, 3321, 3319, 1, 0, 0, 0, 3321, 3322, 1, 0, 0, 0, 3322, 301, 1, 0, 0, 0, 3323, 3321, 1, 0, 0, 0, 3324, 3326, 3, 326, 163, 0, 3325, 3327, 5, 326, 0, 0, 3326, 3325, 1, 0, 0, 0, 3326, 3327, 1, 0, 0, 0, 3327, 3328, 1, 0, 0, 0, 3328, 3331, 3, 278, 139, 0, 3329, 3330, 5, 172, 0, 0, 3330, 3332, 5, 173, 0, 0, 3331, 3329, 1, 0, 0, 0, 3331, 3332, 1, 0, 0, 0, 3332, 3334, 1, 0, 0, 0, 3333, 3335, 3, 34, 17, 0, 3334, 3333, 1, 0, 0, 0, 3334, 3335, 1, 0, 0, 0, 3335, 303, 1, 0, 0, 0, 3336, 3337, 5, 300, 0, 0, 3337, 3338, 3, 236, 118, 0, 3338, 3339, 5, 265, 0, 0, 3339, 3340, 3, 236, 118, 0, 3340, 305, 1, 0, 0, 0, 3341, 3342, 5, 302, 0, 0, 3342, 3347, 3, 308, 154, 0, 3343, 3344, 5, 4, 0, 0, 3344, 3346, 3, 308, 154, 0, 3345, 3343, 1, 0, 0, 0, 3346, 3349, 1, 0, 0, 0, 3347, 3345, 1, 0, 0, 0, 3347, 3348, 1, 0, 0, 0, 3348, 307, 1, 0, 0, 0, 3349, 3347, 1, 0, 0, 0, 3350, 3351, 3, 322, 161, 0, 3351, 3352, 5, 20, 0, 0, 3352, 3353, 3, 310, 155, 0, 3353, 309, 1, 0, 0, 0, 3354, 3401, 3, 322, 161, 0, 3355, 3356, 5, 2, 0, 0, 3356, 3357, 3, 322, 161, 0, 3357, 3358, 5, 3, 0, 0, 3358, 3401, 1, 0, 0, 0, 3359, 3394, 5, 2, 0, 0, 3360, 3361, 5, 38, 0, 0, 3361, 3362, 5, 28, 0, 0, 3362, 3367, 3, 236, 118, 0, 3363, 3364, 5, 4, 0, 0, 3364, 3366, 3, 236, 118, 0, 3365, 3363, 1, 0, 0, 0, 3366, 3369, 1, 0, 0, 0, 3367, 3365, 1, 0, 0, 0, 3367, 3368, 1, 0, 0, 0, 3368, 3395, 1, 0, 0, 0, 3369, 3367, 1, 0, 0, 0, 3370, 3371, 7, 49, 0, 0, 3371, 3372, 5, 28, 0, 0, 3372, 3377, 3, 236, 118, 0, 3373, 3374, 5, 4, 0, 0, 3374, 3376, 3, 236, 118, 0, 3375, 3373, 1, 0, 0, 0, 3376, 3379, 1, 0, 0, 0, 3377, 3375, 1, 0, 0, 0, 3377, 3378, 1, 0, 0, 0, 3378, 3381, 1, 0, 0, 0, 3379, 3377, 1, 0, 0, 0, 3380, 3370, 1, 0, 0, 0, 3380, 3381, 1, 0, 0, 0, 3381, 3392, 1, 0, 0, 0, 3382, 3383, 7, 50, 0, 0, 3383, 3384, 5, 28, 0, 0, 3384, 3389, 3, 94, 47, 0, 3385, 3386, 5, 4, 0, 0, 3386, 3388, 3, 94, 47, 0, 3387, 3385, 1, 0, 0, 0, 3388, 3391, 1, 0, 0, 0, 3389, 3387, 1, 0, 0, 0, 3389, 3390, 1, 0, 0, 0, 3390, 3393, 1, 0, 0, 0, 3391, 3389, 1, 0, 0, 0, 3392, 3382, 1, 0, 0, 0, 3392, 3393, 1, 0, 0, 0, 3393, 3395, 1, 0, 0, 0, 3394, 3360, 1, 0, 0, 0, 3394, 3380, 1, 0, 0, 0, 3395, 3397, 1, 0, 0, 0, 3396, 3398, 3, 312, 156, 0, 3397, 3396, 1, 0, 0, 0, 3397, 3398, 1, 0, 0, 0, 3398, 3399, 1, 0, 0, 0, 3399, 3401, 5, 3, 0, 0, 3400, 3354, 1, 0, 0, 0, 3400, 3355, 1, 0, 0, 0, 3400, 3359, 1, 0, 0, 0, 3401, 311, 1, 0, 0, 0, 3402, 3403, 5, 206, 0, 0, 3403, 3419, 3, 314, 157, 0, 3404, 3405, 5, 228, 0, 0, 3405, 3419, 3, 314, 157, 0, 3406, 3407, 5, 206, 0, 0, 3407, 3408, 5, 24, 0, 0, 3408, 3409, 3, 314, 157, 0, 3409, 3410, 5, 14, 0, 0, 3410, 3411, 3, 314, 157, 0, 3411, 3419, 1, 0, 0, 0, 3412, 3413, 5, 228, 0, 0, 3413, 3414, 5, 24, 0, 0, 3414, 3415, 3, 314, 157, 0, 3415, 3416, 5, 14, 0, 0, 3416, 3417, 3, 314, 157, 0, 3417, 3419, 1, 0, 0, 0, 3418, 3402, 1, 0, 0, 0, 3418, 3404, 1, 0, 0, 0, 3418, 3406, 1, 0, 0, 0, 3418, 3412, 1, 0, 0, 0, 3419, 313, 1, 0, 0, 0, 3420, 3421, 5, 282, 0, 0, 3421, 3428, 7, 51, 0, 0, 3422, 3423, 5, 56, 0, 0, 3423, 3428, 5, 227, 0, 0, 3424, 3425, 3, 236, 118, 0, 3425, 3426, 7, 51, 0, 0, 3426, 3428, 1, 0, 0, 0, 3427, 3420, 1, 0, 0, 0, 3427, 3422, 1, 0, 0, 0, 3427, 3424, 1, 0, 0, 0, 3428, 315, 1, 0, 0, 0, 3429, 3434, 3, 320, 160, 0, 3430, 3431, 5, 4, 0, 0, 3431, 3433, 3, 320, 160, 0, 3432, 3430, 1, 0, 0, 0, 3433, 3436, 1, 0, 0, 0, 3434, 3432, 1, 0, 0, 0, 3434, 3435, 1, 0, 0, 0, 3435, 317, 1, 0, 0, 0, 3436, 3434, 1, 0, 0, 0, 3437, 3442, 3, 320, 160, 0, 3438, 3442, 5, 99, 0, 0, 3439, 3442, 5, 141, 0, 0, 3440, 3442, 5, 221, 0, 0, 3441, 3437, 1, 0, 0, 0, 3441, 3438, 1, 0, 0, 0, 3441, 3439, 1, 0, 0, 0, 3441, 3440, 1, 0, 0, 0, 3442, 319, 1, 0, 0, 0, 3443, 3448, 3, 326, 163, 0, 3444, 3445, 5, 5, 0, 0, 3445, 3447, 3, 326, 163, 0, 3446, 3444, 1, 0, 0, 0, 3447, 3450, 1, 0, 0, 0, 3448, 3446, 1, 0, 0, 0, 3448, 3449, 1, 0, 0, 0, 3449, 321, 1, 0, 0, 0, 3450, 3448, 1, 0, 0, 0, 3451, 3452, 3, 326, 163, 0, 3452, 3453, 3, 324, 162, 0, 3453, 323, 1, 0, 0, 0, 3454, 3455, 5, 317, 0, 0, 3455, 3457, 3, 326, 163, 0, 3456, 3454, 1, 0, 0, 0, 3457, 3458, 1, 0, 0, 0, 3458, 3456, 1, 0, 0, 0, 3458, 3459, 1, 0, 0, 0, 3459, 3462, 1, 0, 0, 0, 3460, 3462, 1, 0, 0, 0, 3461, 3456, 1, 0, 0, 0, 3461, 3460, 1, 0, 0, 0, 3462, 325, 1, 0, 0, 0, 3463, 3466, 3, 328, 164, 0, 3464, 3466, 3, 346, 173, 0, 3465, 3463, 1, 0, 0, 0, 3465, 3464, 1, 0, 0, 0, 3466, 327, 1, 0, 0, 0, 3467, 3472, 5, 341, 0, 0, 3468, 3472, 3, 330, 165, 0, 3469, 3472, 3, 344, 172, 0, 3470, 3472, 3, 348, 174, 0, 3471, 3467, 1, 0, 0, 0, 3471, 3468, 1, 0, 0, 0, 3471, 3469, 1, 0, 0, 0, 3471, 3470, 1, 0, 0, 0, 3472, 329, 1, 0, 0, 0, 3473, 3474, 7, 52, 0, 0, 3474, 331, 1, 0, 0, 0, 3475, 3476, 5, 342, 0, 0, 3476, 333, 1, 0, 0, 0, 3477, 3479, 5, 317, 0, 0, 3478, 3477, 1, 0, 0, 0, 3478, 3479, 1, 0, 0, 0, 3479, 3480, 1, 0, 0, 0, 3480, 3518, 5, 336, 0, 0, 3481, 3483, 5, 317, 0, 0, 3482, 3481, 1, 0, 0, 0, 3482, 3483, 1, 0, 0, 0, 3483, 3484, 1, 0, 0, 0, 3484, 3518, 5, 337, 0, 0, 3485, 3487, 5, 317, 0, 0, 3486, 3485, 1, 0, 0, 0, 3486, 3487, 1, 0, 0, 0, 3487, 3488, 1, 0, 0, 0, 3488, 3518, 7, 53, 0, 0, 3489, 3491, 5, 317, 0, 0, 3490, 3489, 1, 0, 0, 0, 3490, 3491, 1, 0, 0, 0, 3491, 3492, 1, 0, 0, 0, 3492, 3518, 5, 335, 0, 0, 3493, 3495, 5, 317, 0, 0, 3494, 3493, 1, 0, 0, 0, 3494, 3495, 1, 0, 0, 0, 3495, 3496, 1, 0, 0, 0, 3496, 3518, 5, 332, 0, 0, 3497, 3499, 5, 317, 0, 0, 3498, 3497, 1, 0, 0, 0, 3498, 3499, 1, 0, 0, 0, 3499, 3500, 1, 0, 0, 0, 3500, 3518, 5, 333, 0, 0, 3501, 3503, 5, 317, 0, 0, 3502, 3501, 1, 0, 0, 0, 3502, 3503, 1, 0, 0, 0, 3503, 3504, 1, 0, 0, 0, 3504, 3518, 5, 334, 0, 0, 3505, 3507, 5, 317, 0, 0, 3506, 3505, 1, 0, 0, 0, 3506, 3507, 1, 0, 0, 0, 3507, 3508, 1, 0, 0, 0, 3508, 3518, 5, 339, 0, 0, 3509, 3511, 5, 317, 0, 0, 3510, 3509, 1, 0, 0, 0, 3510, 3511, 1, 0, 0, 0, 3511, 3512, 1, 0, 0, 0, 3512, 3518, 5, 338, 0, 0, 3513, 3515, 5, 317, 0, 0, 3514, 3513, 1, 0, 0, 0, 3514, 3515, 1, 0, 0, 0, 3515, 3516, 1, 0, 0, 0, 3516, 3518, 5, 340, 0, 0, 3517, 3478, 1, 0, 0, 0, 3517, 3482, 1, 0, 0, 0, 3517, 3486, 1, 0, 0, 0, 3517, 3490, 1, 0, 0, 0, 3517, 3494, 1, 0, 0, 0, 3517, 3498, 1, 0, 0, 0, 3517, 3502, 1, 0, 0, 0, 3517, 3506, 1, 0, 0, 0, 3517, 3510, 1, 0, 0, 0, 3517, 3514, 1, 0, 0, 0, 3518, 335, 1, 0, 0, 0, 3519, 3520, 5, 280, 0, 0, 3520, 3531, 3, 278, 139, 0, 3521, 3531, 3, 34, 17, 0, 3522, 3531, 3, 276, 138, 0, 3523, 3524, 7, 54, 0, 0, 3524, 3525, 5, 172, 0, 0, 3525, 3531, 5, 173, 0, 0, 3526, 3527, 5, 239, 0, 0, 3527, 3531, 3, 286, 143, 0, 3528, 3529, 5, 82, 0, 0, 3529, 3531, 5, 70, 0, 0, 3530, 3519, 1, 0, 0, 0, 3530, 3521, 1, 0, 0, 0, 3530, 3522, 1, 0, 0, 0, 3530, 3523, 1, 0, 0, 0, 3530, 3526, 1, 0, 0, 0, 3530, 3528, 1, 0, 0, 0, 3531, 337, 1, 0, 0, 0, 3532, 3533, 7, 55, 0, 0, 3533, 339, 1, 0, 0, 0, 3534, 3537, 3, 338, 169, 0, 3535, 3537, 5, 173, 0, 0, 3536, 3534, 1, 0, 0, 0, 3536, 3535, 1, 0, 0, 0, 3537, 341, 1, 0, 0, 0, 3538, 3541, 5, 335, 0, 0, 3539, 3541, 3, 338, 169, 0, 3540, 3538, 1, 0, 0, 0, 3540, 3539, 1, 0, 0, 0, 3541, 343, 1, 0, 0, 0, 3542, 3543, 7, 56, 0, 0, 3543, 345, 1, 0, 0, 0, 3544, 3545, 7, 57, 0, 0, 3545, 347, 1, 0, 0, 0, 3546, 3547, 7, 58, 0, 0, 3547, 349, 1, 0, 0, 0, 460, 354, 379, 392, 399, 407, 409, 429, 433, 439, 442, 445, 452, 455, 459, 462, 469, 480, 482, 490, 493, 497, 500, 506, 517, 523, 528, 562, 575, 600, 609, 613, 619, 623, 628, 634, 646, 654, 660, 673, 678, 694, 701, 705, 711, 726, 730, 736, 742, 745, 748, 754, 758, 766, 768, 777, 780, 789, 794, 800, 807, 810, 816, 827, 830, 834, 839, 844, 851, 854, 857, 864, 869, 878, 886, 892, 895, 898, 904, 908, 913, 916, 920, 922, 930, 938, 941, 946, 952, 958, 961, 965, 968, 972, 1000, 1003, 1007, 1013, 1016, 1019, 1025, 1033, 1038, 1044, 1050, 1053, 1060, 1067, 1075, 1092, 1106, 1109, 1115, 1124, 1133, 1141, 1146, 1151, 1158, 1164, 1169, 1177, 1180, 1184, 1196, 1200, 1207, 1323, 1331, 1339, 1348, 1358, 1362, 1365, 1371, 1377, 1389, 1401, 1406, 1417, 1424, 1426, 1429, 1434, 1438, 1443, 1446, 1451, 1460, 1465, 1468, 1473, 1477, 1482, 1484, 1488, 1497, 1505, 1511, 1522, 1529, 1538, 1542, 1549, 1552, 1564, 1569, 1574, 1579, 1583, 1586, 1589, 1592, 1596, 1601, 1605, 1608, 1611, 1614, 1616, 1627, 1645, 1647, 1656, 1663, 1666, 1673, 1677, 1683, 1691, 1702, 1713, 1721, 1727, 1739, 1746, 1753, 1765, 1773, 1779, 1785, 1788, 1797, 1800, 1809, 1812, 1821, 1824, 1833, 1836, 1839, 1844, 1846, 1850, 1857, 1861, 1867, 1871, 1879, 1883, 1886, 1889, 1892, 1896, 1902, 1909, 1914, 1917, 1920, 1924, 1934, 1938, 1940, 1943, 1947, 1953, 1957, 1968, 1978, 1982, 1994, 2006, 2021, 2026, 2032, 2039, 2055, 2060, 2073, 2078, 2086, 2092, 2096, 2099, 2102, 2109, 2115, 2124, 2134, 2149, 2154, 2156, 2160, 2169, 2182, 2187, 2191, 2199, 2202, 2206, 2220, 2233, 2238, 2242, 2245, 2249, 2255, 2258, 2265, 2277, 2288, 2301, 2312, 2317, 2325, 2330, 2337, 2346, 2349, 2354, 2361, 2364, 2369, 2375, 2381, 2386, 2390, 2396, 2400, 2403, 2408, 2411, 2416, 2420, 2423, 2426, 2432, 2437, 2444, 2447, 2465, 2467, 2470, 2481, 2490, 2497, 2505, 2512, 2516, 2519, 2527, 2535, 2541, 2549, 2561, 2564, 2570, 2574, 2576, 2585, 2597, 2599, 2606, 2613, 2619, 2625, 2627, 2634, 2642, 2650, 2656, 2661, 2668, 2674, 2678, 2680, 2687, 2696, 2703, 2713, 2718, 2722, 2731, 2744, 2746, 2754, 2756, 2760, 2768, 2777, 2783, 2791, 2796, 2808, 2813, 2816, 2822, 2826, 2831, 2836, 2841, 2847, 2868, 2870, 2899, 2903, 2912, 2916, 2934, 2937, 2945, 2954, 2963, 2986, 2997, 3004, 3007, 3016, 3020, 3024, 3036, 3061, 3068, 3071, 3086, 3107, 3111, 3113, 3123, 3125, 3140, 3142, 3155, 3159, 3166, 3171, 3179, 3184, 3193, 3210, 3214, 3220, 3226, 3235, 3239, 3241, 3248, 3256, 3264, 3274, 3281, 3284, 3291, 3299, 3307, 3321, 3326, 3331, 3334, 3347, 3367, 3377, 3380, 3389, 3392, 3394, 3397, 3400, 3418, 3427, 3434, 3441, 3448, 3458, 3461, 3465, 3471, 3478, 3482, 3486, 3490, 3494, 3498, 3502, 3506, 3510, 3514, 3517, 3530, 3536, 3540] \ No newline at end of file diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParser.py b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParser.py new file mode 100644 index 000000000..fbb6d3495 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParser.py @@ -0,0 +1,28963 @@ +# Generated from SqlBaseParser.g4 by ANTLR 4.13.1 +# encoding: utf-8 +from antlr4 import * +from io import StringIO +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO + +def serializedATN(): + return [ + 4,1,346,3549,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6, + 7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7, + 13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2, + 20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7, + 26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,31,7,31,2,32,7,32,2, + 33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,7,38,2,39,7, + 39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,2,45,7,45,2, + 46,7,46,2,47,7,47,2,48,7,48,2,49,7,49,2,50,7,50,2,51,7,51,2,52,7, + 52,2,53,7,53,2,54,7,54,2,55,7,55,2,56,7,56,2,57,7,57,2,58,7,58,2, + 59,7,59,2,60,7,60,2,61,7,61,2,62,7,62,2,63,7,63,2,64,7,64,2,65,7, + 65,2,66,7,66,2,67,7,67,2,68,7,68,2,69,7,69,2,70,7,70,2,71,7,71,2, + 72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,2,77,7,77,2,78,7, + 78,2,79,7,79,2,80,7,80,2,81,7,81,2,82,7,82,2,83,7,83,2,84,7,84,2, + 85,7,85,2,86,7,86,2,87,7,87,2,88,7,88,2,89,7,89,2,90,7,90,2,91,7, + 91,2,92,7,92,2,93,7,93,2,94,7,94,2,95,7,95,2,96,7,96,2,97,7,97,2, + 98,7,98,2,99,7,99,2,100,7,100,2,101,7,101,2,102,7,102,2,103,7,103, + 2,104,7,104,2,105,7,105,2,106,7,106,2,107,7,107,2,108,7,108,2,109, + 7,109,2,110,7,110,2,111,7,111,2,112,7,112,2,113,7,113,2,114,7,114, + 2,115,7,115,2,116,7,116,2,117,7,117,2,118,7,118,2,119,7,119,2,120, + 7,120,2,121,7,121,2,122,7,122,2,123,7,123,2,124,7,124,2,125,7,125, + 2,126,7,126,2,127,7,127,2,128,7,128,2,129,7,129,2,130,7,130,2,131, + 7,131,2,132,7,132,2,133,7,133,2,134,7,134,2,135,7,135,2,136,7,136, + 2,137,7,137,2,138,7,138,2,139,7,139,2,140,7,140,2,141,7,141,2,142, + 7,142,2,143,7,143,2,144,7,144,2,145,7,145,2,146,7,146,2,147,7,147, + 2,148,7,148,2,149,7,149,2,150,7,150,2,151,7,151,2,152,7,152,2,153, + 7,153,2,154,7,154,2,155,7,155,2,156,7,156,2,157,7,157,2,158,7,158, + 2,159,7,159,2,160,7,160,2,161,7,161,2,162,7,162,2,163,7,163,2,164, + 7,164,2,165,7,165,2,166,7,166,2,167,7,167,2,168,7,168,2,169,7,169, + 2,170,7,170,2,171,7,171,2,172,7,172,2,173,7,173,2,174,7,174,1,0, + 1,0,5,0,353,8,0,10,0,12,0,356,9,0,1,0,1,0,1,1,1,1,1,1,1,2,1,2,1, + 2,1,3,1,3,1,3,1,4,1,4,1,4,1,5,1,5,1,5,1,6,1,6,1,6,1,7,1,7,3,7,380, + 8,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,3,7,393,8,7,1,7, + 1,7,1,7,1,7,1,7,3,7,400,8,7,1,7,1,7,1,7,1,7,1,7,1,7,5,7,408,8,7, + 10,7,12,7,411,9,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1, + 7,1,7,1,7,1,7,1,7,1,7,3,7,430,8,7,1,7,1,7,3,7,434,8,7,1,7,1,7,1, + 7,1,7,3,7,440,8,7,1,7,3,7,443,8,7,1,7,3,7,446,8,7,1,7,1,7,1,7,1, + 7,1,7,3,7,453,8,7,1,7,3,7,456,8,7,1,7,1,7,3,7,460,8,7,1,7,3,7,463, + 8,7,1,7,1,7,1,7,1,7,1,7,3,7,470,8,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7, + 1,7,1,7,5,7,481,8,7,10,7,12,7,484,9,7,1,7,1,7,1,7,1,7,1,7,3,7,491, + 8,7,1,7,3,7,494,8,7,1,7,1,7,3,7,498,8,7,1,7,3,7,501,8,7,1,7,1,7, + 1,7,1,7,3,7,507,8,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,3,7,518, + 8,7,1,7,1,7,1,7,1,7,3,7,524,8,7,1,7,1,7,1,7,3,7,529,8,7,1,7,1,7, + 1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7, + 1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,3,7,563, + 8,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,3,7,576,8,7,1,7, + 1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7, + 1,7,1,7,1,7,1,7,1,7,1,7,3,7,601,8,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7, + 3,7,610,8,7,1,7,1,7,3,7,614,8,7,1,7,1,7,1,7,1,7,3,7,620,8,7,1,7, + 1,7,3,7,624,8,7,1,7,1,7,1,7,3,7,629,8,7,1,7,1,7,1,7,1,7,3,7,635, + 8,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,3,7,647,8,7,1,7,1,7, + 1,7,1,7,1,7,1,7,3,7,655,8,7,1,7,1,7,1,7,1,7,3,7,661,8,7,1,7,1,7, + 1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,3,7,674,8,7,1,7,4,7,677,8,7, + 11,7,12,7,678,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1, + 7,1,7,3,7,695,8,7,1,7,1,7,1,7,5,7,700,8,7,10,7,12,7,703,9,7,1,7, + 3,7,706,8,7,1,7,1,7,1,7,1,7,3,7,712,8,7,1,7,1,7,1,7,1,7,1,7,1,7, + 1,7,1,7,1,7,1,7,1,7,1,7,1,7,3,7,727,8,7,1,7,1,7,3,7,731,8,7,1,7, + 1,7,1,7,1,7,3,7,737,8,7,1,7,1,7,1,7,1,7,3,7,743,8,7,1,7,3,7,746, + 8,7,1,7,3,7,749,8,7,1,7,1,7,1,7,1,7,3,7,755,8,7,1,7,1,7,3,7,759, + 8,7,1,7,1,7,1,7,1,7,1,7,1,7,5,7,767,8,7,10,7,12,7,770,9,7,1,7,1, + 7,1,7,1,7,1,7,1,7,3,7,778,8,7,1,7,3,7,781,8,7,1,7,1,7,1,7,1,7,1, + 7,1,7,1,7,3,7,790,8,7,1,7,1,7,1,7,3,7,795,8,7,1,7,1,7,1,7,1,7,3, + 7,801,8,7,1,7,1,7,1,7,1,7,1,7,3,7,808,8,7,1,7,3,7,811,8,7,1,7,1, + 7,1,7,1,7,3,7,817,8,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,5,7,826,8,7,10, + 7,12,7,829,9,7,3,7,831,8,7,1,7,1,7,3,7,835,8,7,1,7,1,7,1,7,3,7,840, + 8,7,1,7,1,7,1,7,3,7,845,8,7,1,7,1,7,1,7,1,7,1,7,3,7,852,8,7,1,7, + 3,7,855,8,7,1,7,3,7,858,8,7,1,7,1,7,1,7,1,7,1,7,3,7,865,8,7,1,7, + 1,7,1,7,3,7,870,8,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,3,7,879,8,7,1,7, + 1,7,1,7,1,7,1,7,1,7,3,7,887,8,7,1,7,1,7,1,7,1,7,3,7,893,8,7,1,7, + 3,7,896,8,7,1,7,3,7,899,8,7,1,7,1,7,1,7,1,7,3,7,905,8,7,1,7,1,7, + 3,7,909,8,7,1,7,1,7,1,7,3,7,914,8,7,1,7,3,7,917,8,7,1,7,1,7,3,7, + 921,8,7,3,7,923,8,7,1,7,1,7,1,7,1,7,1,7,1,7,3,7,931,8,7,1,7,1,7, + 1,7,1,7,1,7,1,7,3,7,939,8,7,1,7,3,7,942,8,7,1,7,1,7,1,7,3,7,947, + 8,7,1,7,1,7,1,7,1,7,3,7,953,8,7,1,7,1,7,1,7,1,7,3,7,959,8,7,1,7, + 3,7,962,8,7,1,7,1,7,3,7,966,8,7,1,7,3,7,969,8,7,1,7,1,7,3,7,973, + 8,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7, + 1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,5,7,999,8,7,10,7,12,7,1002,9, + 7,3,7,1004,8,7,1,7,1,7,3,7,1008,8,7,1,7,1,7,1,7,1,7,3,7,1014,8,7, + 1,7,3,7,1017,8,7,1,7,3,7,1020,8,7,1,7,1,7,1,7,1,7,3,7,1026,8,7,1, + 7,1,7,1,7,1,7,1,7,1,7,3,7,1034,8,7,1,7,1,7,1,7,3,7,1039,8,7,1,7, + 1,7,1,7,1,7,3,7,1045,8,7,1,7,1,7,1,7,1,7,3,7,1051,8,7,1,7,3,7,1054, + 8,7,1,7,1,7,1,7,1,7,1,7,3,7,1061,8,7,1,7,1,7,1,7,5,7,1066,8,7,10, + 7,12,7,1069,9,7,1,7,1,7,1,7,5,7,1074,8,7,10,7,12,7,1077,9,7,1,7, + 1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,5,7,1091,8,7,10,7,12, + 7,1094,9,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,5,7,1105,8,7,10,7, + 12,7,1108,9,7,3,7,1110,8,7,1,7,1,7,5,7,1114,8,7,10,7,12,7,1117,9, + 7,1,7,1,7,1,7,1,7,5,7,1123,8,7,10,7,12,7,1126,9,7,1,7,1,7,1,7,1, + 7,5,7,1132,8,7,10,7,12,7,1135,9,7,1,7,1,7,1,7,1,7,1,7,3,7,1142,8, + 7,1,7,1,7,1,7,3,7,1147,8,7,1,7,1,7,1,7,3,7,1152,8,7,1,7,1,7,1,7, + 1,7,1,7,3,7,1159,8,7,1,7,1,7,1,7,1,7,3,7,1165,8,7,1,7,1,7,1,7,3, + 7,1170,8,7,1,7,1,7,1,7,1,7,5,7,1176,8,7,10,7,12,7,1179,9,7,3,7,1181, + 8,7,1,8,1,8,3,8,1185,8,8,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,11,1, + 11,1,11,3,11,1197,8,11,1,11,1,11,3,11,1201,8,11,1,11,1,11,1,11,1, + 11,1,11,3,11,1208,8,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1, + 11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1, + 11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1, + 11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1, + 11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1, + 11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1, + 11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1, + 11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1, + 11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1, + 11,1,11,3,11,1324,8,11,1,11,1,11,1,11,1,11,1,11,1,11,3,11,1332,8, + 11,1,11,1,11,1,11,1,11,1,11,1,11,3,11,1340,8,11,1,11,1,11,1,11,1, + 11,1,11,1,11,1,11,3,11,1349,8,11,1,11,1,11,1,11,1,11,1,11,1,11,1, + 11,1,11,3,11,1359,8,11,1,12,1,12,3,12,1363,8,12,1,12,3,12,1366,8, + 12,1,12,1,12,1,12,1,12,3,12,1372,8,12,1,12,1,12,1,13,1,13,3,13,1378, + 8,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,3,14,1390, + 8,14,1,14,1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,3,15,1402, + 8,15,1,15,1,15,1,15,3,15,1407,8,15,1,16,1,16,1,16,1,17,1,17,1,17, + 1,18,1,18,1,18,3,18,1418,8,18,1,18,1,18,1,18,1,18,1,18,3,18,1425, + 8,18,3,18,1427,8,18,1,18,3,18,1430,8,18,1,18,1,18,1,18,3,18,1435, + 8,18,1,18,1,18,3,18,1439,8,18,1,18,1,18,1,18,3,18,1444,8,18,1,18, + 3,18,1447,8,18,1,18,1,18,1,18,3,18,1452,8,18,1,18,1,18,1,18,1,18, + 1,18,1,18,1,18,3,18,1461,8,18,1,18,1,18,1,18,3,18,1466,8,18,1,18, + 3,18,1469,8,18,1,18,1,18,1,18,3,18,1474,8,18,1,18,1,18,3,18,1478, + 8,18,1,18,1,18,1,18,3,18,1483,8,18,3,18,1485,8,18,1,19,1,19,3,19, + 1489,8,19,1,20,1,20,1,20,1,20,1,20,5,20,1496,8,20,10,20,12,20,1499, + 9,20,1,20,1,20,1,21,1,21,1,21,3,21,1506,8,21,1,21,1,21,1,21,1,21, + 3,21,1512,8,21,1,22,1,22,1,23,1,23,1,24,1,24,1,24,1,24,1,24,3,24, + 1523,8,24,1,25,1,25,1,25,5,25,1528,8,25,10,25,12,25,1531,9,25,1, + 26,1,26,1,26,1,26,5,26,1537,8,26,10,26,12,26,1540,9,26,1,27,3,27, + 1543,8,27,1,27,1,27,1,27,1,28,1,28,3,28,1550,8,28,1,28,3,28,1553, + 8,28,1,28,1,28,1,28,1,28,1,29,1,29,1,29,1,29,1,29,1,29,3,29,1565, + 8,29,1,29,5,29,1568,8,29,10,29,12,29,1571,9,29,1,30,1,30,3,30,1575, + 8,30,1,30,5,30,1578,8,30,10,30,12,30,1581,9,30,1,30,3,30,1584,8, + 30,1,30,3,30,1587,8,30,1,30,3,30,1590,8,30,1,30,3,30,1593,8,30,1, + 30,1,30,3,30,1597,8,30,1,30,5,30,1600,8,30,10,30,12,30,1603,9,30, + 1,30,3,30,1606,8,30,1,30,3,30,1609,8,30,1,30,3,30,1612,8,30,1,30, + 3,30,1615,8,30,3,30,1617,8,30,1,31,1,31,1,31,1,31,1,31,1,31,1,31, + 1,31,1,31,3,31,1628,8,31,1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,33, + 1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,5,33,1646,8,33,10,33,12, + 33,1649,9,33,1,34,1,34,1,34,1,34,5,34,1655,8,34,10,34,12,34,1658, + 9,34,1,34,1,34,1,35,1,35,3,35,1664,8,35,1,35,3,35,1667,8,35,1,36, + 1,36,1,36,5,36,1672,8,36,10,36,12,36,1675,9,36,1,36,3,36,1678,8, + 36,1,37,1,37,1,37,1,37,3,37,1684,8,37,1,38,1,38,1,38,1,38,5,38,1690, + 8,38,10,38,12,38,1693,9,38,1,38,1,38,1,39,1,39,1,39,1,39,5,39,1701, + 8,39,10,39,12,39,1704,9,39,1,39,1,39,1,40,1,40,1,40,1,40,1,40,1, + 40,3,40,1714,8,40,1,41,1,41,1,41,1,41,1,41,1,41,3,41,1722,8,41,1, + 42,1,42,1,42,1,42,3,42,1728,8,42,1,43,1,43,1,43,1,44,1,44,1,44,1, + 44,1,44,4,44,1738,8,44,11,44,12,44,1739,1,44,1,44,1,44,1,44,1,44, + 3,44,1747,8,44,1,44,1,44,1,44,1,44,1,44,3,44,1754,8,44,1,44,1,44, + 1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,1766,8,44,1,44,1,44, + 1,44,1,44,5,44,1772,8,44,10,44,12,44,1775,9,44,1,44,5,44,1778,8, + 44,10,44,12,44,1781,9,44,1,44,5,44,1784,8,44,10,44,12,44,1787,9, + 44,3,44,1789,8,44,1,45,1,45,1,45,1,45,1,45,5,45,1796,8,45,10,45, + 12,45,1799,9,45,3,45,1801,8,45,1,45,1,45,1,45,1,45,1,45,5,45,1808, + 8,45,10,45,12,45,1811,9,45,3,45,1813,8,45,1,45,1,45,1,45,1,45,1, + 45,5,45,1820,8,45,10,45,12,45,1823,9,45,3,45,1825,8,45,1,45,1,45, + 1,45,1,45,1,45,5,45,1832,8,45,10,45,12,45,1835,9,45,3,45,1837,8, + 45,1,45,3,45,1840,8,45,1,45,1,45,1,45,3,45,1845,8,45,3,45,1847,8, + 45,1,45,1,45,3,45,1851,8,45,1,46,1,46,1,46,1,47,1,47,3,47,1858,8, + 47,1,47,1,47,3,47,1862,8,47,1,48,1,48,4,48,1866,8,48,11,48,12,48, + 1867,1,49,1,49,3,49,1872,8,49,1,49,1,49,1,49,1,49,5,49,1878,8,49, + 10,49,12,49,1881,9,49,1,49,3,49,1884,8,49,1,49,3,49,1887,8,49,1, + 49,3,49,1890,8,49,1,49,3,49,1893,8,49,1,49,1,49,3,49,1897,8,49,1, + 50,1,50,1,50,1,50,3,50,1903,8,50,1,50,1,50,1,50,1,50,1,50,3,50,1910, + 8,50,1,50,1,50,1,50,3,50,1915,8,50,1,50,3,50,1918,8,50,1,50,3,50, + 1921,8,50,1,50,1,50,3,50,1925,8,50,1,50,1,50,1,50,1,50,1,50,1,50, + 1,50,1,50,3,50,1935,8,50,1,50,1,50,3,50,1939,8,50,3,50,1941,8,50, + 1,50,3,50,1944,8,50,1,50,1,50,3,50,1948,8,50,1,51,1,51,5,51,1952, + 8,51,10,51,12,51,1955,9,51,1,51,3,51,1958,8,51,1,51,1,51,1,52,1, + 52,1,52,1,53,1,53,1,53,1,53,3,53,1969,8,53,1,53,1,53,1,53,1,54,1, + 54,1,54,1,54,1,54,3,54,1979,8,54,1,54,1,54,3,54,1983,8,54,1,54,1, + 54,1,54,1,55,1,55,1,55,1,55,1,55,1,55,1,55,3,55,1995,8,55,1,55,1, + 55,1,55,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,2007,8,56,1,57,1, + 57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,5,57,2020,8,57,10, + 57,12,57,2023,9,57,1,57,1,57,3,57,2027,8,57,1,58,1,58,1,58,1,58, + 3,58,2033,8,58,1,59,1,59,1,59,5,59,2038,8,59,10,59,12,59,2041,9, + 59,1,60,1,60,1,60,1,60,1,61,1,61,1,61,1,62,1,62,1,62,1,63,1,63,1, + 63,3,63,2056,8,63,1,63,5,63,2059,8,63,10,63,12,63,2062,9,63,1,63, + 1,63,1,64,1,64,1,64,1,64,1,64,1,64,5,64,2072,8,64,10,64,12,64,2075, + 9,64,1,64,1,64,3,64,2079,8,64,1,65,1,65,1,65,1,65,5,65,2085,8,65, + 10,65,12,65,2088,9,65,1,65,5,65,2091,8,65,10,65,12,65,2094,9,65, + 1,65,3,65,2097,8,65,1,65,3,65,2100,8,65,1,66,3,66,2103,8,66,1,66, + 1,66,1,66,1,66,1,66,3,66,2110,8,66,1,66,1,66,1,66,1,66,3,66,2116, + 8,66,1,67,1,67,1,67,1,67,1,67,5,67,2123,8,67,10,67,12,67,2126,9, + 67,1,67,1,67,1,67,1,67,1,67,5,67,2133,8,67,10,67,12,67,2136,9,67, + 1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,5,67,2148,8,67, + 10,67,12,67,2151,9,67,1,67,1,67,3,67,2155,8,67,3,67,2157,8,67,1, + 68,1,68,3,68,2161,8,68,1,69,1,69,1,69,1,69,1,69,5,69,2168,8,69,10, + 69,12,69,2171,9,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,5,69, + 2181,8,69,10,69,12,69,2184,9,69,1,69,1,69,3,69,2188,8,69,1,70,1, + 70,3,70,2192,8,70,1,71,1,71,1,71,1,71,5,71,2198,8,71,10,71,12,71, + 2201,9,71,3,71,2203,8,71,1,71,1,71,3,71,2207,8,71,1,72,1,72,1,72, + 1,72,1,72,1,72,1,72,1,72,1,72,1,72,5,72,2219,8,72,10,72,12,72,2222, + 9,72,1,72,1,72,1,72,1,73,1,73,1,73,1,73,1,73,5,73,2232,8,73,10,73, + 12,73,2235,9,73,1,73,1,73,3,73,2239,8,73,1,74,1,74,3,74,2243,8,74, + 1,74,3,74,2246,8,74,1,75,1,75,3,75,2250,8,75,1,75,1,75,1,75,1,75, + 3,75,2256,8,75,1,75,3,75,2259,8,75,1,76,1,76,1,76,1,77,1,77,3,77, + 2266,8,77,1,78,1,78,1,78,1,78,1,78,1,78,1,78,1,78,5,78,2276,8,78, + 10,78,12,78,2279,9,78,1,78,1,78,1,79,1,79,1,79,1,79,5,79,2287,8, + 79,10,79,12,79,2290,9,79,1,79,1,79,1,79,1,79,1,79,1,79,1,79,1,79, + 5,79,2300,8,79,10,79,12,79,2303,9,79,1,79,1,79,1,80,1,80,1,80,1, + 80,5,80,2311,8,80,10,80,12,80,2314,9,80,1,80,1,80,3,80,2318,8,80, + 1,81,1,81,1,82,1,82,1,83,1,83,3,83,2326,8,83,1,84,1,84,1,85,3,85, + 2331,8,85,1,85,1,85,1,86,1,86,1,86,3,86,2338,8,86,1,86,1,86,1,86, + 1,86,1,86,5,86,2345,8,86,10,86,12,86,2348,9,86,3,86,2350,8,86,1, + 86,1,86,1,86,3,86,2355,8,86,1,86,1,86,1,86,5,86,2360,8,86,10,86, + 12,86,2363,9,86,3,86,2365,8,86,1,87,1,87,1,88,3,88,2370,8,88,1,88, + 1,88,5,88,2374,8,88,10,88,12,88,2377,9,88,1,89,1,89,1,89,3,89,2382, + 8,89,1,90,1,90,1,90,3,90,2387,8,90,1,90,1,90,3,90,2391,8,90,1,90, + 1,90,1,90,1,90,3,90,2397,8,90,1,90,1,90,3,90,2401,8,90,1,91,3,91, + 2404,8,91,1,91,1,91,1,91,3,91,2409,8,91,1,91,3,91,2412,8,91,1,91, + 1,91,1,91,3,91,2417,8,91,1,91,1,91,3,91,2421,8,91,1,91,3,91,2424, + 8,91,1,91,3,91,2427,8,91,1,92,1,92,1,92,1,92,3,92,2433,8,92,1,93, + 1,93,1,93,3,93,2438,8,93,1,93,1,93,1,93,1,93,1,93,3,93,2445,8,93, + 1,94,3,94,2448,8,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94, + 1,94,1,94,1,94,1,94,1,94,1,94,1,94,3,94,2466,8,94,3,94,2468,8,94, + 1,94,3,94,2471,8,94,1,95,1,95,1,95,1,95,1,96,1,96,1,96,5,96,2480, + 8,96,10,96,12,96,2483,9,96,1,97,1,97,1,97,1,97,5,97,2489,8,97,10, + 97,12,97,2492,9,97,1,97,1,97,1,98,1,98,3,98,2498,8,98,1,99,1,99, + 1,99,1,99,5,99,2504,8,99,10,99,12,99,2507,9,99,1,99,1,99,1,100,1, + 100,3,100,2513,8,100,1,101,1,101,3,101,2517,8,101,1,101,3,101,2520, + 8,101,1,101,1,101,1,101,1,101,1,101,1,101,3,101,2528,8,101,1,101, + 1,101,1,101,1,101,1,101,1,101,3,101,2536,8,101,1,101,1,101,1,101, + 1,101,3,101,2542,8,101,1,102,1,102,1,102,1,102,5,102,2548,8,102, + 10,102,12,102,2551,9,102,1,102,1,102,1,103,1,103,1,103,1,103,1,103, + 5,103,2560,8,103,10,103,12,103,2563,9,103,3,103,2565,8,103,1,103, + 1,103,1,103,1,104,3,104,2571,8,104,1,104,1,104,3,104,2575,8,104, + 3,104,2577,8,104,1,105,1,105,1,105,1,105,1,105,1,105,1,105,3,105, + 2586,8,105,1,105,1,105,1,105,1,105,1,105,1,105,1,105,1,105,1,105, + 1,105,3,105,2598,8,105,3,105,2600,8,105,1,105,1,105,1,105,1,105, + 1,105,3,105,2607,8,105,1,105,1,105,1,105,1,105,1,105,3,105,2614, + 8,105,1,105,1,105,1,105,1,105,3,105,2620,8,105,1,105,1,105,1,105, + 1,105,3,105,2626,8,105,3,105,2628,8,105,1,106,1,106,1,106,5,106, + 2633,8,106,10,106,12,106,2636,9,106,1,107,1,107,1,107,5,107,2641, + 8,107,10,107,12,107,2644,9,107,1,108,1,108,1,108,5,108,2649,8,108, + 10,108,12,108,2652,9,108,1,109,1,109,1,109,3,109,2657,8,109,1,110, + 1,110,1,110,3,110,2662,8,110,1,110,1,110,1,111,1,111,1,111,3,111, + 2669,8,111,1,111,1,111,1,112,1,112,3,112,2675,8,112,1,112,1,112, + 3,112,2679,8,112,3,112,2681,8,112,1,113,1,113,1,113,5,113,2686,8, + 113,10,113,12,113,2689,9,113,1,114,1,114,1,114,1,114,5,114,2695, + 8,114,10,114,12,114,2698,9,114,1,114,1,114,1,115,1,115,3,115,2704, + 8,115,1,116,1,116,1,116,1,116,1,116,1,116,5,116,2712,8,116,10,116, + 12,116,2715,9,116,1,116,1,116,3,116,2719,8,116,1,117,1,117,3,117, + 2723,8,117,1,118,1,118,1,119,1,119,1,119,5,119,2730,8,119,10,119, + 12,119,2733,9,119,1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,120, + 1,120,1,120,3,120,2745,8,120,3,120,2747,8,120,1,120,1,120,1,120, + 1,120,1,120,1,120,5,120,2755,8,120,10,120,12,120,2758,9,120,1,121, + 3,121,2761,8,121,1,121,1,121,1,121,1,121,1,121,1,121,3,121,2769, + 8,121,1,121,1,121,1,121,1,121,1,121,5,121,2776,8,121,10,121,12,121, + 2779,9,121,1,121,1,121,1,121,3,121,2784,8,121,1,121,1,121,1,121, + 1,121,1,121,1,121,3,121,2792,8,121,1,121,1,121,1,121,3,121,2797, + 8,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121,5,121,2807, + 8,121,10,121,12,121,2810,9,121,1,121,1,121,3,121,2814,8,121,1,121, + 3,121,2817,8,121,1,121,1,121,1,121,1,121,3,121,2823,8,121,1,121, + 1,121,3,121,2827,8,121,1,121,1,121,1,121,3,121,2832,8,121,1,121, + 1,121,1,121,3,121,2837,8,121,1,121,1,121,1,121,3,121,2842,8,121, + 1,122,1,122,1,122,1,122,3,122,2848,8,122,1,122,1,122,1,122,1,122, + 1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,122, + 1,122,1,122,1,122,1,122,5,122,2869,8,122,10,122,12,122,2872,9,122, + 1,123,1,123,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124, + 1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124, + 1,124,1,124,4,124,2898,8,124,11,124,12,124,2899,1,124,1,124,3,124, + 2904,8,124,1,124,1,124,1,124,1,124,1,124,4,124,2911,8,124,11,124, + 12,124,2912,1,124,1,124,3,124,2917,8,124,1,124,1,124,1,124,1,124, + 1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,5,124, + 2933,8,124,10,124,12,124,2936,9,124,3,124,2938,8,124,1,124,1,124, + 1,124,1,124,1,124,1,124,3,124,2946,8,124,1,124,1,124,1,124,1,124, + 1,124,1,124,1,124,3,124,2955,8,124,1,124,1,124,1,124,1,124,1,124, + 1,124,1,124,3,124,2964,8,124,1,124,1,124,1,124,1,124,1,124,1,124, + 1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124, + 1,124,1,124,4,124,2985,8,124,11,124,12,124,2986,1,124,1,124,1,124, + 1,124,1,124,1,124,1,124,1,124,1,124,3,124,2998,8,124,1,124,1,124, + 1,124,5,124,3003,8,124,10,124,12,124,3006,9,124,3,124,3008,8,124, + 1,124,1,124,1,124,1,124,1,124,1,124,1,124,3,124,3017,8,124,1,124, + 1,124,3,124,3021,8,124,1,124,1,124,3,124,3025,8,124,1,124,1,124, + 1,124,1,124,1,124,1,124,1,124,1,124,4,124,3035,8,124,11,124,12,124, + 3036,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124, + 1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124, + 1,124,1,124,3,124,3062,8,124,1,124,1,124,1,124,1,124,1,124,3,124, + 3069,8,124,1,124,3,124,3072,8,124,1,124,1,124,1,124,1,124,1,124, + 1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,3,124,3087,8,124, + 1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124, + 1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,3,124,3108,8,124, + 1,124,1,124,3,124,3112,8,124,3,124,3114,8,124,1,124,1,124,1,124, + 1,124,1,124,1,124,1,124,1,124,5,124,3124,8,124,10,124,12,124,3127, + 9,124,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125, + 4,125,3139,8,125,11,125,12,125,3140,3,125,3143,8,125,1,126,1,126, + 1,127,1,127,1,128,1,128,1,129,1,129,1,130,1,130,1,130,3,130,3156, + 8,130,1,131,1,131,3,131,3160,8,131,1,132,1,132,1,132,4,132,3165, + 8,132,11,132,12,132,3166,1,133,1,133,1,133,3,133,3172,8,133,1,134, + 1,134,1,134,1,134,1,134,1,135,3,135,3180,8,135,1,135,1,135,1,135, + 3,135,3185,8,135,1,136,1,136,1,137,1,137,1,138,1,138,1,138,3,138, + 3194,8,138,1,139,1,139,1,139,1,139,1,139,1,139,1,139,1,139,1,139, + 1,139,1,139,1,139,1,139,1,139,1,139,3,139,3211,8,139,1,139,1,139, + 3,139,3215,8,139,1,139,1,139,1,139,1,139,3,139,3221,8,139,1,139, + 1,139,1,139,1,139,3,139,3227,8,139,1,139,1,139,1,139,1,139,1,139, + 5,139,3234,8,139,10,139,12,139,3237,9,139,1,139,3,139,3240,8,139, + 3,139,3242,8,139,1,140,1,140,1,140,5,140,3247,8,140,10,140,12,140, + 3250,9,140,1,141,1,141,1,141,5,141,3255,8,141,10,141,12,141,3258, + 9,141,1,142,1,142,1,142,1,142,1,142,3,142,3265,8,142,1,143,1,143, + 1,143,1,144,1,144,1,144,5,144,3273,8,144,10,144,12,144,3276,9,144, + 1,145,1,145,1,145,1,145,3,145,3282,8,145,1,145,3,145,3285,8,145, + 1,146,1,146,1,146,5,146,3290,8,146,10,146,12,146,3293,9,146,1,147, + 1,147,1,147,5,147,3298,8,147,10,147,12,147,3301,9,147,1,148,1,148, + 1,148,1,148,1,148,3,148,3308,8,148,1,149,1,149,1,149,1,149,1,149, + 1,149,1,149,1,150,1,150,1,150,5,150,3320,8,150,10,150,12,150,3323, + 9,150,1,151,1,151,3,151,3327,8,151,1,151,1,151,1,151,3,151,3332, + 8,151,1,151,3,151,3335,8,151,1,152,1,152,1,152,1,152,1,152,1,153, + 1,153,1,153,1,153,5,153,3346,8,153,10,153,12,153,3349,9,153,1,154, + 1,154,1,154,1,154,1,155,1,155,1,155,1,155,1,155,1,155,1,155,1,155, + 1,155,1,155,1,155,5,155,3366,8,155,10,155,12,155,3369,9,155,1,155, + 1,155,1,155,1,155,1,155,5,155,3376,8,155,10,155,12,155,3379,9,155, + 3,155,3381,8,155,1,155,1,155,1,155,1,155,1,155,5,155,3388,8,155, + 10,155,12,155,3391,9,155,3,155,3393,8,155,3,155,3395,8,155,1,155, + 3,155,3398,8,155,1,155,3,155,3401,8,155,1,156,1,156,1,156,1,156, + 1,156,1,156,1,156,1,156,1,156,1,156,1,156,1,156,1,156,1,156,1,156, + 1,156,3,156,3419,8,156,1,157,1,157,1,157,1,157,1,157,1,157,1,157, + 3,157,3428,8,157,1,158,1,158,1,158,5,158,3433,8,158,10,158,12,158, + 3436,9,158,1,159,1,159,1,159,1,159,3,159,3442,8,159,1,160,1,160, + 1,160,5,160,3447,8,160,10,160,12,160,3450,9,160,1,161,1,161,1,161, + 1,162,1,162,4,162,3457,8,162,11,162,12,162,3458,1,162,3,162,3462, + 8,162,1,163,1,163,3,163,3466,8,163,1,164,1,164,1,164,1,164,3,164, + 3472,8,164,1,165,1,165,1,166,1,166,1,167,3,167,3479,8,167,1,167, + 1,167,3,167,3483,8,167,1,167,1,167,3,167,3487,8,167,1,167,1,167, + 3,167,3491,8,167,1,167,1,167,3,167,3495,8,167,1,167,1,167,3,167, + 3499,8,167,1,167,1,167,3,167,3503,8,167,1,167,1,167,3,167,3507,8, + 167,1,167,1,167,3,167,3511,8,167,1,167,1,167,3,167,3515,8,167,1, + 167,3,167,3518,8,167,1,168,1,168,1,168,1,168,1,168,1,168,1,168,1, + 168,1,168,1,168,1,168,3,168,3531,8,168,1,169,1,169,1,170,1,170,3, + 170,3537,8,170,1,171,1,171,3,171,3541,8,171,1,172,1,172,1,173,1, + 173,1,174,1,174,1,174,9,1000,1067,1075,1092,1106,1115,1124,1133, + 1177,4,58,240,244,248,175,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28, + 30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72, + 74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112, + 114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144, + 146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176, + 178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208, + 210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240, + 242,244,246,248,250,252,254,256,258,260,262,264,266,268,270,272, + 274,276,278,280,282,284,286,288,290,292,294,296,298,300,302,304, + 306,308,310,312,314,316,318,320,322,324,326,328,330,332,334,336, + 338,340,342,344,346,348,0,59,2,0,69,69,202,202,2,0,30,30,219,219, + 2,0,107,107,122,122,1,0,43,44,2,0,258,258,296,296,2,0,11,11,35,35, + 5,0,40,40,52,52,93,93,106,106,152,152,1,0,74,75,2,0,93,93,106,106, + 3,0,8,8,82,82,255,255,2,0,8,8,146,146,3,0,65,65,166,166,231,231, + 3,0,66,66,167,167,232,232,4,0,87,87,130,130,240,240,284,284,2,0, + 21,21,74,74,2,0,101,101,137,137,2,0,257,257,295,295,2,0,256,256, + 267,267,2,0,55,55,226,226,2,0,89,89,123,123,2,0,10,10,79,79,2,0, + 335,335,337,337,1,0,142,143,3,0,10,10,16,16,244,244,3,0,96,96,277, + 277,286,286,2,0,316,317,321,321,2,0,81,81,318,320,2,0,316,317,324, + 324,11,0,61,61,63,63,117,117,157,157,159,159,161,161,163,163,204, + 204,229,229,298,298,305,305,3,0,57,57,59,60,292,292,2,0,67,67,268, + 268,2,0,68,68,269,269,2,0,32,32,279,279,2,0,120,120,218,218,1,0, + 253,254,2,0,4,4,107,107,2,0,4,4,103,103,3,0,25,25,140,140,272,272, + 1,0,193,194,1,0,308,315,2,0,81,81,316,325,4,0,14,14,122,122,172, + 172,181,181,2,0,96,96,277,277,1,0,316,317,7,0,61,62,117,118,157, + 164,168,169,229,230,298,299,305,306,6,0,61,61,117,117,161,161,163, + 163,229,229,305,305,2,0,163,163,305,305,4,0,61,61,117,117,161,161, + 229,229,3,0,117,117,161,161,229,229,2,0,80,80,190,190,2,0,182,182, + 245,245,2,0,102,102,199,199,2,0,331,331,342,342,1,0,336,337,2,0, + 82,82,239,239,1,0,330,331,51,0,8,9,11,13,15,15,17,19,21,22,24,24, + 26,30,33,35,37,40,42,42,44,50,52,52,55,56,61,78,80,82,86,86,88,95, + 98,98,100,102,105,106,109,112,115,115,117,121,123,125,127,129,131, + 131,134,134,136,137,139,139,142,169,171,171,174,175,179,180,183, + 183,185,186,188,192,195,199,201,210,212,220,222,232,234,237,239, + 243,245,257,259,264,267,269,271,271,273,283,287,291,294,299,302, + 302,305,307,16,0,15,15,54,54,87,87,108,108,126,126,130,130,135,135, + 138,138,141,141,170,170,177,177,221,221,234,234,240,240,284,284, + 293,293,17,0,8,14,16,53,55,86,88,107,109,125,127,129,131,134,136, + 137,139,140,142,169,171,176,178,220,222,233,235,239,241,283,285, + 292,294,307,4068,0,350,1,0,0,0,2,359,1,0,0,0,4,362,1,0,0,0,6,365, + 1,0,0,0,8,368,1,0,0,0,10,371,1,0,0,0,12,374,1,0,0,0,14,1180,1,0, + 0,0,16,1184,1,0,0,0,18,1186,1,0,0,0,20,1188,1,0,0,0,22,1358,1,0, + 0,0,24,1360,1,0,0,0,26,1377,1,0,0,0,28,1383,1,0,0,0,30,1395,1,0, + 0,0,32,1408,1,0,0,0,34,1411,1,0,0,0,36,1484,1,0,0,0,38,1486,1,0, + 0,0,40,1490,1,0,0,0,42,1511,1,0,0,0,44,1513,1,0,0,0,46,1515,1,0, + 0,0,48,1522,1,0,0,0,50,1524,1,0,0,0,52,1532,1,0,0,0,54,1542,1,0, + 0,0,56,1547,1,0,0,0,58,1558,1,0,0,0,60,1616,1,0,0,0,62,1627,1,0, + 0,0,64,1629,1,0,0,0,66,1647,1,0,0,0,68,1650,1,0,0,0,70,1661,1,0, + 0,0,72,1677,1,0,0,0,74,1683,1,0,0,0,76,1685,1,0,0,0,78,1696,1,0, + 0,0,80,1713,1,0,0,0,82,1721,1,0,0,0,84,1723,1,0,0,0,86,1729,1,0, + 0,0,88,1788,1,0,0,0,90,1800,1,0,0,0,92,1852,1,0,0,0,94,1855,1,0, + 0,0,96,1863,1,0,0,0,98,1896,1,0,0,0,100,1917,1,0,0,0,102,1949,1, + 0,0,0,104,1961,1,0,0,0,106,1964,1,0,0,0,108,1973,1,0,0,0,110,1987, + 1,0,0,0,112,2006,1,0,0,0,114,2026,1,0,0,0,116,2032,1,0,0,0,118,2034, + 1,0,0,0,120,2042,1,0,0,0,122,2046,1,0,0,0,124,2049,1,0,0,0,126,2052, + 1,0,0,0,128,2078,1,0,0,0,130,2080,1,0,0,0,132,2115,1,0,0,0,134,2156, + 1,0,0,0,136,2160,1,0,0,0,138,2187,1,0,0,0,140,2191,1,0,0,0,142,2206, + 1,0,0,0,144,2208,1,0,0,0,146,2238,1,0,0,0,148,2240,1,0,0,0,150,2247, + 1,0,0,0,152,2260,1,0,0,0,154,2265,1,0,0,0,156,2267,1,0,0,0,158,2282, + 1,0,0,0,160,2306,1,0,0,0,162,2319,1,0,0,0,164,2321,1,0,0,0,166,2323, + 1,0,0,0,168,2327,1,0,0,0,170,2330,1,0,0,0,172,2334,1,0,0,0,174,2366, + 1,0,0,0,176,2369,1,0,0,0,178,2381,1,0,0,0,180,2400,1,0,0,0,182,2426, + 1,0,0,0,184,2432,1,0,0,0,186,2434,1,0,0,0,188,2470,1,0,0,0,190,2472, + 1,0,0,0,192,2476,1,0,0,0,194,2484,1,0,0,0,196,2495,1,0,0,0,198,2499, + 1,0,0,0,200,2510,1,0,0,0,202,2541,1,0,0,0,204,2543,1,0,0,0,206,2554, + 1,0,0,0,208,2576,1,0,0,0,210,2627,1,0,0,0,212,2629,1,0,0,0,214,2637, + 1,0,0,0,216,2645,1,0,0,0,218,2653,1,0,0,0,220,2661,1,0,0,0,222,2668, + 1,0,0,0,224,2672,1,0,0,0,226,2682,1,0,0,0,228,2690,1,0,0,0,230,2703, + 1,0,0,0,232,2718,1,0,0,0,234,2722,1,0,0,0,236,2724,1,0,0,0,238,2726, + 1,0,0,0,240,2746,1,0,0,0,242,2841,1,0,0,0,244,2847,1,0,0,0,246,2873, + 1,0,0,0,248,3113,1,0,0,0,250,3142,1,0,0,0,252,3144,1,0,0,0,254,3146, + 1,0,0,0,256,3148,1,0,0,0,258,3150,1,0,0,0,260,3152,1,0,0,0,262,3157, + 1,0,0,0,264,3164,1,0,0,0,266,3168,1,0,0,0,268,3173,1,0,0,0,270,3179, + 1,0,0,0,272,3186,1,0,0,0,274,3188,1,0,0,0,276,3193,1,0,0,0,278,3241, + 1,0,0,0,280,3243,1,0,0,0,282,3251,1,0,0,0,284,3264,1,0,0,0,286,3266, + 1,0,0,0,288,3269,1,0,0,0,290,3277,1,0,0,0,292,3286,1,0,0,0,294,3294, + 1,0,0,0,296,3307,1,0,0,0,298,3309,1,0,0,0,300,3316,1,0,0,0,302,3324, + 1,0,0,0,304,3336,1,0,0,0,306,3341,1,0,0,0,308,3350,1,0,0,0,310,3400, + 1,0,0,0,312,3418,1,0,0,0,314,3427,1,0,0,0,316,3429,1,0,0,0,318,3441, + 1,0,0,0,320,3443,1,0,0,0,322,3451,1,0,0,0,324,3461,1,0,0,0,326,3465, + 1,0,0,0,328,3471,1,0,0,0,330,3473,1,0,0,0,332,3475,1,0,0,0,334,3517, + 1,0,0,0,336,3530,1,0,0,0,338,3532,1,0,0,0,340,3536,1,0,0,0,342,3540, + 1,0,0,0,344,3542,1,0,0,0,346,3544,1,0,0,0,348,3546,1,0,0,0,350,354, + 3,14,7,0,351,353,5,1,0,0,352,351,1,0,0,0,353,356,1,0,0,0,354,352, + 1,0,0,0,354,355,1,0,0,0,355,357,1,0,0,0,356,354,1,0,0,0,357,358, + 5,0,0,1,358,1,1,0,0,0,359,360,3,224,112,0,360,361,5,0,0,1,361,3, + 1,0,0,0,362,363,3,220,110,0,363,364,5,0,0,1,364,5,1,0,0,0,365,366, + 3,214,107,0,366,367,5,0,0,1,367,7,1,0,0,0,368,369,3,222,111,0,369, + 370,5,0,0,1,370,9,1,0,0,0,371,372,3,278,139,0,372,373,5,0,0,1,373, + 11,1,0,0,0,374,375,3,288,144,0,375,376,5,0,0,1,376,13,1,0,0,0,377, + 1181,3,54,27,0,378,380,3,52,26,0,379,378,1,0,0,0,379,380,1,0,0,0, + 380,381,1,0,0,0,381,1181,3,88,44,0,382,383,5,291,0,0,383,1181,3, + 214,107,0,384,385,5,291,0,0,385,386,3,44,22,0,386,387,3,214,107, + 0,387,1181,1,0,0,0,388,389,5,239,0,0,389,392,5,33,0,0,390,393,3, + 326,163,0,391,393,3,338,169,0,392,390,1,0,0,0,392,391,1,0,0,0,393, + 1181,1,0,0,0,394,395,5,53,0,0,395,399,3,44,22,0,396,397,5,119,0, + 0,397,398,5,172,0,0,398,400,5,90,0,0,399,396,1,0,0,0,399,400,1,0, + 0,0,400,401,1,0,0,0,401,409,3,214,107,0,402,408,3,34,17,0,403,408, + 3,32,16,0,404,405,5,303,0,0,405,406,7,0,0,0,406,408,3,68,34,0,407, + 402,1,0,0,0,407,403,1,0,0,0,407,404,1,0,0,0,408,411,1,0,0,0,409, + 407,1,0,0,0,409,410,1,0,0,0,410,1181,1,0,0,0,411,409,1,0,0,0,412, + 413,5,11,0,0,413,414,3,44,22,0,414,415,3,214,107,0,415,416,5,239, + 0,0,416,417,7,0,0,0,417,418,3,68,34,0,418,1181,1,0,0,0,419,420,5, + 11,0,0,420,421,3,44,22,0,421,422,3,214,107,0,422,423,5,239,0,0,423, + 424,3,32,16,0,424,1181,1,0,0,0,425,426,5,82,0,0,426,429,3,44,22, + 0,427,428,5,119,0,0,428,430,5,90,0,0,429,427,1,0,0,0,429,430,1,0, + 0,0,430,431,1,0,0,0,431,433,3,214,107,0,432,434,7,1,0,0,433,432, + 1,0,0,0,433,434,1,0,0,0,434,1181,1,0,0,0,435,436,5,242,0,0,436,439, + 3,46,23,0,437,438,7,2,0,0,438,440,3,214,107,0,439,437,1,0,0,0,439, + 440,1,0,0,0,440,445,1,0,0,0,441,443,5,142,0,0,442,441,1,0,0,0,442, + 443,1,0,0,0,443,444,1,0,0,0,444,446,3,338,169,0,445,442,1,0,0,0, + 445,446,1,0,0,0,446,1181,1,0,0,0,447,452,3,24,12,0,448,449,5,2,0, + 0,449,450,3,292,146,0,450,451,5,3,0,0,451,453,1,0,0,0,452,448,1, + 0,0,0,452,453,1,0,0,0,453,455,1,0,0,0,454,456,3,64,32,0,455,454, + 1,0,0,0,455,456,1,0,0,0,456,457,1,0,0,0,457,462,3,66,33,0,458,460, + 5,20,0,0,459,458,1,0,0,0,459,460,1,0,0,0,460,461,1,0,0,0,461,463, + 3,54,27,0,462,459,1,0,0,0,462,463,1,0,0,0,463,1181,1,0,0,0,464,465, + 5,53,0,0,465,469,5,258,0,0,466,467,5,119,0,0,467,468,5,172,0,0,468, + 470,5,90,0,0,469,466,1,0,0,0,469,470,1,0,0,0,470,471,1,0,0,0,471, + 472,3,220,110,0,472,473,5,142,0,0,473,482,3,220,110,0,474,481,3, + 64,32,0,475,481,3,210,105,0,476,481,3,80,40,0,477,481,3,32,16,0, + 478,479,5,262,0,0,479,481,3,68,34,0,480,474,1,0,0,0,480,475,1,0, + 0,0,480,476,1,0,0,0,480,477,1,0,0,0,480,478,1,0,0,0,481,484,1,0, + 0,0,482,480,1,0,0,0,482,483,1,0,0,0,483,1181,1,0,0,0,484,482,1,0, + 0,0,485,490,3,26,13,0,486,487,5,2,0,0,487,488,3,292,146,0,488,489, + 5,3,0,0,489,491,1,0,0,0,490,486,1,0,0,0,490,491,1,0,0,0,491,493, + 1,0,0,0,492,494,3,64,32,0,493,492,1,0,0,0,493,494,1,0,0,0,494,495, + 1,0,0,0,495,500,3,66,33,0,496,498,5,20,0,0,497,496,1,0,0,0,497,498, + 1,0,0,0,498,499,1,0,0,0,499,501,3,54,27,0,500,497,1,0,0,0,500,501, + 1,0,0,0,501,1181,1,0,0,0,502,503,5,13,0,0,503,504,5,258,0,0,504, + 506,3,214,107,0,505,507,3,40,20,0,506,505,1,0,0,0,506,507,1,0,0, + 0,507,508,1,0,0,0,508,509,5,49,0,0,509,517,5,249,0,0,510,518,3,326, + 163,0,511,512,5,103,0,0,512,513,5,44,0,0,513,518,3,192,96,0,514, + 515,5,103,0,0,515,516,5,10,0,0,516,518,5,44,0,0,517,510,1,0,0,0, + 517,511,1,0,0,0,517,514,1,0,0,0,517,518,1,0,0,0,518,1181,1,0,0,0, + 519,520,5,13,0,0,520,523,5,259,0,0,521,522,7,2,0,0,522,524,3,214, + 107,0,523,521,1,0,0,0,523,524,1,0,0,0,524,525,1,0,0,0,525,526,5, + 49,0,0,526,528,5,249,0,0,527,529,3,326,163,0,528,527,1,0,0,0,528, + 529,1,0,0,0,529,1181,1,0,0,0,530,531,5,11,0,0,531,532,5,258,0,0, + 532,533,3,214,107,0,533,534,5,8,0,0,534,535,7,3,0,0,535,536,3,280, + 140,0,536,1181,1,0,0,0,537,538,5,11,0,0,538,539,5,258,0,0,539,540, + 3,214,107,0,540,541,5,8,0,0,541,542,7,3,0,0,542,543,5,2,0,0,543, + 544,3,280,140,0,544,545,5,3,0,0,545,1181,1,0,0,0,546,547,5,11,0, + 0,547,548,5,258,0,0,548,549,3,214,107,0,549,550,5,213,0,0,550,551, + 5,43,0,0,551,552,3,214,107,0,552,553,5,270,0,0,553,554,3,322,161, + 0,554,1181,1,0,0,0,555,556,5,11,0,0,556,557,5,258,0,0,557,558,3, + 214,107,0,558,559,5,82,0,0,559,562,7,3,0,0,560,561,5,119,0,0,561, + 563,5,90,0,0,562,560,1,0,0,0,562,563,1,0,0,0,563,564,1,0,0,0,564, + 565,5,2,0,0,565,566,3,212,106,0,566,567,5,3,0,0,567,1181,1,0,0,0, + 568,569,5,11,0,0,569,570,5,258,0,0,570,571,3,214,107,0,571,572,5, + 82,0,0,572,575,7,3,0,0,573,574,5,119,0,0,574,576,5,90,0,0,575,573, + 1,0,0,0,575,576,1,0,0,0,576,577,1,0,0,0,577,578,3,212,106,0,578, + 1181,1,0,0,0,579,580,5,11,0,0,580,581,7,4,0,0,581,582,3,214,107, + 0,582,583,5,213,0,0,583,584,5,270,0,0,584,585,3,214,107,0,585,1181, + 1,0,0,0,586,587,5,11,0,0,587,588,7,4,0,0,588,589,3,214,107,0,589, + 590,5,239,0,0,590,591,5,262,0,0,591,592,3,68,34,0,592,1181,1,0,0, + 0,593,594,5,11,0,0,594,595,7,4,0,0,595,596,3,214,107,0,596,597,5, + 289,0,0,597,600,5,262,0,0,598,599,5,119,0,0,599,601,5,90,0,0,600, + 598,1,0,0,0,600,601,1,0,0,0,601,602,1,0,0,0,602,603,3,68,34,0,603, + 1181,1,0,0,0,604,605,5,11,0,0,605,606,5,258,0,0,606,607,3,214,107, + 0,607,609,7,5,0,0,608,610,5,43,0,0,609,608,1,0,0,0,609,610,1,0,0, + 0,610,611,1,0,0,0,611,613,3,214,107,0,612,614,3,336,168,0,613,612, + 1,0,0,0,613,614,1,0,0,0,614,1181,1,0,0,0,615,616,5,11,0,0,616,617, + 5,258,0,0,617,619,3,214,107,0,618,620,3,40,20,0,619,618,1,0,0,0, + 619,620,1,0,0,0,620,621,1,0,0,0,621,623,5,35,0,0,622,624,5,43,0, + 0,623,622,1,0,0,0,623,624,1,0,0,0,624,625,1,0,0,0,625,626,3,214, + 107,0,626,628,3,290,145,0,627,629,3,276,138,0,628,627,1,0,0,0,628, + 629,1,0,0,0,629,1181,1,0,0,0,630,631,5,11,0,0,631,632,5,258,0,0, + 632,634,3,214,107,0,633,635,3,40,20,0,634,633,1,0,0,0,634,635,1, + 0,0,0,635,636,1,0,0,0,636,637,5,216,0,0,637,638,5,44,0,0,638,639, + 5,2,0,0,639,640,3,280,140,0,640,641,5,3,0,0,641,1181,1,0,0,0,642, + 643,5,11,0,0,643,644,5,258,0,0,644,646,3,214,107,0,645,647,3,40, + 20,0,646,645,1,0,0,0,646,647,1,0,0,0,647,648,1,0,0,0,648,649,5,239, + 0,0,649,650,5,236,0,0,650,654,3,338,169,0,651,652,5,303,0,0,652, + 653,5,237,0,0,653,655,3,68,34,0,654,651,1,0,0,0,654,655,1,0,0,0, + 655,1181,1,0,0,0,656,657,5,11,0,0,657,658,5,258,0,0,658,660,3,214, + 107,0,659,661,3,40,20,0,660,659,1,0,0,0,660,661,1,0,0,0,661,662, + 1,0,0,0,662,663,5,239,0,0,663,664,5,237,0,0,664,665,3,68,34,0,665, + 1181,1,0,0,0,666,667,5,11,0,0,667,668,7,4,0,0,668,669,3,214,107, + 0,669,673,5,8,0,0,670,671,5,119,0,0,671,672,5,172,0,0,672,674,5, + 90,0,0,673,670,1,0,0,0,673,674,1,0,0,0,674,676,1,0,0,0,675,677,3, + 38,19,0,676,675,1,0,0,0,677,678,1,0,0,0,678,676,1,0,0,0,678,679, + 1,0,0,0,679,1181,1,0,0,0,680,681,5,11,0,0,681,682,5,258,0,0,682, + 683,3,214,107,0,683,684,3,40,20,0,684,685,5,213,0,0,685,686,5,270, + 0,0,686,687,3,40,20,0,687,1181,1,0,0,0,688,689,5,11,0,0,689,690, + 7,4,0,0,690,691,3,214,107,0,691,694,5,82,0,0,692,693,5,119,0,0,693, + 695,5,90,0,0,694,692,1,0,0,0,694,695,1,0,0,0,695,696,1,0,0,0,696, + 701,3,40,20,0,697,698,5,4,0,0,698,700,3,40,20,0,699,697,1,0,0,0, + 700,703,1,0,0,0,701,699,1,0,0,0,701,702,1,0,0,0,702,705,1,0,0,0, + 703,701,1,0,0,0,704,706,5,203,0,0,705,704,1,0,0,0,705,706,1,0,0, + 0,706,1181,1,0,0,0,707,708,5,11,0,0,708,709,5,258,0,0,709,711,3, + 214,107,0,710,712,3,40,20,0,711,710,1,0,0,0,711,712,1,0,0,0,712, + 713,1,0,0,0,713,714,5,239,0,0,714,715,3,32,16,0,715,1181,1,0,0,0, + 716,717,5,11,0,0,717,718,5,258,0,0,718,719,3,214,107,0,719,720,5, + 209,0,0,720,721,5,192,0,0,721,1181,1,0,0,0,722,723,5,82,0,0,723, + 726,5,258,0,0,724,725,5,119,0,0,725,727,5,90,0,0,726,724,1,0,0,0, + 726,727,1,0,0,0,727,728,1,0,0,0,728,730,3,214,107,0,729,731,5,203, + 0,0,730,729,1,0,0,0,730,731,1,0,0,0,731,1181,1,0,0,0,732,733,5,82, + 0,0,733,736,5,296,0,0,734,735,5,119,0,0,735,737,5,90,0,0,736,734, + 1,0,0,0,736,737,1,0,0,0,737,738,1,0,0,0,738,1181,3,214,107,0,739, + 742,5,53,0,0,740,741,5,181,0,0,741,743,5,216,0,0,742,740,1,0,0,0, + 742,743,1,0,0,0,743,748,1,0,0,0,744,746,5,112,0,0,745,744,1,0,0, + 0,745,746,1,0,0,0,746,747,1,0,0,0,747,749,5,263,0,0,748,745,1,0, + 0,0,748,749,1,0,0,0,749,750,1,0,0,0,750,754,5,296,0,0,751,752,5, + 119,0,0,752,753,5,172,0,0,753,755,5,90,0,0,754,751,1,0,0,0,754,755, + 1,0,0,0,755,756,1,0,0,0,756,758,3,214,107,0,757,759,3,198,99,0,758, + 757,1,0,0,0,758,759,1,0,0,0,759,768,1,0,0,0,760,767,3,34,17,0,761, + 762,5,191,0,0,762,763,5,177,0,0,763,767,3,190,95,0,764,765,5,262, + 0,0,765,767,3,68,34,0,766,760,1,0,0,0,766,761,1,0,0,0,766,764,1, + 0,0,0,767,770,1,0,0,0,768,766,1,0,0,0,768,769,1,0,0,0,769,771,1, + 0,0,0,770,768,1,0,0,0,771,772,5,20,0,0,772,773,3,54,27,0,773,1181, + 1,0,0,0,774,777,5,53,0,0,775,776,5,181,0,0,776,778,5,216,0,0,777, + 775,1,0,0,0,777,778,1,0,0,0,778,780,1,0,0,0,779,781,5,112,0,0,780, + 779,1,0,0,0,780,781,1,0,0,0,781,782,1,0,0,0,782,783,5,263,0,0,783, + 784,5,296,0,0,784,789,3,220,110,0,785,786,5,2,0,0,786,787,3,288, + 144,0,787,788,5,3,0,0,788,790,1,0,0,0,789,785,1,0,0,0,789,790,1, + 0,0,0,790,791,1,0,0,0,791,794,3,64,32,0,792,793,5,180,0,0,793,795, + 3,68,34,0,794,792,1,0,0,0,794,795,1,0,0,0,795,1181,1,0,0,0,796,797, + 5,11,0,0,797,798,5,296,0,0,798,800,3,214,107,0,799,801,5,20,0,0, + 800,799,1,0,0,0,800,801,1,0,0,0,801,802,1,0,0,0,802,803,3,54,27, + 0,803,1181,1,0,0,0,804,807,5,53,0,0,805,806,5,181,0,0,806,808,5, + 216,0,0,807,805,1,0,0,0,807,808,1,0,0,0,808,810,1,0,0,0,809,811, + 5,263,0,0,810,809,1,0,0,0,810,811,1,0,0,0,811,812,1,0,0,0,812,816, + 5,109,0,0,813,814,5,119,0,0,814,815,5,172,0,0,815,817,5,90,0,0,816, + 813,1,0,0,0,816,817,1,0,0,0,817,818,1,0,0,0,818,819,3,214,107,0, + 819,820,5,20,0,0,820,830,3,338,169,0,821,822,5,293,0,0,822,827,3, + 86,43,0,823,824,5,4,0,0,824,826,3,86,43,0,825,823,1,0,0,0,826,829, + 1,0,0,0,827,825,1,0,0,0,827,828,1,0,0,0,828,831,1,0,0,0,829,827, + 1,0,0,0,830,821,1,0,0,0,830,831,1,0,0,0,831,1181,1,0,0,0,832,834, + 5,82,0,0,833,835,5,263,0,0,834,833,1,0,0,0,834,835,1,0,0,0,835,836, + 1,0,0,0,836,839,5,109,0,0,837,838,5,119,0,0,838,840,5,90,0,0,839, + 837,1,0,0,0,839,840,1,0,0,0,840,841,1,0,0,0,841,1181,3,214,107,0, + 842,844,5,91,0,0,843,845,7,6,0,0,844,843,1,0,0,0,844,845,1,0,0,0, + 845,846,1,0,0,0,846,1181,3,14,7,0,847,848,5,242,0,0,848,851,5,259, + 0,0,849,850,7,2,0,0,850,852,3,214,107,0,851,849,1,0,0,0,851,852, + 1,0,0,0,852,857,1,0,0,0,853,855,5,142,0,0,854,853,1,0,0,0,854,855, + 1,0,0,0,855,856,1,0,0,0,856,858,3,338,169,0,857,854,1,0,0,0,857, + 858,1,0,0,0,858,1181,1,0,0,0,859,860,5,242,0,0,860,861,5,258,0,0, + 861,864,5,93,0,0,862,863,7,2,0,0,863,865,3,214,107,0,864,862,1,0, + 0,0,864,865,1,0,0,0,865,866,1,0,0,0,866,867,5,142,0,0,867,869,3, + 338,169,0,868,870,3,40,20,0,869,868,1,0,0,0,869,870,1,0,0,0,870, + 1181,1,0,0,0,871,872,5,242,0,0,872,873,5,262,0,0,873,878,3,214,107, + 0,874,875,5,2,0,0,875,876,3,72,36,0,876,877,5,3,0,0,877,879,1,0, + 0,0,878,874,1,0,0,0,878,879,1,0,0,0,879,1181,1,0,0,0,880,881,5,242, + 0,0,881,882,5,44,0,0,882,883,7,2,0,0,883,886,3,214,107,0,884,885, + 7,2,0,0,885,887,3,214,107,0,886,884,1,0,0,0,886,887,1,0,0,0,887, + 1181,1,0,0,0,888,889,5,242,0,0,889,892,5,297,0,0,890,891,7,2,0,0, + 891,893,3,214,107,0,892,890,1,0,0,0,892,893,1,0,0,0,893,898,1,0, + 0,0,894,896,5,142,0,0,895,894,1,0,0,0,895,896,1,0,0,0,896,897,1, + 0,0,0,897,899,3,338,169,0,898,895,1,0,0,0,898,899,1,0,0,0,899,1181, + 1,0,0,0,900,901,5,242,0,0,901,902,5,192,0,0,902,904,3,214,107,0, + 903,905,3,40,20,0,904,903,1,0,0,0,904,905,1,0,0,0,905,1181,1,0,0, + 0,906,908,5,242,0,0,907,909,3,326,163,0,908,907,1,0,0,0,908,909, + 1,0,0,0,909,910,1,0,0,0,910,913,5,110,0,0,911,912,7,2,0,0,912,914, + 3,214,107,0,913,911,1,0,0,0,913,914,1,0,0,0,914,922,1,0,0,0,915, + 917,5,142,0,0,916,915,1,0,0,0,916,917,1,0,0,0,917,920,1,0,0,0,918, + 921,3,214,107,0,919,921,3,338,169,0,920,918,1,0,0,0,920,919,1,0, + 0,0,921,923,1,0,0,0,922,916,1,0,0,0,922,923,1,0,0,0,923,1181,1,0, + 0,0,924,925,5,242,0,0,925,926,5,53,0,0,926,927,5,258,0,0,927,930, + 3,214,107,0,928,929,5,20,0,0,929,931,5,236,0,0,930,928,1,0,0,0,930, + 931,1,0,0,0,931,1181,1,0,0,0,932,933,5,242,0,0,933,934,5,56,0,0, + 934,1181,3,44,22,0,935,936,5,242,0,0,936,941,5,34,0,0,937,939,5, + 142,0,0,938,937,1,0,0,0,938,939,1,0,0,0,939,940,1,0,0,0,940,942, + 3,338,169,0,941,938,1,0,0,0,941,942,1,0,0,0,942,1181,1,0,0,0,943, + 944,7,7,0,0,944,946,5,109,0,0,945,947,5,93,0,0,946,945,1,0,0,0,946, + 947,1,0,0,0,947,948,1,0,0,0,948,1181,3,48,24,0,949,950,7,7,0,0,950, + 952,3,44,22,0,951,953,5,93,0,0,952,951,1,0,0,0,952,953,1,0,0,0,953, + 954,1,0,0,0,954,955,3,214,107,0,955,1181,1,0,0,0,956,958,7,7,0,0, + 957,959,5,258,0,0,958,957,1,0,0,0,958,959,1,0,0,0,959,961,1,0,0, + 0,960,962,7,8,0,0,961,960,1,0,0,0,961,962,1,0,0,0,962,963,1,0,0, + 0,963,965,3,214,107,0,964,966,3,40,20,0,965,964,1,0,0,0,965,966, + 1,0,0,0,966,968,1,0,0,0,967,969,3,50,25,0,968,967,1,0,0,0,968,969, + 1,0,0,0,969,1181,1,0,0,0,970,972,7,7,0,0,971,973,5,205,0,0,972,971, + 1,0,0,0,972,973,1,0,0,0,973,974,1,0,0,0,974,1181,3,54,27,0,975,976, + 5,45,0,0,976,977,5,177,0,0,977,978,3,44,22,0,978,979,3,214,107,0, + 979,980,5,133,0,0,980,981,3,340,170,0,981,1181,1,0,0,0,982,983,5, + 45,0,0,983,984,5,177,0,0,984,985,5,258,0,0,985,986,3,214,107,0,986, + 987,5,133,0,0,987,988,3,340,170,0,988,1181,1,0,0,0,989,990,5,212, + 0,0,990,991,5,258,0,0,991,1181,3,214,107,0,992,993,5,212,0,0,993, + 994,5,109,0,0,994,1181,3,214,107,0,995,1003,5,212,0,0,996,1004,3, + 338,169,0,997,999,9,0,0,0,998,997,1,0,0,0,999,1002,1,0,0,0,1000, + 1001,1,0,0,0,1000,998,1,0,0,0,1001,1004,1,0,0,0,1002,1000,1,0,0, + 0,1003,996,1,0,0,0,1003,1000,1,0,0,0,1004,1181,1,0,0,0,1005,1007, + 5,29,0,0,1006,1008,5,139,0,0,1007,1006,1,0,0,0,1007,1008,1,0,0,0, + 1008,1009,1,0,0,0,1009,1010,5,258,0,0,1010,1013,3,214,107,0,1011, + 1012,5,180,0,0,1012,1014,3,68,34,0,1013,1011,1,0,0,0,1013,1014,1, + 0,0,0,1014,1019,1,0,0,0,1015,1017,5,20,0,0,1016,1015,1,0,0,0,1016, + 1017,1,0,0,0,1017,1018,1,0,0,0,1018,1020,3,54,27,0,1019,1016,1,0, + 0,0,1019,1020,1,0,0,0,1020,1181,1,0,0,0,1021,1022,5,283,0,0,1022, + 1025,5,258,0,0,1023,1024,5,119,0,0,1024,1026,5,90,0,0,1025,1023, + 1,0,0,0,1025,1026,1,0,0,0,1026,1027,1,0,0,0,1027,1181,3,214,107, + 0,1028,1029,5,37,0,0,1029,1181,5,29,0,0,1030,1031,5,147,0,0,1031, + 1033,5,64,0,0,1032,1034,5,148,0,0,1033,1032,1,0,0,0,1033,1034,1, + 0,0,0,1034,1035,1,0,0,0,1035,1036,5,127,0,0,1036,1038,3,338,169, + 0,1037,1039,5,189,0,0,1038,1037,1,0,0,0,1038,1039,1,0,0,0,1039,1040, + 1,0,0,0,1040,1041,5,132,0,0,1041,1042,5,258,0,0,1042,1044,3,214, + 107,0,1043,1045,3,40,20,0,1044,1043,1,0,0,0,1044,1045,1,0,0,0,1045, + 1181,1,0,0,0,1046,1047,5,278,0,0,1047,1048,5,258,0,0,1048,1050,3, + 214,107,0,1049,1051,3,40,20,0,1050,1049,1,0,0,0,1050,1051,1,0,0, + 0,1051,1181,1,0,0,0,1052,1054,5,165,0,0,1053,1052,1,0,0,0,1053,1054, + 1,0,0,0,1054,1055,1,0,0,0,1055,1056,5,214,0,0,1056,1057,5,258,0, + 0,1057,1060,3,214,107,0,1058,1059,7,9,0,0,1059,1061,5,192,0,0,1060, + 1058,1,0,0,0,1060,1061,1,0,0,0,1061,1181,1,0,0,0,1062,1063,7,10, + 0,0,1063,1067,3,326,163,0,1064,1066,9,0,0,0,1065,1064,1,0,0,0,1066, + 1069,1,0,0,0,1067,1068,1,0,0,0,1067,1065,1,0,0,0,1068,1181,1,0,0, + 0,1069,1067,1,0,0,0,1070,1071,5,239,0,0,1071,1075,5,223,0,0,1072, + 1074,9,0,0,0,1073,1072,1,0,0,0,1074,1077,1,0,0,0,1075,1076,1,0,0, + 0,1075,1073,1,0,0,0,1076,1181,1,0,0,0,1077,1075,1,0,0,0,1078,1079, + 5,239,0,0,1079,1080,5,266,0,0,1080,1081,5,307,0,0,1081,1181,3,260, + 130,0,1082,1083,5,239,0,0,1083,1084,5,266,0,0,1084,1085,5,307,0, + 0,1085,1181,3,16,8,0,1086,1087,5,239,0,0,1087,1088,5,266,0,0,1088, + 1092,5,307,0,0,1089,1091,9,0,0,0,1090,1089,1,0,0,0,1091,1094,1,0, + 0,0,1092,1093,1,0,0,0,1092,1090,1,0,0,0,1093,1181,1,0,0,0,1094,1092, + 1,0,0,0,1095,1096,5,239,0,0,1096,1097,3,18,9,0,1097,1098,5,308,0, + 0,1098,1099,3,20,10,0,1099,1181,1,0,0,0,1100,1101,5,239,0,0,1101, + 1109,3,18,9,0,1102,1106,5,308,0,0,1103,1105,9,0,0,0,1104,1103,1, + 0,0,0,1105,1108,1,0,0,0,1106,1107,1,0,0,0,1106,1104,1,0,0,0,1107, + 1110,1,0,0,0,1108,1106,1,0,0,0,1109,1102,1,0,0,0,1109,1110,1,0,0, + 0,1110,1181,1,0,0,0,1111,1115,5,239,0,0,1112,1114,9,0,0,0,1113,1112, + 1,0,0,0,1114,1117,1,0,0,0,1115,1116,1,0,0,0,1115,1113,1,0,0,0,1116, + 1118,1,0,0,0,1117,1115,1,0,0,0,1118,1119,5,308,0,0,1119,1181,3,20, + 10,0,1120,1124,5,239,0,0,1121,1123,9,0,0,0,1122,1121,1,0,0,0,1123, + 1126,1,0,0,0,1124,1125,1,0,0,0,1124,1122,1,0,0,0,1125,1181,1,0,0, + 0,1126,1124,1,0,0,0,1127,1128,5,217,0,0,1128,1181,3,18,9,0,1129, + 1133,5,217,0,0,1130,1132,9,0,0,0,1131,1130,1,0,0,0,1132,1135,1,0, + 0,0,1133,1134,1,0,0,0,1133,1131,1,0,0,0,1134,1181,1,0,0,0,1135,1133, + 1,0,0,0,1136,1137,5,53,0,0,1137,1141,5,124,0,0,1138,1139,5,119,0, + 0,1139,1140,5,172,0,0,1140,1142,5,90,0,0,1141,1138,1,0,0,0,1141, + 1142,1,0,0,0,1142,1143,1,0,0,0,1143,1144,3,326,163,0,1144,1146,5, + 177,0,0,1145,1147,5,258,0,0,1146,1145,1,0,0,0,1146,1147,1,0,0,0, + 1147,1148,1,0,0,0,1148,1151,3,214,107,0,1149,1150,5,293,0,0,1150, + 1152,3,326,163,0,1151,1149,1,0,0,0,1151,1152,1,0,0,0,1152,1153,1, + 0,0,0,1153,1154,5,2,0,0,1154,1155,3,216,108,0,1155,1158,5,3,0,0, + 1156,1157,5,180,0,0,1157,1159,3,68,34,0,1158,1156,1,0,0,0,1158,1159, + 1,0,0,0,1159,1181,1,0,0,0,1160,1161,5,82,0,0,1161,1164,5,124,0,0, + 1162,1163,5,119,0,0,1163,1165,5,90,0,0,1164,1162,1,0,0,0,1164,1165, + 1,0,0,0,1165,1166,1,0,0,0,1166,1167,3,326,163,0,1167,1169,5,177, + 0,0,1168,1170,5,258,0,0,1169,1168,1,0,0,0,1169,1170,1,0,0,0,1170, + 1171,1,0,0,0,1171,1172,3,214,107,0,1172,1181,1,0,0,0,1173,1177,3, + 22,11,0,1174,1176,9,0,0,0,1175,1174,1,0,0,0,1176,1179,1,0,0,0,1177, + 1178,1,0,0,0,1177,1175,1,0,0,0,1178,1181,1,0,0,0,1179,1177,1,0,0, + 0,1180,377,1,0,0,0,1180,379,1,0,0,0,1180,382,1,0,0,0,1180,384,1, + 0,0,0,1180,388,1,0,0,0,1180,394,1,0,0,0,1180,412,1,0,0,0,1180,419, + 1,0,0,0,1180,425,1,0,0,0,1180,435,1,0,0,0,1180,447,1,0,0,0,1180, + 464,1,0,0,0,1180,485,1,0,0,0,1180,502,1,0,0,0,1180,519,1,0,0,0,1180, + 530,1,0,0,0,1180,537,1,0,0,0,1180,546,1,0,0,0,1180,555,1,0,0,0,1180, + 568,1,0,0,0,1180,579,1,0,0,0,1180,586,1,0,0,0,1180,593,1,0,0,0,1180, + 604,1,0,0,0,1180,615,1,0,0,0,1180,630,1,0,0,0,1180,642,1,0,0,0,1180, + 656,1,0,0,0,1180,666,1,0,0,0,1180,680,1,0,0,0,1180,688,1,0,0,0,1180, + 707,1,0,0,0,1180,716,1,0,0,0,1180,722,1,0,0,0,1180,732,1,0,0,0,1180, + 739,1,0,0,0,1180,774,1,0,0,0,1180,796,1,0,0,0,1180,804,1,0,0,0,1180, + 832,1,0,0,0,1180,842,1,0,0,0,1180,847,1,0,0,0,1180,859,1,0,0,0,1180, + 871,1,0,0,0,1180,880,1,0,0,0,1180,888,1,0,0,0,1180,900,1,0,0,0,1180, + 906,1,0,0,0,1180,924,1,0,0,0,1180,932,1,0,0,0,1180,935,1,0,0,0,1180, + 943,1,0,0,0,1180,949,1,0,0,0,1180,956,1,0,0,0,1180,970,1,0,0,0,1180, + 975,1,0,0,0,1180,982,1,0,0,0,1180,989,1,0,0,0,1180,992,1,0,0,0,1180, + 995,1,0,0,0,1180,1005,1,0,0,0,1180,1021,1,0,0,0,1180,1028,1,0,0, + 0,1180,1030,1,0,0,0,1180,1046,1,0,0,0,1180,1053,1,0,0,0,1180,1062, + 1,0,0,0,1180,1070,1,0,0,0,1180,1078,1,0,0,0,1180,1082,1,0,0,0,1180, + 1086,1,0,0,0,1180,1095,1,0,0,0,1180,1100,1,0,0,0,1180,1111,1,0,0, + 0,1180,1120,1,0,0,0,1180,1127,1,0,0,0,1180,1129,1,0,0,0,1180,1136, + 1,0,0,0,1180,1160,1,0,0,0,1180,1173,1,0,0,0,1181,15,1,0,0,0,1182, + 1185,3,338,169,0,1183,1185,5,148,0,0,1184,1182,1,0,0,0,1184,1183, + 1,0,0,0,1185,17,1,0,0,0,1186,1187,3,330,165,0,1187,19,1,0,0,0,1188, + 1189,3,332,166,0,1189,21,1,0,0,0,1190,1191,5,53,0,0,1191,1359,5, + 223,0,0,1192,1193,5,82,0,0,1193,1359,5,223,0,0,1194,1196,5,113,0, + 0,1195,1197,5,223,0,0,1196,1195,1,0,0,0,1196,1197,1,0,0,0,1197,1359, + 1,0,0,0,1198,1200,5,220,0,0,1199,1201,5,223,0,0,1200,1199,1,0,0, + 0,1200,1201,1,0,0,0,1201,1359,1,0,0,0,1202,1203,5,242,0,0,1203,1359, + 5,113,0,0,1204,1205,5,242,0,0,1205,1207,5,223,0,0,1206,1208,5,113, + 0,0,1207,1206,1,0,0,0,1207,1208,1,0,0,0,1208,1359,1,0,0,0,1209,1210, + 5,242,0,0,1210,1359,5,201,0,0,1211,1212,5,242,0,0,1212,1359,5,224, + 0,0,1213,1214,5,242,0,0,1214,1215,5,56,0,0,1215,1359,5,224,0,0,1216, + 1217,5,92,0,0,1217,1359,5,258,0,0,1218,1219,5,121,0,0,1219,1359, + 5,258,0,0,1220,1221,5,242,0,0,1221,1359,5,48,0,0,1222,1223,5,242, + 0,0,1223,1224,5,53,0,0,1224,1359,5,258,0,0,1225,1226,5,242,0,0,1226, + 1359,5,274,0,0,1227,1228,5,242,0,0,1228,1359,5,125,0,0,1229,1230, + 5,242,0,0,1230,1359,5,151,0,0,1231,1232,5,53,0,0,1232,1359,5,124, + 0,0,1233,1234,5,82,0,0,1234,1359,5,124,0,0,1235,1236,5,11,0,0,1236, + 1359,5,124,0,0,1237,1238,5,150,0,0,1238,1359,5,258,0,0,1239,1240, + 5,150,0,0,1240,1359,5,65,0,0,1241,1242,5,287,0,0,1242,1359,5,258, + 0,0,1243,1244,5,287,0,0,1244,1359,5,65,0,0,1245,1246,5,53,0,0,1246, + 1247,5,263,0,0,1247,1359,5,153,0,0,1248,1249,5,82,0,0,1249,1250, + 5,263,0,0,1250,1359,5,153,0,0,1251,1252,5,11,0,0,1252,1253,5,258, + 0,0,1253,1254,3,220,110,0,1254,1255,5,172,0,0,1255,1256,5,39,0,0, + 1256,1359,1,0,0,0,1257,1258,5,11,0,0,1258,1259,5,258,0,0,1259,1260, + 3,220,110,0,1260,1261,5,39,0,0,1261,1262,5,28,0,0,1262,1359,1,0, + 0,0,1263,1264,5,11,0,0,1264,1265,5,258,0,0,1265,1266,3,220,110,0, + 1266,1267,5,172,0,0,1267,1268,5,246,0,0,1268,1359,1,0,0,0,1269,1270, + 5,11,0,0,1270,1271,5,258,0,0,1271,1272,3,220,110,0,1272,1273,5,243, + 0,0,1273,1274,5,28,0,0,1274,1359,1,0,0,0,1275,1276,5,11,0,0,1276, + 1277,5,258,0,0,1277,1278,3,220,110,0,1278,1279,5,172,0,0,1279,1280, + 5,243,0,0,1280,1359,1,0,0,0,1281,1282,5,11,0,0,1282,1283,5,258,0, + 0,1283,1284,3,220,110,0,1284,1285,5,172,0,0,1285,1286,5,250,0,0, + 1286,1287,5,20,0,0,1287,1288,5,77,0,0,1288,1359,1,0,0,0,1289,1290, + 5,11,0,0,1290,1291,5,258,0,0,1291,1292,3,220,110,0,1292,1293,5,239, + 0,0,1293,1294,5,243,0,0,1294,1295,5,149,0,0,1295,1359,1,0,0,0,1296, + 1297,5,11,0,0,1297,1298,5,258,0,0,1298,1299,3,220,110,0,1299,1300, + 5,88,0,0,1300,1301,5,190,0,0,1301,1359,1,0,0,0,1302,1303,5,11,0, + 0,1303,1304,5,258,0,0,1304,1305,3,220,110,0,1305,1306,5,18,0,0,1306, + 1307,5,190,0,0,1307,1359,1,0,0,0,1308,1309,5,11,0,0,1309,1310,5, + 258,0,0,1310,1311,3,220,110,0,1311,1312,5,281,0,0,1312,1313,5,190, + 0,0,1313,1359,1,0,0,0,1314,1315,5,11,0,0,1315,1316,5,258,0,0,1316, + 1317,3,220,110,0,1317,1318,5,271,0,0,1318,1359,1,0,0,0,1319,1320, + 5,11,0,0,1320,1321,5,258,0,0,1321,1323,3,220,110,0,1322,1324,3,40, + 20,0,1323,1322,1,0,0,0,1323,1324,1,0,0,0,1324,1325,1,0,0,0,1325, + 1326,5,47,0,0,1326,1359,1,0,0,0,1327,1328,5,11,0,0,1328,1329,5,258, + 0,0,1329,1331,3,220,110,0,1330,1332,3,40,20,0,1331,1330,1,0,0,0, + 1331,1332,1,0,0,0,1332,1333,1,0,0,0,1333,1334,5,50,0,0,1334,1359, + 1,0,0,0,1335,1336,5,11,0,0,1336,1337,5,258,0,0,1337,1339,3,220,110, + 0,1338,1340,3,40,20,0,1339,1338,1,0,0,0,1339,1340,1,0,0,0,1340,1341, + 1,0,0,0,1341,1342,5,239,0,0,1342,1343,5,100,0,0,1343,1359,1,0,0, + 0,1344,1345,5,11,0,0,1345,1346,5,258,0,0,1346,1348,3,220,110,0,1347, + 1349,3,40,20,0,1348,1347,1,0,0,0,1348,1349,1,0,0,0,1349,1350,1,0, + 0,0,1350,1351,5,216,0,0,1351,1352,5,44,0,0,1352,1359,1,0,0,0,1353, + 1354,5,248,0,0,1354,1359,5,273,0,0,1355,1359,5,46,0,0,1356,1359, + 5,225,0,0,1357,1359,5,76,0,0,1358,1190,1,0,0,0,1358,1192,1,0,0,0, + 1358,1194,1,0,0,0,1358,1198,1,0,0,0,1358,1202,1,0,0,0,1358,1204, + 1,0,0,0,1358,1209,1,0,0,0,1358,1211,1,0,0,0,1358,1213,1,0,0,0,1358, + 1216,1,0,0,0,1358,1218,1,0,0,0,1358,1220,1,0,0,0,1358,1222,1,0,0, + 0,1358,1225,1,0,0,0,1358,1227,1,0,0,0,1358,1229,1,0,0,0,1358,1231, + 1,0,0,0,1358,1233,1,0,0,0,1358,1235,1,0,0,0,1358,1237,1,0,0,0,1358, + 1239,1,0,0,0,1358,1241,1,0,0,0,1358,1243,1,0,0,0,1358,1245,1,0,0, + 0,1358,1248,1,0,0,0,1358,1251,1,0,0,0,1358,1257,1,0,0,0,1358,1263, + 1,0,0,0,1358,1269,1,0,0,0,1358,1275,1,0,0,0,1358,1281,1,0,0,0,1358, + 1289,1,0,0,0,1358,1296,1,0,0,0,1358,1302,1,0,0,0,1358,1308,1,0,0, + 0,1358,1314,1,0,0,0,1358,1319,1,0,0,0,1358,1327,1,0,0,0,1358,1335, + 1,0,0,0,1358,1344,1,0,0,0,1358,1353,1,0,0,0,1358,1355,1,0,0,0,1358, + 1356,1,0,0,0,1358,1357,1,0,0,0,1359,23,1,0,0,0,1360,1362,5,53,0, + 0,1361,1363,5,263,0,0,1362,1361,1,0,0,0,1362,1363,1,0,0,0,1363,1365, + 1,0,0,0,1364,1366,5,94,0,0,1365,1364,1,0,0,0,1365,1366,1,0,0,0,1366, + 1367,1,0,0,0,1367,1371,5,258,0,0,1368,1369,5,119,0,0,1369,1370,5, + 172,0,0,1370,1372,5,90,0,0,1371,1368,1,0,0,0,1371,1372,1,0,0,0,1372, + 1373,1,0,0,0,1373,1374,3,214,107,0,1374,25,1,0,0,0,1375,1376,5,53, + 0,0,1376,1378,5,181,0,0,1377,1375,1,0,0,0,1377,1378,1,0,0,0,1378, + 1379,1,0,0,0,1379,1380,5,216,0,0,1380,1381,5,258,0,0,1381,1382,3, + 214,107,0,1382,27,1,0,0,0,1383,1384,5,39,0,0,1384,1385,5,28,0,0, + 1385,1389,3,190,95,0,1386,1387,5,246,0,0,1387,1388,5,28,0,0,1388, + 1390,3,194,97,0,1389,1386,1,0,0,0,1389,1390,1,0,0,0,1390,1391,1, + 0,0,0,1391,1392,5,132,0,0,1392,1393,5,335,0,0,1393,1394,5,27,0,0, + 1394,29,1,0,0,0,1395,1396,5,243,0,0,1396,1397,5,28,0,0,1397,1398, + 3,190,95,0,1398,1401,5,177,0,0,1399,1402,3,76,38,0,1400,1402,3,78, + 39,0,1401,1399,1,0,0,0,1401,1400,1,0,0,0,1402,1406,1,0,0,0,1403, + 1404,5,250,0,0,1404,1405,5,20,0,0,1405,1407,5,77,0,0,1406,1403,1, + 0,0,0,1406,1407,1,0,0,0,1407,31,1,0,0,0,1408,1409,5,149,0,0,1409, + 1410,3,338,169,0,1410,33,1,0,0,0,1411,1412,5,45,0,0,1412,1413,3, + 338,169,0,1413,35,1,0,0,0,1414,1415,5,129,0,0,1415,1417,5,189,0, + 0,1416,1418,5,258,0,0,1417,1416,1,0,0,0,1417,1418,1,0,0,0,1418,1419, + 1,0,0,0,1419,1426,3,214,107,0,1420,1424,3,40,20,0,1421,1422,5,119, + 0,0,1422,1423,5,172,0,0,1423,1425,5,90,0,0,1424,1421,1,0,0,0,1424, + 1425,1,0,0,0,1425,1427,1,0,0,0,1426,1420,1,0,0,0,1426,1427,1,0,0, + 0,1427,1429,1,0,0,0,1428,1430,3,190,95,0,1429,1428,1,0,0,0,1429, + 1430,1,0,0,0,1430,1485,1,0,0,0,1431,1432,5,129,0,0,1432,1434,5,132, + 0,0,1433,1435,5,258,0,0,1434,1433,1,0,0,0,1434,1435,1,0,0,0,1435, + 1436,1,0,0,0,1436,1438,3,214,107,0,1437,1439,3,40,20,0,1438,1437, + 1,0,0,0,1438,1439,1,0,0,0,1439,1443,1,0,0,0,1440,1441,5,119,0,0, + 1441,1442,5,172,0,0,1442,1444,5,90,0,0,1443,1440,1,0,0,0,1443,1444, + 1,0,0,0,1444,1446,1,0,0,0,1445,1447,3,190,95,0,1446,1445,1,0,0,0, + 1446,1447,1,0,0,0,1447,1485,1,0,0,0,1448,1449,5,129,0,0,1449,1451, + 5,132,0,0,1450,1452,5,258,0,0,1451,1450,1,0,0,0,1451,1452,1,0,0, + 0,1452,1453,1,0,0,0,1453,1454,3,214,107,0,1454,1455,5,216,0,0,1455, + 1456,3,122,61,0,1456,1485,1,0,0,0,1457,1458,5,129,0,0,1458,1460, + 5,189,0,0,1459,1461,5,148,0,0,1460,1459,1,0,0,0,1460,1461,1,0,0, + 0,1461,1462,1,0,0,0,1462,1463,5,78,0,0,1463,1465,3,338,169,0,1464, + 1466,3,210,105,0,1465,1464,1,0,0,0,1465,1466,1,0,0,0,1466,1468,1, + 0,0,0,1467,1469,3,80,40,0,1468,1467,1,0,0,0,1468,1469,1,0,0,0,1469, + 1485,1,0,0,0,1470,1471,5,129,0,0,1471,1473,5,189,0,0,1472,1474,5, + 148,0,0,1473,1472,1,0,0,0,1473,1474,1,0,0,0,1474,1475,1,0,0,0,1475, + 1477,5,78,0,0,1476,1478,3,338,169,0,1477,1476,1,0,0,0,1477,1478, + 1,0,0,0,1478,1479,1,0,0,0,1479,1482,3,64,32,0,1480,1481,5,180,0, + 0,1481,1483,3,68,34,0,1482,1480,1,0,0,0,1482,1483,1,0,0,0,1483,1485, + 1,0,0,0,1484,1414,1,0,0,0,1484,1431,1,0,0,0,1484,1448,1,0,0,0,1484, + 1457,1,0,0,0,1484,1470,1,0,0,0,1485,37,1,0,0,0,1486,1488,3,40,20, + 0,1487,1489,3,32,16,0,1488,1487,1,0,0,0,1488,1489,1,0,0,0,1489,39, + 1,0,0,0,1490,1491,5,190,0,0,1491,1492,5,2,0,0,1492,1497,3,42,21, + 0,1493,1494,5,4,0,0,1494,1496,3,42,21,0,1495,1493,1,0,0,0,1496,1499, + 1,0,0,0,1497,1495,1,0,0,0,1497,1498,1,0,0,0,1498,1500,1,0,0,0,1499, + 1497,1,0,0,0,1500,1501,5,3,0,0,1501,41,1,0,0,0,1502,1505,3,326,163, + 0,1503,1504,5,308,0,0,1504,1506,3,250,125,0,1505,1503,1,0,0,0,1505, + 1506,1,0,0,0,1506,1512,1,0,0,0,1507,1508,3,326,163,0,1508,1509,5, + 308,0,0,1509,1510,5,70,0,0,1510,1512,1,0,0,0,1511,1502,1,0,0,0,1511, + 1507,1,0,0,0,1512,43,1,0,0,0,1513,1514,7,11,0,0,1514,45,1,0,0,0, + 1515,1516,7,12,0,0,1516,47,1,0,0,0,1517,1523,3,320,160,0,1518,1523, + 3,338,169,0,1519,1523,3,252,126,0,1520,1523,3,254,127,0,1521,1523, + 3,256,128,0,1522,1517,1,0,0,0,1522,1518,1,0,0,0,1522,1519,1,0,0, + 0,1522,1520,1,0,0,0,1522,1521,1,0,0,0,1523,49,1,0,0,0,1524,1529, + 3,326,163,0,1525,1526,5,5,0,0,1526,1528,3,326,163,0,1527,1525,1, + 0,0,0,1528,1531,1,0,0,0,1529,1527,1,0,0,0,1529,1530,1,0,0,0,1530, + 51,1,0,0,0,1531,1529,1,0,0,0,1532,1533,5,303,0,0,1533,1538,3,56, + 28,0,1534,1535,5,4,0,0,1535,1537,3,56,28,0,1536,1534,1,0,0,0,1537, + 1540,1,0,0,0,1538,1536,1,0,0,0,1538,1539,1,0,0,0,1539,53,1,0,0,0, + 1540,1538,1,0,0,0,1541,1543,3,52,26,0,1542,1541,1,0,0,0,1542,1543, + 1,0,0,0,1543,1544,1,0,0,0,1544,1545,3,58,29,0,1545,1546,3,90,45, + 0,1546,55,1,0,0,0,1547,1549,3,322,161,0,1548,1550,3,190,95,0,1549, + 1548,1,0,0,0,1549,1550,1,0,0,0,1550,1552,1,0,0,0,1551,1553,5,20, + 0,0,1552,1551,1,0,0,0,1552,1553,1,0,0,0,1553,1554,1,0,0,0,1554,1555, + 5,2,0,0,1555,1556,3,54,27,0,1556,1557,5,3,0,0,1557,57,1,0,0,0,1558, + 1559,6,29,-1,0,1559,1560,3,62,31,0,1560,1569,1,0,0,0,1561,1562,10, + 1,0,0,1562,1564,7,13,0,0,1563,1565,3,174,87,0,1564,1563,1,0,0,0, + 1564,1565,1,0,0,0,1565,1566,1,0,0,0,1566,1568,3,58,29,2,1567,1561, + 1,0,0,0,1568,1571,1,0,0,0,1569,1567,1,0,0,0,1569,1570,1,0,0,0,1570, + 59,1,0,0,0,1571,1569,1,0,0,0,1572,1574,3,100,50,0,1573,1575,3,130, + 65,0,1574,1573,1,0,0,0,1574,1575,1,0,0,0,1575,1579,1,0,0,0,1576, + 1578,3,172,86,0,1577,1576,1,0,0,0,1578,1581,1,0,0,0,1579,1577,1, + 0,0,0,1579,1580,1,0,0,0,1580,1583,1,0,0,0,1581,1579,1,0,0,0,1582, + 1584,3,122,61,0,1583,1582,1,0,0,0,1583,1584,1,0,0,0,1584,1586,1, + 0,0,0,1585,1587,3,134,67,0,1586,1585,1,0,0,0,1586,1587,1,0,0,0,1587, + 1589,1,0,0,0,1588,1590,3,124,62,0,1589,1588,1,0,0,0,1589,1590,1, + 0,0,0,1590,1592,1,0,0,0,1591,1593,3,306,153,0,1592,1591,1,0,0,0, + 1592,1593,1,0,0,0,1593,1617,1,0,0,0,1594,1596,3,102,51,0,1595,1597, + 3,130,65,0,1596,1595,1,0,0,0,1596,1597,1,0,0,0,1597,1601,1,0,0,0, + 1598,1600,3,172,86,0,1599,1598,1,0,0,0,1600,1603,1,0,0,0,1601,1599, + 1,0,0,0,1601,1602,1,0,0,0,1602,1605,1,0,0,0,1603,1601,1,0,0,0,1604, + 1606,3,122,61,0,1605,1604,1,0,0,0,1605,1606,1,0,0,0,1606,1608,1, + 0,0,0,1607,1609,3,134,67,0,1608,1607,1,0,0,0,1608,1609,1,0,0,0,1609, + 1611,1,0,0,0,1610,1612,3,124,62,0,1611,1610,1,0,0,0,1611,1612,1, + 0,0,0,1612,1614,1,0,0,0,1613,1615,3,306,153,0,1614,1613,1,0,0,0, + 1614,1615,1,0,0,0,1615,1617,1,0,0,0,1616,1572,1,0,0,0,1616,1594, + 1,0,0,0,1617,61,1,0,0,0,1618,1628,3,60,30,0,1619,1628,3,96,48,0, + 1620,1621,5,258,0,0,1621,1628,3,214,107,0,1622,1628,3,204,102,0, + 1623,1624,5,2,0,0,1624,1625,3,54,27,0,1625,1626,5,3,0,0,1626,1628, + 1,0,0,0,1627,1618,1,0,0,0,1627,1619,1,0,0,0,1627,1620,1,0,0,0,1627, + 1622,1,0,0,0,1627,1623,1,0,0,0,1628,63,1,0,0,0,1629,1630,5,293,0, + 0,1630,1631,3,214,107,0,1631,65,1,0,0,0,1632,1633,5,180,0,0,1633, + 1646,3,68,34,0,1634,1635,5,191,0,0,1635,1636,5,28,0,0,1636,1646, + 3,228,114,0,1637,1646,3,30,15,0,1638,1646,3,28,14,0,1639,1646,3, + 210,105,0,1640,1646,3,80,40,0,1641,1646,3,32,16,0,1642,1646,3,34, + 17,0,1643,1644,5,262,0,0,1644,1646,3,68,34,0,1645,1632,1,0,0,0,1645, + 1634,1,0,0,0,1645,1637,1,0,0,0,1645,1638,1,0,0,0,1645,1639,1,0,0, + 0,1645,1640,1,0,0,0,1645,1641,1,0,0,0,1645,1642,1,0,0,0,1645,1643, + 1,0,0,0,1646,1649,1,0,0,0,1647,1645,1,0,0,0,1647,1648,1,0,0,0,1648, + 67,1,0,0,0,1649,1647,1,0,0,0,1650,1651,5,2,0,0,1651,1656,3,70,35, + 0,1652,1653,5,4,0,0,1653,1655,3,70,35,0,1654,1652,1,0,0,0,1655,1658, + 1,0,0,0,1656,1654,1,0,0,0,1656,1657,1,0,0,0,1657,1659,1,0,0,0,1658, + 1656,1,0,0,0,1659,1660,5,3,0,0,1660,69,1,0,0,0,1661,1666,3,72,36, + 0,1662,1664,5,308,0,0,1663,1662,1,0,0,0,1663,1664,1,0,0,0,1664,1665, + 1,0,0,0,1665,1667,3,74,37,0,1666,1663,1,0,0,0,1666,1667,1,0,0,0, + 1667,71,1,0,0,0,1668,1673,3,326,163,0,1669,1670,5,5,0,0,1670,1672, + 3,326,163,0,1671,1669,1,0,0,0,1672,1675,1,0,0,0,1673,1671,1,0,0, + 0,1673,1674,1,0,0,0,1674,1678,1,0,0,0,1675,1673,1,0,0,0,1676,1678, + 3,338,169,0,1677,1668,1,0,0,0,1677,1676,1,0,0,0,1678,73,1,0,0,0, + 1679,1684,5,335,0,0,1680,1684,5,337,0,0,1681,1684,3,258,129,0,1682, + 1684,3,338,169,0,1683,1679,1,0,0,0,1683,1680,1,0,0,0,1683,1681,1, + 0,0,0,1683,1682,1,0,0,0,1684,75,1,0,0,0,1685,1686,5,2,0,0,1686,1691, + 3,250,125,0,1687,1688,5,4,0,0,1688,1690,3,250,125,0,1689,1687,1, + 0,0,0,1690,1693,1,0,0,0,1691,1689,1,0,0,0,1691,1692,1,0,0,0,1692, + 1694,1,0,0,0,1693,1691,1,0,0,0,1694,1695,5,3,0,0,1695,77,1,0,0,0, + 1696,1697,5,2,0,0,1697,1702,3,76,38,0,1698,1699,5,4,0,0,1699,1701, + 3,76,38,0,1700,1698,1,0,0,0,1701,1704,1,0,0,0,1702,1700,1,0,0,0, + 1702,1703,1,0,0,0,1703,1705,1,0,0,0,1704,1702,1,0,0,0,1705,1706, + 5,3,0,0,1706,79,1,0,0,0,1707,1708,5,250,0,0,1708,1709,5,20,0,0,1709, + 1714,3,82,41,0,1710,1711,5,250,0,0,1711,1712,5,28,0,0,1712,1714, + 3,84,42,0,1713,1707,1,0,0,0,1713,1710,1,0,0,0,1714,81,1,0,0,0,1715, + 1716,5,128,0,0,1716,1717,3,338,169,0,1717,1718,5,185,0,0,1718,1719, + 3,338,169,0,1719,1722,1,0,0,0,1720,1722,3,326,163,0,1721,1715,1, + 0,0,0,1721,1720,1,0,0,0,1722,83,1,0,0,0,1723,1727,3,338,169,0,1724, + 1725,5,303,0,0,1725,1726,5,237,0,0,1726,1728,3,68,34,0,1727,1724, + 1,0,0,0,1727,1728,1,0,0,0,1728,85,1,0,0,0,1729,1730,3,326,163,0, + 1730,1731,3,338,169,0,1731,87,1,0,0,0,1732,1733,3,36,18,0,1733,1734, + 3,54,27,0,1734,1789,1,0,0,0,1735,1737,3,130,65,0,1736,1738,3,92, + 46,0,1737,1736,1,0,0,0,1738,1739,1,0,0,0,1739,1737,1,0,0,0,1739, + 1740,1,0,0,0,1740,1789,1,0,0,0,1741,1742,5,72,0,0,1742,1743,5,107, + 0,0,1743,1744,3,214,107,0,1744,1746,3,208,104,0,1745,1747,3,122, + 61,0,1746,1745,1,0,0,0,1746,1747,1,0,0,0,1747,1789,1,0,0,0,1748, + 1749,5,290,0,0,1749,1750,3,214,107,0,1750,1751,3,208,104,0,1751, + 1753,3,104,52,0,1752,1754,3,122,61,0,1753,1752,1,0,0,0,1753,1754, + 1,0,0,0,1754,1789,1,0,0,0,1755,1756,5,156,0,0,1756,1757,5,132,0, + 0,1757,1758,3,214,107,0,1758,1759,3,208,104,0,1759,1765,5,293,0, + 0,1760,1766,3,214,107,0,1761,1762,5,2,0,0,1762,1763,3,54,27,0,1763, + 1764,5,3,0,0,1764,1766,1,0,0,0,1765,1760,1,0,0,0,1765,1761,1,0,0, + 0,1766,1767,1,0,0,0,1767,1768,3,208,104,0,1768,1769,5,177,0,0,1769, + 1773,3,240,120,0,1770,1772,3,106,53,0,1771,1770,1,0,0,0,1772,1775, + 1,0,0,0,1773,1771,1,0,0,0,1773,1774,1,0,0,0,1774,1779,1,0,0,0,1775, + 1773,1,0,0,0,1776,1778,3,108,54,0,1777,1776,1,0,0,0,1778,1781,1, + 0,0,0,1779,1777,1,0,0,0,1779,1780,1,0,0,0,1780,1785,1,0,0,0,1781, + 1779,1,0,0,0,1782,1784,3,110,55,0,1783,1782,1,0,0,0,1784,1787,1, + 0,0,0,1785,1783,1,0,0,0,1785,1786,1,0,0,0,1786,1789,1,0,0,0,1787, + 1785,1,0,0,0,1788,1732,1,0,0,0,1788,1735,1,0,0,0,1788,1741,1,0,0, + 0,1788,1748,1,0,0,0,1788,1755,1,0,0,0,1789,89,1,0,0,0,1790,1791, + 5,182,0,0,1791,1792,5,28,0,0,1792,1797,3,94,47,0,1793,1794,5,4,0, + 0,1794,1796,3,94,47,0,1795,1793,1,0,0,0,1796,1799,1,0,0,0,1797,1795, + 1,0,0,0,1797,1798,1,0,0,0,1798,1801,1,0,0,0,1799,1797,1,0,0,0,1800, + 1790,1,0,0,0,1800,1801,1,0,0,0,1801,1812,1,0,0,0,1802,1803,5,38, + 0,0,1803,1804,5,28,0,0,1804,1809,3,236,118,0,1805,1806,5,4,0,0,1806, + 1808,3,236,118,0,1807,1805,1,0,0,0,1808,1811,1,0,0,0,1809,1807,1, + 0,0,0,1809,1810,1,0,0,0,1810,1813,1,0,0,0,1811,1809,1,0,0,0,1812, + 1802,1,0,0,0,1812,1813,1,0,0,0,1813,1824,1,0,0,0,1814,1815,5,80, + 0,0,1815,1816,5,28,0,0,1816,1821,3,236,118,0,1817,1818,5,4,0,0,1818, + 1820,3,236,118,0,1819,1817,1,0,0,0,1820,1823,1,0,0,0,1821,1819,1, + 0,0,0,1821,1822,1,0,0,0,1822,1825,1,0,0,0,1823,1821,1,0,0,0,1824, + 1814,1,0,0,0,1824,1825,1,0,0,0,1825,1836,1,0,0,0,1826,1827,5,245, + 0,0,1827,1828,5,28,0,0,1828,1833,3,94,47,0,1829,1830,5,4,0,0,1830, + 1832,3,94,47,0,1831,1829,1,0,0,0,1832,1835,1,0,0,0,1833,1831,1,0, + 0,0,1833,1834,1,0,0,0,1834,1837,1,0,0,0,1835,1833,1,0,0,0,1836,1826, + 1,0,0,0,1836,1837,1,0,0,0,1837,1839,1,0,0,0,1838,1840,3,306,153, + 0,1839,1838,1,0,0,0,1839,1840,1,0,0,0,1840,1846,1,0,0,0,1841,1844, + 5,144,0,0,1842,1845,5,10,0,0,1843,1845,3,236,118,0,1844,1842,1,0, + 0,0,1844,1843,1,0,0,0,1845,1847,1,0,0,0,1846,1841,1,0,0,0,1846,1847, + 1,0,0,0,1847,1850,1,0,0,0,1848,1849,5,176,0,0,1849,1851,3,236,118, + 0,1850,1848,1,0,0,0,1850,1851,1,0,0,0,1851,91,1,0,0,0,1852,1853, + 3,36,18,0,1853,1854,3,98,49,0,1854,93,1,0,0,0,1855,1857,3,236,118, + 0,1856,1858,7,14,0,0,1857,1856,1,0,0,0,1857,1858,1,0,0,0,1858,1861, + 1,0,0,0,1859,1860,5,174,0,0,1860,1862,7,15,0,0,1861,1859,1,0,0,0, + 1861,1862,1,0,0,0,1862,95,1,0,0,0,1863,1865,3,130,65,0,1864,1866, + 3,98,49,0,1865,1864,1,0,0,0,1866,1867,1,0,0,0,1867,1865,1,0,0,0, + 1867,1868,1,0,0,0,1868,97,1,0,0,0,1869,1871,3,100,50,0,1870,1872, + 3,122,61,0,1871,1870,1,0,0,0,1871,1872,1,0,0,0,1872,1873,1,0,0,0, + 1873,1874,3,90,45,0,1874,1897,1,0,0,0,1875,1879,3,102,51,0,1876, + 1878,3,172,86,0,1877,1876,1,0,0,0,1878,1881,1,0,0,0,1879,1877,1, + 0,0,0,1879,1880,1,0,0,0,1880,1883,1,0,0,0,1881,1879,1,0,0,0,1882, + 1884,3,122,61,0,1883,1882,1,0,0,0,1883,1884,1,0,0,0,1884,1886,1, + 0,0,0,1885,1887,3,134,67,0,1886,1885,1,0,0,0,1886,1887,1,0,0,0,1887, + 1889,1,0,0,0,1888,1890,3,124,62,0,1889,1888,1,0,0,0,1889,1890,1, + 0,0,0,1890,1892,1,0,0,0,1891,1893,3,306,153,0,1892,1891,1,0,0,0, + 1892,1893,1,0,0,0,1893,1894,1,0,0,0,1894,1895,3,90,45,0,1895,1897, + 1,0,0,0,1896,1869,1,0,0,0,1896,1875,1,0,0,0,1897,99,1,0,0,0,1898, + 1899,5,233,0,0,1899,1900,5,275,0,0,1900,1902,5,2,0,0,1901,1903,3, + 174,87,0,1902,1901,1,0,0,0,1902,1903,1,0,0,0,1903,1904,1,0,0,0,1904, + 1905,3,238,119,0,1905,1906,5,3,0,0,1906,1918,1,0,0,0,1907,1909,5, + 154,0,0,1908,1910,3,174,87,0,1909,1908,1,0,0,0,1909,1910,1,0,0,0, + 1910,1911,1,0,0,0,1911,1918,3,238,119,0,1912,1914,5,210,0,0,1913, + 1915,3,174,87,0,1914,1913,1,0,0,0,1914,1915,1,0,0,0,1915,1916,1, + 0,0,0,1916,1918,3,238,119,0,1917,1898,1,0,0,0,1917,1907,1,0,0,0, + 1917,1912,1,0,0,0,1918,1920,1,0,0,0,1919,1921,3,210,105,0,1920,1919, + 1,0,0,0,1920,1921,1,0,0,0,1921,1924,1,0,0,0,1922,1923,5,208,0,0, + 1923,1925,3,338,169,0,1924,1922,1,0,0,0,1924,1925,1,0,0,0,1925,1926, + 1,0,0,0,1926,1927,5,293,0,0,1927,1940,3,338,169,0,1928,1938,5,20, + 0,0,1929,1939,3,192,96,0,1930,1939,3,288,144,0,1931,1934,5,2,0,0, + 1932,1935,3,192,96,0,1933,1935,3,288,144,0,1934,1932,1,0,0,0,1934, + 1933,1,0,0,0,1935,1936,1,0,0,0,1936,1937,5,3,0,0,1937,1939,1,0,0, + 0,1938,1929,1,0,0,0,1938,1930,1,0,0,0,1938,1931,1,0,0,0,1939,1941, + 1,0,0,0,1940,1928,1,0,0,0,1940,1941,1,0,0,0,1941,1943,1,0,0,0,1942, + 1944,3,210,105,0,1943,1942,1,0,0,0,1943,1944,1,0,0,0,1944,1947,1, + 0,0,0,1945,1946,5,207,0,0,1946,1948,3,338,169,0,1947,1945,1,0,0, + 0,1947,1948,1,0,0,0,1948,101,1,0,0,0,1949,1953,5,233,0,0,1950,1952, + 3,126,63,0,1951,1950,1,0,0,0,1952,1955,1,0,0,0,1953,1951,1,0,0,0, + 1953,1954,1,0,0,0,1954,1957,1,0,0,0,1955,1953,1,0,0,0,1956,1958, + 3,174,87,0,1957,1956,1,0,0,0,1957,1958,1,0,0,0,1958,1959,1,0,0,0, + 1959,1960,3,226,113,0,1960,103,1,0,0,0,1961,1962,5,239,0,0,1962, + 1963,3,118,59,0,1963,105,1,0,0,0,1964,1965,5,300,0,0,1965,1968,5, + 155,0,0,1966,1967,5,14,0,0,1967,1969,3,240,120,0,1968,1966,1,0,0, + 0,1968,1969,1,0,0,0,1969,1970,1,0,0,0,1970,1971,5,265,0,0,1971,1972, + 3,112,56,0,1972,107,1,0,0,0,1973,1974,5,300,0,0,1974,1975,5,172, + 0,0,1975,1978,5,155,0,0,1976,1977,5,28,0,0,1977,1979,5,261,0,0,1978, + 1976,1,0,0,0,1978,1979,1,0,0,0,1979,1982,1,0,0,0,1980,1981,5,14, + 0,0,1981,1983,3,240,120,0,1982,1980,1,0,0,0,1982,1983,1,0,0,0,1983, + 1984,1,0,0,0,1984,1985,5,265,0,0,1985,1986,3,114,57,0,1986,109,1, + 0,0,0,1987,1988,5,300,0,0,1988,1989,5,172,0,0,1989,1990,5,155,0, + 0,1990,1991,5,28,0,0,1991,1994,5,247,0,0,1992,1993,5,14,0,0,1993, + 1995,3,240,120,0,1994,1992,1,0,0,0,1994,1995,1,0,0,0,1995,1996,1, + 0,0,0,1996,1997,5,265,0,0,1997,1998,3,116,58,0,1998,111,1,0,0,0, + 1999,2007,5,72,0,0,2000,2001,5,290,0,0,2001,2002,5,239,0,0,2002, + 2007,5,318,0,0,2003,2004,5,290,0,0,2004,2005,5,239,0,0,2005,2007, + 3,118,59,0,2006,1999,1,0,0,0,2006,2000,1,0,0,0,2006,2003,1,0,0,0, + 2007,113,1,0,0,0,2008,2009,5,129,0,0,2009,2027,5,318,0,0,2010,2011, + 5,129,0,0,2011,2012,5,2,0,0,2012,2013,3,212,106,0,2013,2014,5,3, + 0,0,2014,2015,5,294,0,0,2015,2016,5,2,0,0,2016,2021,3,236,118,0, + 2017,2018,5,4,0,0,2018,2020,3,236,118,0,2019,2017,1,0,0,0,2020,2023, + 1,0,0,0,2021,2019,1,0,0,0,2021,2022,1,0,0,0,2022,2024,1,0,0,0,2023, + 2021,1,0,0,0,2024,2025,5,3,0,0,2025,2027,1,0,0,0,2026,2008,1,0,0, + 0,2026,2010,1,0,0,0,2027,115,1,0,0,0,2028,2033,5,72,0,0,2029,2030, + 5,290,0,0,2030,2031,5,239,0,0,2031,2033,3,118,59,0,2032,2028,1,0, + 0,0,2032,2029,1,0,0,0,2033,117,1,0,0,0,2034,2039,3,120,60,0,2035, + 2036,5,4,0,0,2036,2038,3,120,60,0,2037,2035,1,0,0,0,2038,2041,1, + 0,0,0,2039,2037,1,0,0,0,2039,2040,1,0,0,0,2040,119,1,0,0,0,2041, + 2039,1,0,0,0,2042,2043,3,214,107,0,2043,2044,5,308,0,0,2044,2045, + 3,236,118,0,2045,121,1,0,0,0,2046,2047,5,301,0,0,2047,2048,3,240, + 120,0,2048,123,1,0,0,0,2049,2050,5,116,0,0,2050,2051,3,240,120,0, + 2051,125,1,0,0,0,2052,2053,5,328,0,0,2053,2060,3,128,64,0,2054,2056, + 5,4,0,0,2055,2054,1,0,0,0,2055,2056,1,0,0,0,2056,2057,1,0,0,0,2057, + 2059,3,128,64,0,2058,2055,1,0,0,0,2059,2062,1,0,0,0,2060,2058,1, + 0,0,0,2060,2061,1,0,0,0,2061,2063,1,0,0,0,2062,2060,1,0,0,0,2063, + 2064,5,329,0,0,2064,127,1,0,0,0,2065,2079,3,326,163,0,2066,2067, + 3,326,163,0,2067,2068,5,2,0,0,2068,2073,3,248,124,0,2069,2070,5, + 4,0,0,2070,2072,3,248,124,0,2071,2069,1,0,0,0,2072,2075,1,0,0,0, + 2073,2071,1,0,0,0,2073,2074,1,0,0,0,2074,2076,1,0,0,0,2075,2073, + 1,0,0,0,2076,2077,5,3,0,0,2077,2079,1,0,0,0,2078,2065,1,0,0,0,2078, + 2066,1,0,0,0,2079,129,1,0,0,0,2080,2081,5,107,0,0,2081,2086,3,176, + 88,0,2082,2083,5,4,0,0,2083,2085,3,176,88,0,2084,2082,1,0,0,0,2085, + 2088,1,0,0,0,2086,2084,1,0,0,0,2086,2087,1,0,0,0,2087,2092,1,0,0, + 0,2088,2086,1,0,0,0,2089,2091,3,172,86,0,2090,2089,1,0,0,0,2091, + 2094,1,0,0,0,2092,2090,1,0,0,0,2092,2093,1,0,0,0,2093,2096,1,0,0, + 0,2094,2092,1,0,0,0,2095,2097,3,144,72,0,2096,2095,1,0,0,0,2096, + 2097,1,0,0,0,2097,2099,1,0,0,0,2098,2100,3,150,75,0,2099,2098,1, + 0,0,0,2099,2100,1,0,0,0,2100,131,1,0,0,0,2101,2103,5,103,0,0,2102, + 2101,1,0,0,0,2102,2103,1,0,0,0,2103,2104,1,0,0,0,2104,2105,7,16, + 0,0,2105,2106,5,20,0,0,2106,2107,5,175,0,0,2107,2116,3,342,171,0, + 2108,2110,5,103,0,0,2109,2108,1,0,0,0,2109,2110,1,0,0,0,2110,2111, + 1,0,0,0,2111,2112,7,17,0,0,2112,2113,5,20,0,0,2113,2114,5,175,0, + 0,2114,2116,3,244,122,0,2115,2102,1,0,0,0,2115,2109,1,0,0,0,2116, + 133,1,0,0,0,2117,2118,5,114,0,0,2118,2119,5,28,0,0,2119,2124,3,136, + 68,0,2120,2121,5,4,0,0,2121,2123,3,136,68,0,2122,2120,1,0,0,0,2123, + 2126,1,0,0,0,2124,2122,1,0,0,0,2124,2125,1,0,0,0,2125,2157,1,0,0, + 0,2126,2124,1,0,0,0,2127,2128,5,114,0,0,2128,2129,5,28,0,0,2129, + 2134,3,236,118,0,2130,2131,5,4,0,0,2131,2133,3,236,118,0,2132,2130, + 1,0,0,0,2133,2136,1,0,0,0,2134,2132,1,0,0,0,2134,2135,1,0,0,0,2135, + 2154,1,0,0,0,2136,2134,1,0,0,0,2137,2138,5,303,0,0,2138,2155,5,226, + 0,0,2139,2140,5,303,0,0,2140,2155,5,55,0,0,2141,2142,5,115,0,0,2142, + 2143,5,241,0,0,2143,2144,5,2,0,0,2144,2149,3,142,71,0,2145,2146, + 5,4,0,0,2146,2148,3,142,71,0,2147,2145,1,0,0,0,2148,2151,1,0,0,0, + 2149,2147,1,0,0,0,2149,2150,1,0,0,0,2150,2152,1,0,0,0,2151,2149, + 1,0,0,0,2152,2153,5,3,0,0,2153,2155,1,0,0,0,2154,2137,1,0,0,0,2154, + 2139,1,0,0,0,2154,2141,1,0,0,0,2154,2155,1,0,0,0,2155,2157,1,0,0, + 0,2156,2117,1,0,0,0,2156,2127,1,0,0,0,2157,135,1,0,0,0,2158,2161, + 3,138,69,0,2159,2161,3,236,118,0,2160,2158,1,0,0,0,2160,2159,1,0, + 0,0,2161,137,1,0,0,0,2162,2163,7,18,0,0,2163,2164,5,2,0,0,2164,2169, + 3,142,71,0,2165,2166,5,4,0,0,2166,2168,3,142,71,0,2167,2165,1,0, + 0,0,2168,2171,1,0,0,0,2169,2167,1,0,0,0,2169,2170,1,0,0,0,2170,2172, + 1,0,0,0,2171,2169,1,0,0,0,2172,2173,5,3,0,0,2173,2188,1,0,0,0,2174, + 2175,5,115,0,0,2175,2176,5,241,0,0,2176,2177,5,2,0,0,2177,2182,3, + 140,70,0,2178,2179,5,4,0,0,2179,2181,3,140,70,0,2180,2178,1,0,0, + 0,2181,2184,1,0,0,0,2182,2180,1,0,0,0,2182,2183,1,0,0,0,2183,2185, + 1,0,0,0,2184,2182,1,0,0,0,2185,2186,5,3,0,0,2186,2188,1,0,0,0,2187, + 2162,1,0,0,0,2187,2174,1,0,0,0,2188,139,1,0,0,0,2189,2192,3,138, + 69,0,2190,2192,3,142,71,0,2191,2189,1,0,0,0,2191,2190,1,0,0,0,2192, + 141,1,0,0,0,2193,2202,5,2,0,0,2194,2199,3,236,118,0,2195,2196,5, + 4,0,0,2196,2198,3,236,118,0,2197,2195,1,0,0,0,2198,2201,1,0,0,0, + 2199,2197,1,0,0,0,2199,2200,1,0,0,0,2200,2203,1,0,0,0,2201,2199, + 1,0,0,0,2202,2194,1,0,0,0,2202,2203,1,0,0,0,2203,2204,1,0,0,0,2204, + 2207,5,3,0,0,2205,2207,3,236,118,0,2206,2193,1,0,0,0,2206,2205,1, + 0,0,0,2207,143,1,0,0,0,2208,2209,5,196,0,0,2209,2210,5,2,0,0,2210, + 2211,3,226,113,0,2211,2212,5,103,0,0,2212,2213,3,146,73,0,2213,2214, + 5,122,0,0,2214,2215,5,2,0,0,2215,2220,3,148,74,0,2216,2217,5,4,0, + 0,2217,2219,3,148,74,0,2218,2216,1,0,0,0,2219,2222,1,0,0,0,2220, + 2218,1,0,0,0,2220,2221,1,0,0,0,2221,2223,1,0,0,0,2222,2220,1,0,0, + 0,2223,2224,5,3,0,0,2224,2225,5,3,0,0,2225,145,1,0,0,0,2226,2239, + 3,326,163,0,2227,2228,5,2,0,0,2228,2233,3,326,163,0,2229,2230,5, + 4,0,0,2230,2232,3,326,163,0,2231,2229,1,0,0,0,2232,2235,1,0,0,0, + 2233,2231,1,0,0,0,2233,2234,1,0,0,0,2234,2236,1,0,0,0,2235,2233, + 1,0,0,0,2236,2237,5,3,0,0,2237,2239,1,0,0,0,2238,2226,1,0,0,0,2238, + 2227,1,0,0,0,2239,147,1,0,0,0,2240,2245,3,236,118,0,2241,2243,5, + 20,0,0,2242,2241,1,0,0,0,2242,2243,1,0,0,0,2243,2244,1,0,0,0,2244, + 2246,3,326,163,0,2245,2242,1,0,0,0,2245,2246,1,0,0,0,2246,149,1, + 0,0,0,2247,2249,5,288,0,0,2248,2250,3,152,76,0,2249,2248,1,0,0,0, + 2249,2250,1,0,0,0,2250,2251,1,0,0,0,2251,2252,5,2,0,0,2252,2253, + 3,154,77,0,2253,2258,5,3,0,0,2254,2256,5,20,0,0,2255,2254,1,0,0, + 0,2255,2256,1,0,0,0,2256,2257,1,0,0,0,2257,2259,3,326,163,0,2258, + 2255,1,0,0,0,2258,2259,1,0,0,0,2259,151,1,0,0,0,2260,2261,7,19,0, + 0,2261,2262,5,174,0,0,2262,153,1,0,0,0,2263,2266,3,156,78,0,2264, + 2266,3,158,79,0,2265,2263,1,0,0,0,2265,2264,1,0,0,0,2266,155,1,0, + 0,0,2267,2268,3,162,81,0,2268,2269,5,103,0,0,2269,2270,3,164,82, + 0,2270,2271,5,122,0,0,2271,2272,5,2,0,0,2272,2277,3,166,83,0,2273, + 2274,5,4,0,0,2274,2276,3,166,83,0,2275,2273,1,0,0,0,2276,2279,1, + 0,0,0,2277,2275,1,0,0,0,2277,2278,1,0,0,0,2278,2280,1,0,0,0,2279, + 2277,1,0,0,0,2280,2281,5,3,0,0,2281,157,1,0,0,0,2282,2283,5,2,0, + 0,2283,2288,3,162,81,0,2284,2285,5,4,0,0,2285,2287,3,162,81,0,2286, + 2284,1,0,0,0,2287,2290,1,0,0,0,2288,2286,1,0,0,0,2288,2289,1,0,0, + 0,2289,2291,1,0,0,0,2290,2288,1,0,0,0,2291,2292,5,3,0,0,2292,2293, + 5,103,0,0,2293,2294,3,164,82,0,2294,2295,5,122,0,0,2295,2296,5,2, + 0,0,2296,2301,3,160,80,0,2297,2298,5,4,0,0,2298,2300,3,160,80,0, + 2299,2297,1,0,0,0,2300,2303,1,0,0,0,2301,2299,1,0,0,0,2301,2302, + 1,0,0,0,2302,2304,1,0,0,0,2303,2301,1,0,0,0,2304,2305,5,3,0,0,2305, + 159,1,0,0,0,2306,2307,5,2,0,0,2307,2312,3,168,84,0,2308,2309,5,4, + 0,0,2309,2311,3,168,84,0,2310,2308,1,0,0,0,2311,2314,1,0,0,0,2312, + 2310,1,0,0,0,2312,2313,1,0,0,0,2313,2315,1,0,0,0,2314,2312,1,0,0, + 0,2315,2317,5,3,0,0,2316,2318,3,170,85,0,2317,2316,1,0,0,0,2317, + 2318,1,0,0,0,2318,161,1,0,0,0,2319,2320,3,326,163,0,2320,163,1,0, + 0,0,2321,2322,3,326,163,0,2322,165,1,0,0,0,2323,2325,3,168,84,0, + 2324,2326,3,170,85,0,2325,2324,1,0,0,0,2325,2326,1,0,0,0,2326,167, + 1,0,0,0,2327,2328,3,214,107,0,2328,169,1,0,0,0,2329,2331,5,20,0, + 0,2330,2329,1,0,0,0,2330,2331,1,0,0,0,2331,2332,1,0,0,0,2332,2333, + 3,326,163,0,2333,171,1,0,0,0,2334,2335,5,138,0,0,2335,2337,5,296, + 0,0,2336,2338,5,184,0,0,2337,2336,1,0,0,0,2337,2338,1,0,0,0,2338, + 2339,1,0,0,0,2339,2340,3,320,160,0,2340,2349,5,2,0,0,2341,2346,3, + 236,118,0,2342,2343,5,4,0,0,2343,2345,3,236,118,0,2344,2342,1,0, + 0,0,2345,2348,1,0,0,0,2346,2344,1,0,0,0,2346,2347,1,0,0,0,2347,2350, + 1,0,0,0,2348,2346,1,0,0,0,2349,2341,1,0,0,0,2349,2350,1,0,0,0,2350, + 2351,1,0,0,0,2351,2352,5,3,0,0,2352,2364,3,326,163,0,2353,2355,5, + 20,0,0,2354,2353,1,0,0,0,2354,2355,1,0,0,0,2355,2356,1,0,0,0,2356, + 2361,3,326,163,0,2357,2358,5,4,0,0,2358,2360,3,326,163,0,2359,2357, + 1,0,0,0,2360,2363,1,0,0,0,2361,2359,1,0,0,0,2361,2362,1,0,0,0,2362, + 2365,1,0,0,0,2363,2361,1,0,0,0,2364,2354,1,0,0,0,2364,2365,1,0,0, + 0,2365,173,1,0,0,0,2366,2367,7,20,0,0,2367,175,1,0,0,0,2368,2370, + 5,138,0,0,2369,2368,1,0,0,0,2369,2370,1,0,0,0,2370,2371,1,0,0,0, + 2371,2375,3,202,101,0,2372,2374,3,178,89,0,2373,2372,1,0,0,0,2374, + 2377,1,0,0,0,2375,2373,1,0,0,0,2375,2376,1,0,0,0,2376,177,1,0,0, + 0,2377,2375,1,0,0,0,2378,2382,3,180,90,0,2379,2382,3,144,72,0,2380, + 2382,3,150,75,0,2381,2378,1,0,0,0,2381,2379,1,0,0,0,2381,2380,1, + 0,0,0,2382,179,1,0,0,0,2383,2384,3,182,91,0,2384,2386,5,135,0,0, + 2385,2387,5,138,0,0,2386,2385,1,0,0,0,2386,2387,1,0,0,0,2387,2388, + 1,0,0,0,2388,2390,3,202,101,0,2389,2391,3,184,92,0,2390,2389,1,0, + 0,0,2390,2391,1,0,0,0,2391,2401,1,0,0,0,2392,2393,5,170,0,0,2393, + 2394,3,182,91,0,2394,2396,5,135,0,0,2395,2397,5,138,0,0,2396,2395, + 1,0,0,0,2396,2397,1,0,0,0,2397,2398,1,0,0,0,2398,2399,3,202,101, + 0,2399,2401,1,0,0,0,2400,2383,1,0,0,0,2400,2392,1,0,0,0,2401,181, + 1,0,0,0,2402,2404,5,126,0,0,2403,2402,1,0,0,0,2403,2404,1,0,0,0, + 2404,2427,1,0,0,0,2405,2427,5,54,0,0,2406,2408,5,141,0,0,2407,2409, + 5,184,0,0,2408,2407,1,0,0,0,2408,2409,1,0,0,0,2409,2427,1,0,0,0, + 2410,2412,5,141,0,0,2411,2410,1,0,0,0,2411,2412,1,0,0,0,2412,2413, + 1,0,0,0,2413,2427,5,234,0,0,2414,2416,5,221,0,0,2415,2417,5,184, + 0,0,2416,2415,1,0,0,0,2416,2417,1,0,0,0,2417,2427,1,0,0,0,2418,2420, + 5,108,0,0,2419,2421,5,184,0,0,2420,2419,1,0,0,0,2420,2421,1,0,0, + 0,2421,2427,1,0,0,0,2422,2424,5,141,0,0,2423,2422,1,0,0,0,2423,2424, + 1,0,0,0,2424,2425,1,0,0,0,2425,2427,5,15,0,0,2426,2403,1,0,0,0,2426, + 2405,1,0,0,0,2426,2406,1,0,0,0,2426,2411,1,0,0,0,2426,2414,1,0,0, + 0,2426,2418,1,0,0,0,2426,2423,1,0,0,0,2427,183,1,0,0,0,2428,2429, + 5,177,0,0,2429,2433,3,240,120,0,2430,2431,5,293,0,0,2431,2433,3, + 190,95,0,2432,2428,1,0,0,0,2432,2430,1,0,0,0,2433,185,1,0,0,0,2434, + 2435,5,260,0,0,2435,2437,5,2,0,0,2436,2438,3,188,94,0,2437,2436, + 1,0,0,0,2437,2438,1,0,0,0,2438,2439,1,0,0,0,2439,2444,5,3,0,0,2440, + 2441,5,215,0,0,2441,2442,5,2,0,0,2442,2443,5,335,0,0,2443,2445,5, + 3,0,0,2444,2440,1,0,0,0,2444,2445,1,0,0,0,2445,187,1,0,0,0,2446, + 2448,5,317,0,0,2447,2446,1,0,0,0,2447,2448,1,0,0,0,2448,2449,1,0, + 0,0,2449,2450,7,21,0,0,2450,2471,5,195,0,0,2451,2452,3,236,118,0, + 2452,2453,5,228,0,0,2453,2471,1,0,0,0,2454,2455,5,26,0,0,2455,2456, + 5,335,0,0,2456,2457,5,183,0,0,2457,2458,5,175,0,0,2458,2467,5,335, + 0,0,2459,2465,5,177,0,0,2460,2466,3,326,163,0,2461,2462,3,320,160, + 0,2462,2463,5,2,0,0,2463,2464,5,3,0,0,2464,2466,1,0,0,0,2465,2460, + 1,0,0,0,2465,2461,1,0,0,0,2466,2468,1,0,0,0,2467,2459,1,0,0,0,2467, + 2468,1,0,0,0,2468,2471,1,0,0,0,2469,2471,3,236,118,0,2470,2447,1, + 0,0,0,2470,2451,1,0,0,0,2470,2454,1,0,0,0,2470,2469,1,0,0,0,2471, + 189,1,0,0,0,2472,2473,5,2,0,0,2473,2474,3,192,96,0,2474,2475,5,3, + 0,0,2475,191,1,0,0,0,2476,2481,3,322,161,0,2477,2478,5,4,0,0,2478, + 2480,3,322,161,0,2479,2477,1,0,0,0,2480,2483,1,0,0,0,2481,2479,1, + 0,0,0,2481,2482,1,0,0,0,2482,193,1,0,0,0,2483,2481,1,0,0,0,2484, + 2485,5,2,0,0,2485,2490,3,196,98,0,2486,2487,5,4,0,0,2487,2489,3, + 196,98,0,2488,2486,1,0,0,0,2489,2492,1,0,0,0,2490,2488,1,0,0,0,2490, + 2491,1,0,0,0,2491,2493,1,0,0,0,2492,2490,1,0,0,0,2493,2494,5,3,0, + 0,2494,195,1,0,0,0,2495,2497,3,322,161,0,2496,2498,7,14,0,0,2497, + 2496,1,0,0,0,2497,2498,1,0,0,0,2498,197,1,0,0,0,2499,2500,5,2,0, + 0,2500,2505,3,200,100,0,2501,2502,5,4,0,0,2502,2504,3,200,100,0, + 2503,2501,1,0,0,0,2504,2507,1,0,0,0,2505,2503,1,0,0,0,2505,2506, + 1,0,0,0,2506,2508,1,0,0,0,2507,2505,1,0,0,0,2508,2509,5,3,0,0,2509, + 199,1,0,0,0,2510,2512,3,326,163,0,2511,2513,3,34,17,0,2512,2511, + 1,0,0,0,2512,2513,1,0,0,0,2513,201,1,0,0,0,2514,2516,3,214,107,0, + 2515,2517,3,132,66,0,2516,2515,1,0,0,0,2516,2517,1,0,0,0,2517,2519, + 1,0,0,0,2518,2520,3,186,93,0,2519,2518,1,0,0,0,2519,2520,1,0,0,0, + 2520,2521,1,0,0,0,2521,2522,3,208,104,0,2522,2542,1,0,0,0,2523,2524, + 5,2,0,0,2524,2525,3,54,27,0,2525,2527,5,3,0,0,2526,2528,3,186,93, + 0,2527,2526,1,0,0,0,2527,2528,1,0,0,0,2528,2529,1,0,0,0,2529,2530, + 3,208,104,0,2530,2542,1,0,0,0,2531,2532,5,2,0,0,2532,2533,3,176, + 88,0,2533,2535,5,3,0,0,2534,2536,3,186,93,0,2535,2534,1,0,0,0,2535, + 2536,1,0,0,0,2536,2537,1,0,0,0,2537,2538,3,208,104,0,2538,2542,1, + 0,0,0,2539,2542,3,204,102,0,2540,2542,3,206,103,0,2541,2514,1,0, + 0,0,2541,2523,1,0,0,0,2541,2531,1,0,0,0,2541,2539,1,0,0,0,2541,2540, + 1,0,0,0,2542,203,1,0,0,0,2543,2544,5,294,0,0,2544,2549,3,236,118, + 0,2545,2546,5,4,0,0,2546,2548,3,236,118,0,2547,2545,1,0,0,0,2548, + 2551,1,0,0,0,2549,2547,1,0,0,0,2549,2550,1,0,0,0,2550,2552,1,0,0, + 0,2551,2549,1,0,0,0,2552,2553,3,208,104,0,2553,205,1,0,0,0,2554, + 2555,3,318,159,0,2555,2564,5,2,0,0,2556,2561,3,236,118,0,2557,2558, + 5,4,0,0,2558,2560,3,236,118,0,2559,2557,1,0,0,0,2560,2563,1,0,0, + 0,2561,2559,1,0,0,0,2561,2562,1,0,0,0,2562,2565,1,0,0,0,2563,2561, + 1,0,0,0,2564,2556,1,0,0,0,2564,2565,1,0,0,0,2565,2566,1,0,0,0,2566, + 2567,5,3,0,0,2567,2568,3,208,104,0,2568,207,1,0,0,0,2569,2571,5, + 20,0,0,2570,2569,1,0,0,0,2570,2571,1,0,0,0,2571,2572,1,0,0,0,2572, + 2574,3,328,164,0,2573,2575,3,190,95,0,2574,2573,1,0,0,0,2574,2575, + 1,0,0,0,2575,2577,1,0,0,0,2576,2570,1,0,0,0,2576,2577,1,0,0,0,2577, + 209,1,0,0,0,2578,2579,5,227,0,0,2579,2580,5,105,0,0,2580,2581,5, + 236,0,0,2581,2585,3,338,169,0,2582,2583,5,303,0,0,2583,2584,5,237, + 0,0,2584,2586,3,68,34,0,2585,2582,1,0,0,0,2585,2586,1,0,0,0,2586, + 2628,1,0,0,0,2587,2588,5,227,0,0,2588,2589,5,105,0,0,2589,2599,5, + 73,0,0,2590,2591,5,98,0,0,2591,2592,5,264,0,0,2592,2593,5,28,0,0, + 2593,2597,3,338,169,0,2594,2595,5,86,0,0,2595,2596,5,28,0,0,2596, + 2598,3,338,169,0,2597,2594,1,0,0,0,2597,2598,1,0,0,0,2598,2600,1, + 0,0,0,2599,2590,1,0,0,0,2599,2600,1,0,0,0,2600,2606,1,0,0,0,2601, + 2602,5,42,0,0,2602,2603,5,134,0,0,2603,2604,5,264,0,0,2604,2605, + 5,28,0,0,2605,2607,3,338,169,0,2606,2601,1,0,0,0,2606,2607,1,0,0, + 0,2607,2613,1,0,0,0,2608,2609,5,154,0,0,2609,2610,5,136,0,0,2610, + 2611,5,264,0,0,2611,2612,5,28,0,0,2612,2614,3,338,169,0,2613,2608, + 1,0,0,0,2613,2614,1,0,0,0,2614,2619,1,0,0,0,2615,2616,5,145,0,0, + 2616,2617,5,264,0,0,2617,2618,5,28,0,0,2618,2620,3,338,169,0,2619, + 2615,1,0,0,0,2619,2620,1,0,0,0,2620,2625,1,0,0,0,2621,2622,5,173, + 0,0,2622,2623,5,71,0,0,2623,2624,5,20,0,0,2624,2626,3,338,169,0, + 2625,2621,1,0,0,0,2625,2626,1,0,0,0,2626,2628,1,0,0,0,2627,2578, + 1,0,0,0,2627,2587,1,0,0,0,2628,211,1,0,0,0,2629,2634,3,214,107,0, + 2630,2631,5,4,0,0,2631,2633,3,214,107,0,2632,2630,1,0,0,0,2633,2636, + 1,0,0,0,2634,2632,1,0,0,0,2634,2635,1,0,0,0,2635,213,1,0,0,0,2636, + 2634,1,0,0,0,2637,2642,3,322,161,0,2638,2639,5,5,0,0,2639,2641,3, + 322,161,0,2640,2638,1,0,0,0,2641,2644,1,0,0,0,2642,2640,1,0,0,0, + 2642,2643,1,0,0,0,2643,215,1,0,0,0,2644,2642,1,0,0,0,2645,2650,3, + 218,109,0,2646,2647,5,4,0,0,2647,2649,3,218,109,0,2648,2646,1,0, + 0,0,2649,2652,1,0,0,0,2650,2648,1,0,0,0,2650,2651,1,0,0,0,2651,217, + 1,0,0,0,2652,2650,1,0,0,0,2653,2656,3,214,107,0,2654,2655,5,180, + 0,0,2655,2657,3,68,34,0,2656,2654,1,0,0,0,2656,2657,1,0,0,0,2657, + 219,1,0,0,0,2658,2659,3,322,161,0,2659,2660,5,5,0,0,2660,2662,1, + 0,0,0,2661,2658,1,0,0,0,2661,2662,1,0,0,0,2662,2663,1,0,0,0,2663, + 2664,3,322,161,0,2664,221,1,0,0,0,2665,2666,3,322,161,0,2666,2667, + 5,5,0,0,2667,2669,1,0,0,0,2668,2665,1,0,0,0,2668,2669,1,0,0,0,2669, + 2670,1,0,0,0,2670,2671,3,322,161,0,2671,223,1,0,0,0,2672,2680,3, + 236,118,0,2673,2675,5,20,0,0,2674,2673,1,0,0,0,2674,2675,1,0,0,0, + 2675,2678,1,0,0,0,2676,2679,3,322,161,0,2677,2679,3,190,95,0,2678, + 2676,1,0,0,0,2678,2677,1,0,0,0,2679,2681,1,0,0,0,2680,2674,1,0,0, + 0,2680,2681,1,0,0,0,2681,225,1,0,0,0,2682,2687,3,224,112,0,2683, + 2684,5,4,0,0,2684,2686,3,224,112,0,2685,2683,1,0,0,0,2686,2689,1, + 0,0,0,2687,2685,1,0,0,0,2687,2688,1,0,0,0,2688,227,1,0,0,0,2689, + 2687,1,0,0,0,2690,2691,5,2,0,0,2691,2696,3,230,115,0,2692,2693,5, + 4,0,0,2693,2695,3,230,115,0,2694,2692,1,0,0,0,2695,2698,1,0,0,0, + 2696,2694,1,0,0,0,2696,2697,1,0,0,0,2697,2699,1,0,0,0,2698,2696, + 1,0,0,0,2699,2700,5,3,0,0,2700,229,1,0,0,0,2701,2704,3,232,116,0, + 2702,2704,3,290,145,0,2703,2701,1,0,0,0,2703,2702,1,0,0,0,2704,231, + 1,0,0,0,2705,2719,3,320,160,0,2706,2707,3,326,163,0,2707,2708,5, + 2,0,0,2708,2713,3,234,117,0,2709,2710,5,4,0,0,2710,2712,3,234,117, + 0,2711,2709,1,0,0,0,2712,2715,1,0,0,0,2713,2711,1,0,0,0,2713,2714, + 1,0,0,0,2714,2716,1,0,0,0,2715,2713,1,0,0,0,2716,2717,5,3,0,0,2717, + 2719,1,0,0,0,2718,2705,1,0,0,0,2718,2706,1,0,0,0,2719,233,1,0,0, + 0,2720,2723,3,320,160,0,2721,2723,3,250,125,0,2722,2720,1,0,0,0, + 2722,2721,1,0,0,0,2723,235,1,0,0,0,2724,2725,3,240,120,0,2725,237, + 1,0,0,0,2726,2731,3,236,118,0,2727,2728,5,4,0,0,2728,2730,3,236, + 118,0,2729,2727,1,0,0,0,2730,2733,1,0,0,0,2731,2729,1,0,0,0,2731, + 2732,1,0,0,0,2732,239,1,0,0,0,2733,2731,1,0,0,0,2734,2735,6,120, + -1,0,2735,2736,5,172,0,0,2736,2747,3,240,120,5,2737,2738,5,90,0, + 0,2738,2739,5,2,0,0,2739,2740,3,54,27,0,2740,2741,5,3,0,0,2741,2747, + 1,0,0,0,2742,2744,3,244,122,0,2743,2745,3,242,121,0,2744,2743,1, + 0,0,0,2744,2745,1,0,0,0,2745,2747,1,0,0,0,2746,2734,1,0,0,0,2746, + 2737,1,0,0,0,2746,2742,1,0,0,0,2747,2756,1,0,0,0,2748,2749,10,2, + 0,0,2749,2750,5,14,0,0,2750,2755,3,240,120,3,2751,2752,10,1,0,0, + 2752,2753,5,181,0,0,2753,2755,3,240,120,2,2754,2748,1,0,0,0,2754, + 2751,1,0,0,0,2755,2758,1,0,0,0,2756,2754,1,0,0,0,2756,2757,1,0,0, + 0,2757,241,1,0,0,0,2758,2756,1,0,0,0,2759,2761,5,172,0,0,2760,2759, + 1,0,0,0,2760,2761,1,0,0,0,2761,2762,1,0,0,0,2762,2763,5,24,0,0,2763, + 2764,3,244,122,0,2764,2765,5,14,0,0,2765,2766,3,244,122,0,2766,2842, + 1,0,0,0,2767,2769,5,172,0,0,2768,2767,1,0,0,0,2768,2769,1,0,0,0, + 2769,2770,1,0,0,0,2770,2771,5,122,0,0,2771,2772,5,2,0,0,2772,2777, + 3,236,118,0,2773,2774,5,4,0,0,2774,2776,3,236,118,0,2775,2773,1, + 0,0,0,2776,2779,1,0,0,0,2777,2775,1,0,0,0,2777,2778,1,0,0,0,2778, + 2780,1,0,0,0,2779,2777,1,0,0,0,2780,2781,5,3,0,0,2781,2842,1,0,0, + 0,2782,2784,5,172,0,0,2783,2782,1,0,0,0,2783,2784,1,0,0,0,2784,2785, + 1,0,0,0,2785,2786,5,122,0,0,2786,2787,5,2,0,0,2787,2788,3,54,27, + 0,2788,2789,5,3,0,0,2789,2842,1,0,0,0,2790,2792,5,172,0,0,2791,2790, + 1,0,0,0,2791,2792,1,0,0,0,2792,2793,1,0,0,0,2793,2794,5,222,0,0, + 2794,2842,3,244,122,0,2795,2797,5,172,0,0,2796,2795,1,0,0,0,2796, + 2797,1,0,0,0,2797,2798,1,0,0,0,2798,2799,7,22,0,0,2799,2813,7,23, + 0,0,2800,2801,5,2,0,0,2801,2814,5,3,0,0,2802,2803,5,2,0,0,2803,2808, + 3,236,118,0,2804,2805,5,4,0,0,2805,2807,3,236,118,0,2806,2804,1, + 0,0,0,2807,2810,1,0,0,0,2808,2806,1,0,0,0,2808,2809,1,0,0,0,2809, + 2811,1,0,0,0,2810,2808,1,0,0,0,2811,2812,5,3,0,0,2812,2814,1,0,0, + 0,2813,2800,1,0,0,0,2813,2802,1,0,0,0,2814,2842,1,0,0,0,2815,2817, + 5,172,0,0,2816,2815,1,0,0,0,2816,2817,1,0,0,0,2817,2818,1,0,0,0, + 2818,2819,7,22,0,0,2819,2822,3,244,122,0,2820,2821,5,85,0,0,2821, + 2823,3,338,169,0,2822,2820,1,0,0,0,2822,2823,1,0,0,0,2823,2842,1, + 0,0,0,2824,2826,5,133,0,0,2825,2827,5,172,0,0,2826,2825,1,0,0,0, + 2826,2827,1,0,0,0,2827,2828,1,0,0,0,2828,2842,5,173,0,0,2829,2831, + 5,133,0,0,2830,2832,5,172,0,0,2831,2830,1,0,0,0,2831,2832,1,0,0, + 0,2832,2833,1,0,0,0,2833,2842,7,24,0,0,2834,2836,5,133,0,0,2835, + 2837,5,172,0,0,2836,2835,1,0,0,0,2836,2837,1,0,0,0,2837,2838,1,0, + 0,0,2838,2839,5,79,0,0,2839,2840,5,107,0,0,2840,2842,3,244,122,0, + 2841,2760,1,0,0,0,2841,2768,1,0,0,0,2841,2783,1,0,0,0,2841,2791, + 1,0,0,0,2841,2796,1,0,0,0,2841,2816,1,0,0,0,2841,2824,1,0,0,0,2841, + 2829,1,0,0,0,2841,2834,1,0,0,0,2842,243,1,0,0,0,2843,2844,6,122, + -1,0,2844,2848,3,248,124,0,2845,2846,7,25,0,0,2846,2848,3,244,122, + 7,2847,2843,1,0,0,0,2847,2845,1,0,0,0,2848,2870,1,0,0,0,2849,2850, + 10,6,0,0,2850,2851,7,26,0,0,2851,2869,3,244,122,7,2852,2853,10,5, + 0,0,2853,2854,7,27,0,0,2854,2869,3,244,122,6,2855,2856,10,4,0,0, + 2856,2857,5,322,0,0,2857,2869,3,244,122,5,2858,2859,10,3,0,0,2859, + 2860,5,325,0,0,2860,2869,3,244,122,4,2861,2862,10,2,0,0,2862,2863, + 5,323,0,0,2863,2869,3,244,122,3,2864,2865,10,1,0,0,2865,2866,3,252, + 126,0,2866,2867,3,244,122,2,2867,2869,1,0,0,0,2868,2849,1,0,0,0, + 2868,2852,1,0,0,0,2868,2855,1,0,0,0,2868,2858,1,0,0,0,2868,2861, + 1,0,0,0,2868,2864,1,0,0,0,2869,2872,1,0,0,0,2870,2868,1,0,0,0,2870, + 2871,1,0,0,0,2871,245,1,0,0,0,2872,2870,1,0,0,0,2873,2874,7,28,0, + 0,2874,247,1,0,0,0,2875,2876,6,124,-1,0,2876,3114,7,29,0,0,2877, + 2878,7,30,0,0,2878,2879,5,2,0,0,2879,2880,3,246,123,0,2880,2881, + 5,4,0,0,2881,2882,3,244,122,0,2882,2883,5,4,0,0,2883,2884,3,244, + 122,0,2884,2885,5,3,0,0,2885,3114,1,0,0,0,2886,2887,7,31,0,0,2887, + 2888,5,2,0,0,2888,2889,3,246,123,0,2889,2890,5,4,0,0,2890,2891,3, + 244,122,0,2891,2892,5,4,0,0,2892,2893,3,244,122,0,2893,2894,5,3, + 0,0,2894,3114,1,0,0,0,2895,2897,5,31,0,0,2896,2898,3,304,152,0,2897, + 2896,1,0,0,0,2898,2899,1,0,0,0,2899,2897,1,0,0,0,2899,2900,1,0,0, + 0,2900,2903,1,0,0,0,2901,2902,5,83,0,0,2902,2904,3,236,118,0,2903, + 2901,1,0,0,0,2903,2904,1,0,0,0,2904,2905,1,0,0,0,2905,2906,5,84, + 0,0,2906,3114,1,0,0,0,2907,2908,5,31,0,0,2908,2910,3,236,118,0,2909, + 2911,3,304,152,0,2910,2909,1,0,0,0,2911,2912,1,0,0,0,2912,2910,1, + 0,0,0,2912,2913,1,0,0,0,2913,2916,1,0,0,0,2914,2915,5,83,0,0,2915, + 2917,3,236,118,0,2916,2914,1,0,0,0,2916,2917,1,0,0,0,2917,2918,1, + 0,0,0,2918,2919,5,84,0,0,2919,3114,1,0,0,0,2920,2921,7,32,0,0,2921, + 2922,5,2,0,0,2922,2923,3,236,118,0,2923,2924,5,20,0,0,2924,2925, + 3,278,139,0,2925,2926,5,3,0,0,2926,3114,1,0,0,0,2927,2928,5,252, + 0,0,2928,2937,5,2,0,0,2929,2934,3,224,112,0,2930,2931,5,4,0,0,2931, + 2933,3,224,112,0,2932,2930,1,0,0,0,2933,2936,1,0,0,0,2934,2932,1, + 0,0,0,2934,2935,1,0,0,0,2935,2938,1,0,0,0,2936,2934,1,0,0,0,2937, + 2929,1,0,0,0,2937,2938,1,0,0,0,2938,2939,1,0,0,0,2939,3114,5,3,0, + 0,2940,2941,5,101,0,0,2941,2942,5,2,0,0,2942,2945,3,236,118,0,2943, + 2944,5,120,0,0,2944,2946,5,174,0,0,2945,2943,1,0,0,0,2945,2946,1, + 0,0,0,2946,2947,1,0,0,0,2947,2948,5,3,0,0,2948,3114,1,0,0,0,2949, + 2950,5,17,0,0,2950,2951,5,2,0,0,2951,2954,3,236,118,0,2952,2953, + 5,120,0,0,2953,2955,5,174,0,0,2954,2952,1,0,0,0,2954,2955,1,0,0, + 0,2955,2956,1,0,0,0,2956,2957,5,3,0,0,2957,3114,1,0,0,0,2958,2959, + 5,137,0,0,2959,2960,5,2,0,0,2960,2963,3,236,118,0,2961,2962,5,120, + 0,0,2962,2964,5,174,0,0,2963,2961,1,0,0,0,2963,2964,1,0,0,0,2964, + 2965,1,0,0,0,2965,2966,5,3,0,0,2966,3114,1,0,0,0,2967,2968,5,198, + 0,0,2968,2969,5,2,0,0,2969,2970,3,244,122,0,2970,2971,5,122,0,0, + 2971,2972,3,244,122,0,2972,2973,5,3,0,0,2973,3114,1,0,0,0,2974,3114, + 3,250,125,0,2975,3114,5,318,0,0,2976,2977,3,320,160,0,2977,2978, + 5,5,0,0,2978,2979,5,318,0,0,2979,3114,1,0,0,0,2980,2981,5,2,0,0, + 2981,2984,3,224,112,0,2982,2983,5,4,0,0,2983,2985,3,224,112,0,2984, + 2982,1,0,0,0,2985,2986,1,0,0,0,2986,2984,1,0,0,0,2986,2987,1,0,0, + 0,2987,2988,1,0,0,0,2988,2989,5,3,0,0,2989,3114,1,0,0,0,2990,2991, + 5,2,0,0,2991,2992,3,54,27,0,2992,2993,5,3,0,0,2993,3114,1,0,0,0, + 2994,2995,3,318,159,0,2995,3007,5,2,0,0,2996,2998,3,174,87,0,2997, + 2996,1,0,0,0,2997,2998,1,0,0,0,2998,2999,1,0,0,0,2999,3004,3,236, + 118,0,3000,3001,5,4,0,0,3001,3003,3,236,118,0,3002,3000,1,0,0,0, + 3003,3006,1,0,0,0,3004,3002,1,0,0,0,3004,3005,1,0,0,0,3005,3008, + 1,0,0,0,3006,3004,1,0,0,0,3007,2997,1,0,0,0,3007,3008,1,0,0,0,3008, + 3009,1,0,0,0,3009,3016,5,3,0,0,3010,3011,5,99,0,0,3011,3012,5,2, + 0,0,3012,3013,5,301,0,0,3013,3014,3,240,120,0,3014,3015,5,3,0,0, + 3015,3017,1,0,0,0,3016,3010,1,0,0,0,3016,3017,1,0,0,0,3017,3020, + 1,0,0,0,3018,3019,7,33,0,0,3019,3021,5,174,0,0,3020,3018,1,0,0,0, + 3020,3021,1,0,0,0,3021,3024,1,0,0,0,3022,3023,5,186,0,0,3023,3025, + 3,310,155,0,3024,3022,1,0,0,0,3024,3025,1,0,0,0,3025,3114,1,0,0, + 0,3026,3027,3,326,163,0,3027,3028,5,327,0,0,3028,3029,3,236,118, + 0,3029,3114,1,0,0,0,3030,3031,5,2,0,0,3031,3034,3,326,163,0,3032, + 3033,5,4,0,0,3033,3035,3,326,163,0,3034,3032,1,0,0,0,3035,3036,1, + 0,0,0,3036,3034,1,0,0,0,3036,3037,1,0,0,0,3037,3038,1,0,0,0,3038, + 3039,5,3,0,0,3039,3040,5,327,0,0,3040,3041,3,236,118,0,3041,3114, + 1,0,0,0,3042,3114,3,326,163,0,3043,3044,5,2,0,0,3044,3045,3,236, + 118,0,3045,3046,5,3,0,0,3046,3114,1,0,0,0,3047,3048,5,95,0,0,3048, + 3049,5,2,0,0,3049,3050,3,326,163,0,3050,3051,5,107,0,0,3051,3052, + 3,244,122,0,3052,3053,5,3,0,0,3053,3114,1,0,0,0,3054,3055,7,34,0, + 0,3055,3056,5,2,0,0,3056,3057,3,244,122,0,3057,3058,7,35,0,0,3058, + 3061,3,244,122,0,3059,3060,7,36,0,0,3060,3062,3,244,122,0,3061,3059, + 1,0,0,0,3061,3062,1,0,0,0,3062,3063,1,0,0,0,3063,3064,5,3,0,0,3064, + 3114,1,0,0,0,3065,3066,5,276,0,0,3066,3068,5,2,0,0,3067,3069,7,37, + 0,0,3068,3067,1,0,0,0,3068,3069,1,0,0,0,3069,3071,1,0,0,0,3070,3072, + 3,244,122,0,3071,3070,1,0,0,0,3071,3072,1,0,0,0,3072,3073,1,0,0, + 0,3073,3074,5,107,0,0,3074,3075,3,244,122,0,3075,3076,5,3,0,0,3076, + 3114,1,0,0,0,3077,3078,5,188,0,0,3078,3079,5,2,0,0,3079,3080,3,244, + 122,0,3080,3081,5,197,0,0,3081,3082,3,244,122,0,3082,3083,5,107, + 0,0,3083,3086,3,244,122,0,3084,3085,5,103,0,0,3085,3087,3,244,122, + 0,3086,3084,1,0,0,0,3086,3087,1,0,0,0,3087,3088,1,0,0,0,3088,3089, + 5,3,0,0,3089,3114,1,0,0,0,3090,3091,7,38,0,0,3091,3092,5,2,0,0,3092, + 3093,3,244,122,0,3093,3094,5,3,0,0,3094,3095,5,304,0,0,3095,3096, + 5,114,0,0,3096,3097,5,2,0,0,3097,3098,5,182,0,0,3098,3099,5,28,0, + 0,3099,3100,3,94,47,0,3100,3107,5,3,0,0,3101,3102,5,99,0,0,3102, + 3103,5,2,0,0,3103,3104,5,301,0,0,3104,3105,3,240,120,0,3105,3106, + 5,3,0,0,3106,3108,1,0,0,0,3107,3101,1,0,0,0,3107,3108,1,0,0,0,3108, + 3111,1,0,0,0,3109,3110,5,186,0,0,3110,3112,3,310,155,0,3111,3109, + 1,0,0,0,3111,3112,1,0,0,0,3112,3114,1,0,0,0,3113,2875,1,0,0,0,3113, + 2877,1,0,0,0,3113,2886,1,0,0,0,3113,2895,1,0,0,0,3113,2907,1,0,0, + 0,3113,2920,1,0,0,0,3113,2927,1,0,0,0,3113,2940,1,0,0,0,3113,2949, + 1,0,0,0,3113,2958,1,0,0,0,3113,2967,1,0,0,0,3113,2974,1,0,0,0,3113, + 2975,1,0,0,0,3113,2976,1,0,0,0,3113,2980,1,0,0,0,3113,2990,1,0,0, + 0,3113,2994,1,0,0,0,3113,3026,1,0,0,0,3113,3030,1,0,0,0,3113,3042, + 1,0,0,0,3113,3043,1,0,0,0,3113,3047,1,0,0,0,3113,3054,1,0,0,0,3113, + 3065,1,0,0,0,3113,3077,1,0,0,0,3113,3090,1,0,0,0,3114,3125,1,0,0, + 0,3115,3116,10,9,0,0,3116,3117,5,6,0,0,3117,3118,3,244,122,0,3118, + 3119,5,7,0,0,3119,3124,1,0,0,0,3120,3121,10,7,0,0,3121,3122,5,5, + 0,0,3122,3124,3,326,163,0,3123,3115,1,0,0,0,3123,3120,1,0,0,0,3124, + 3127,1,0,0,0,3125,3123,1,0,0,0,3125,3126,1,0,0,0,3126,249,1,0,0, + 0,3127,3125,1,0,0,0,3128,3143,5,173,0,0,3129,3130,5,326,0,0,3130, + 3143,3,326,163,0,3131,3143,3,260,130,0,3132,3133,3,326,163,0,3133, + 3134,3,338,169,0,3134,3143,1,0,0,0,3135,3143,3,334,167,0,3136,3143, + 3,258,129,0,3137,3139,3,338,169,0,3138,3137,1,0,0,0,3139,3140,1, + 0,0,0,3140,3138,1,0,0,0,3140,3141,1,0,0,0,3141,3143,1,0,0,0,3142, + 3128,1,0,0,0,3142,3129,1,0,0,0,3142,3131,1,0,0,0,3142,3132,1,0,0, + 0,3142,3135,1,0,0,0,3142,3136,1,0,0,0,3142,3138,1,0,0,0,3143,251, + 1,0,0,0,3144,3145,7,39,0,0,3145,253,1,0,0,0,3146,3147,7,40,0,0,3147, + 255,1,0,0,0,3148,3149,7,41,0,0,3149,257,1,0,0,0,3150,3151,7,42,0, + 0,3151,259,1,0,0,0,3152,3155,5,131,0,0,3153,3156,3,262,131,0,3154, + 3156,3,266,133,0,3155,3153,1,0,0,0,3155,3154,1,0,0,0,3156,261,1, + 0,0,0,3157,3159,3,264,132,0,3158,3160,3,268,134,0,3159,3158,1,0, + 0,0,3159,3160,1,0,0,0,3160,263,1,0,0,0,3161,3162,3,270,135,0,3162, + 3163,3,272,136,0,3163,3165,1,0,0,0,3164,3161,1,0,0,0,3165,3166,1, + 0,0,0,3166,3164,1,0,0,0,3166,3167,1,0,0,0,3167,265,1,0,0,0,3168, + 3171,3,268,134,0,3169,3172,3,264,132,0,3170,3172,3,268,134,0,3171, + 3169,1,0,0,0,3171,3170,1,0,0,0,3171,3172,1,0,0,0,3172,267,1,0,0, + 0,3173,3174,3,270,135,0,3174,3175,3,274,137,0,3175,3176,5,270,0, + 0,3176,3177,3,274,137,0,3177,269,1,0,0,0,3178,3180,7,43,0,0,3179, + 3178,1,0,0,0,3179,3180,1,0,0,0,3180,3184,1,0,0,0,3181,3185,5,335, + 0,0,3182,3185,5,337,0,0,3183,3185,3,338,169,0,3184,3181,1,0,0,0, + 3184,3182,1,0,0,0,3184,3183,1,0,0,0,3185,271,1,0,0,0,3186,3187,7, + 44,0,0,3187,273,1,0,0,0,3188,3189,7,45,0,0,3189,275,1,0,0,0,3190, + 3194,5,101,0,0,3191,3192,5,9,0,0,3192,3194,3,322,161,0,3193,3190, + 1,0,0,0,3193,3191,1,0,0,0,3194,277,1,0,0,0,3195,3196,5,19,0,0,3196, + 3197,5,312,0,0,3197,3198,3,278,139,0,3198,3199,5,314,0,0,3199,3242, + 1,0,0,0,3200,3201,5,154,0,0,3201,3202,5,312,0,0,3202,3203,3,278, + 139,0,3203,3204,5,4,0,0,3204,3205,3,278,139,0,3205,3206,5,314,0, + 0,3206,3242,1,0,0,0,3207,3214,5,252,0,0,3208,3210,5,312,0,0,3209, + 3211,3,300,150,0,3210,3209,1,0,0,0,3210,3211,1,0,0,0,3211,3212,1, + 0,0,0,3212,3215,5,314,0,0,3213,3215,5,310,0,0,3214,3208,1,0,0,0, + 3214,3213,1,0,0,0,3215,3242,1,0,0,0,3216,3217,5,131,0,0,3217,3220, + 7,46,0,0,3218,3219,5,270,0,0,3219,3221,5,163,0,0,3220,3218,1,0,0, + 0,3220,3221,1,0,0,0,3221,3242,1,0,0,0,3222,3223,5,131,0,0,3223,3226, + 7,47,0,0,3224,3225,5,270,0,0,3225,3227,7,48,0,0,3226,3224,1,0,0, + 0,3226,3227,1,0,0,0,3227,3242,1,0,0,0,3228,3239,3,326,163,0,3229, + 3230,5,2,0,0,3230,3235,5,335,0,0,3231,3232,5,4,0,0,3232,3234,5,335, + 0,0,3233,3231,1,0,0,0,3234,3237,1,0,0,0,3235,3233,1,0,0,0,3235,3236, + 1,0,0,0,3236,3238,1,0,0,0,3237,3235,1,0,0,0,3238,3240,5,3,0,0,3239, + 3229,1,0,0,0,3239,3240,1,0,0,0,3240,3242,1,0,0,0,3241,3195,1,0,0, + 0,3241,3200,1,0,0,0,3241,3207,1,0,0,0,3241,3216,1,0,0,0,3241,3222, + 1,0,0,0,3241,3228,1,0,0,0,3242,279,1,0,0,0,3243,3248,3,282,141,0, + 3244,3245,5,4,0,0,3245,3247,3,282,141,0,3246,3244,1,0,0,0,3247,3250, + 1,0,0,0,3248,3246,1,0,0,0,3248,3249,1,0,0,0,3249,281,1,0,0,0,3250, + 3248,1,0,0,0,3251,3252,3,214,107,0,3252,3256,3,278,139,0,3253,3255, + 3,284,142,0,3254,3253,1,0,0,0,3255,3258,1,0,0,0,3256,3254,1,0,0, + 0,3256,3257,1,0,0,0,3257,283,1,0,0,0,3258,3256,1,0,0,0,3259,3260, + 5,172,0,0,3260,3265,5,173,0,0,3261,3265,3,286,143,0,3262,3265,3, + 34,17,0,3263,3265,3,276,138,0,3264,3259,1,0,0,0,3264,3261,1,0,0, + 0,3264,3262,1,0,0,0,3264,3263,1,0,0,0,3265,285,1,0,0,0,3266,3267, + 5,70,0,0,3267,3268,3,236,118,0,3268,287,1,0,0,0,3269,3274,3,290, + 145,0,3270,3271,5,4,0,0,3271,3273,3,290,145,0,3272,3270,1,0,0,0, + 3273,3276,1,0,0,0,3274,3272,1,0,0,0,3274,3275,1,0,0,0,3275,289,1, + 0,0,0,3276,3274,1,0,0,0,3277,3278,3,322,161,0,3278,3281,3,278,139, + 0,3279,3280,5,172,0,0,3280,3282,5,173,0,0,3281,3279,1,0,0,0,3281, + 3282,1,0,0,0,3282,3284,1,0,0,0,3283,3285,3,34,17,0,3284,3283,1,0, + 0,0,3284,3285,1,0,0,0,3285,291,1,0,0,0,3286,3291,3,294,147,0,3287, + 3288,5,4,0,0,3288,3290,3,294,147,0,3289,3287,1,0,0,0,3290,3293,1, + 0,0,0,3291,3289,1,0,0,0,3291,3292,1,0,0,0,3292,293,1,0,0,0,3293, + 3291,1,0,0,0,3294,3295,3,322,161,0,3295,3299,3,278,139,0,3296,3298, + 3,296,148,0,3297,3296,1,0,0,0,3298,3301,1,0,0,0,3299,3297,1,0,0, + 0,3299,3300,1,0,0,0,3300,295,1,0,0,0,3301,3299,1,0,0,0,3302,3303, + 5,172,0,0,3303,3308,5,173,0,0,3304,3308,3,286,143,0,3305,3308,3, + 298,149,0,3306,3308,3,34,17,0,3307,3302,1,0,0,0,3307,3304,1,0,0, + 0,3307,3305,1,0,0,0,3307,3306,1,0,0,0,3308,297,1,0,0,0,3309,3310, + 5,111,0,0,3310,3311,5,12,0,0,3311,3312,5,20,0,0,3312,3313,5,2,0, + 0,3313,3314,3,236,118,0,3314,3315,5,3,0,0,3315,299,1,0,0,0,3316, + 3321,3,302,151,0,3317,3318,5,4,0,0,3318,3320,3,302,151,0,3319,3317, + 1,0,0,0,3320,3323,1,0,0,0,3321,3319,1,0,0,0,3321,3322,1,0,0,0,3322, + 301,1,0,0,0,3323,3321,1,0,0,0,3324,3326,3,326,163,0,3325,3327,5, + 326,0,0,3326,3325,1,0,0,0,3326,3327,1,0,0,0,3327,3328,1,0,0,0,3328, + 3331,3,278,139,0,3329,3330,5,172,0,0,3330,3332,5,173,0,0,3331,3329, + 1,0,0,0,3331,3332,1,0,0,0,3332,3334,1,0,0,0,3333,3335,3,34,17,0, + 3334,3333,1,0,0,0,3334,3335,1,0,0,0,3335,303,1,0,0,0,3336,3337,5, + 300,0,0,3337,3338,3,236,118,0,3338,3339,5,265,0,0,3339,3340,3,236, + 118,0,3340,305,1,0,0,0,3341,3342,5,302,0,0,3342,3347,3,308,154,0, + 3343,3344,5,4,0,0,3344,3346,3,308,154,0,3345,3343,1,0,0,0,3346,3349, + 1,0,0,0,3347,3345,1,0,0,0,3347,3348,1,0,0,0,3348,307,1,0,0,0,3349, + 3347,1,0,0,0,3350,3351,3,322,161,0,3351,3352,5,20,0,0,3352,3353, + 3,310,155,0,3353,309,1,0,0,0,3354,3401,3,322,161,0,3355,3356,5,2, + 0,0,3356,3357,3,322,161,0,3357,3358,5,3,0,0,3358,3401,1,0,0,0,3359, + 3394,5,2,0,0,3360,3361,5,38,0,0,3361,3362,5,28,0,0,3362,3367,3,236, + 118,0,3363,3364,5,4,0,0,3364,3366,3,236,118,0,3365,3363,1,0,0,0, + 3366,3369,1,0,0,0,3367,3365,1,0,0,0,3367,3368,1,0,0,0,3368,3395, + 1,0,0,0,3369,3367,1,0,0,0,3370,3371,7,49,0,0,3371,3372,5,28,0,0, + 3372,3377,3,236,118,0,3373,3374,5,4,0,0,3374,3376,3,236,118,0,3375, + 3373,1,0,0,0,3376,3379,1,0,0,0,3377,3375,1,0,0,0,3377,3378,1,0,0, + 0,3378,3381,1,0,0,0,3379,3377,1,0,0,0,3380,3370,1,0,0,0,3380,3381, + 1,0,0,0,3381,3392,1,0,0,0,3382,3383,7,50,0,0,3383,3384,5,28,0,0, + 3384,3389,3,94,47,0,3385,3386,5,4,0,0,3386,3388,3,94,47,0,3387,3385, + 1,0,0,0,3388,3391,1,0,0,0,3389,3387,1,0,0,0,3389,3390,1,0,0,0,3390, + 3393,1,0,0,0,3391,3389,1,0,0,0,3392,3382,1,0,0,0,3392,3393,1,0,0, + 0,3393,3395,1,0,0,0,3394,3360,1,0,0,0,3394,3380,1,0,0,0,3395,3397, + 1,0,0,0,3396,3398,3,312,156,0,3397,3396,1,0,0,0,3397,3398,1,0,0, + 0,3398,3399,1,0,0,0,3399,3401,5,3,0,0,3400,3354,1,0,0,0,3400,3355, + 1,0,0,0,3400,3359,1,0,0,0,3401,311,1,0,0,0,3402,3403,5,206,0,0,3403, + 3419,3,314,157,0,3404,3405,5,228,0,0,3405,3419,3,314,157,0,3406, + 3407,5,206,0,0,3407,3408,5,24,0,0,3408,3409,3,314,157,0,3409,3410, + 5,14,0,0,3410,3411,3,314,157,0,3411,3419,1,0,0,0,3412,3413,5,228, + 0,0,3413,3414,5,24,0,0,3414,3415,3,314,157,0,3415,3416,5,14,0,0, + 3416,3417,3,314,157,0,3417,3419,1,0,0,0,3418,3402,1,0,0,0,3418,3404, + 1,0,0,0,3418,3406,1,0,0,0,3418,3412,1,0,0,0,3419,313,1,0,0,0,3420, + 3421,5,282,0,0,3421,3428,7,51,0,0,3422,3423,5,56,0,0,3423,3428,5, + 227,0,0,3424,3425,3,236,118,0,3425,3426,7,51,0,0,3426,3428,1,0,0, + 0,3427,3420,1,0,0,0,3427,3422,1,0,0,0,3427,3424,1,0,0,0,3428,315, + 1,0,0,0,3429,3434,3,320,160,0,3430,3431,5,4,0,0,3431,3433,3,320, + 160,0,3432,3430,1,0,0,0,3433,3436,1,0,0,0,3434,3432,1,0,0,0,3434, + 3435,1,0,0,0,3435,317,1,0,0,0,3436,3434,1,0,0,0,3437,3442,3,320, + 160,0,3438,3442,5,99,0,0,3439,3442,5,141,0,0,3440,3442,5,221,0,0, + 3441,3437,1,0,0,0,3441,3438,1,0,0,0,3441,3439,1,0,0,0,3441,3440, + 1,0,0,0,3442,319,1,0,0,0,3443,3448,3,326,163,0,3444,3445,5,5,0,0, + 3445,3447,3,326,163,0,3446,3444,1,0,0,0,3447,3450,1,0,0,0,3448,3446, + 1,0,0,0,3448,3449,1,0,0,0,3449,321,1,0,0,0,3450,3448,1,0,0,0,3451, + 3452,3,326,163,0,3452,3453,3,324,162,0,3453,323,1,0,0,0,3454,3455, + 5,317,0,0,3455,3457,3,326,163,0,3456,3454,1,0,0,0,3457,3458,1,0, + 0,0,3458,3456,1,0,0,0,3458,3459,1,0,0,0,3459,3462,1,0,0,0,3460,3462, + 1,0,0,0,3461,3456,1,0,0,0,3461,3460,1,0,0,0,3462,325,1,0,0,0,3463, + 3466,3,328,164,0,3464,3466,3,346,173,0,3465,3463,1,0,0,0,3465,3464, + 1,0,0,0,3466,327,1,0,0,0,3467,3472,5,341,0,0,3468,3472,3,330,165, + 0,3469,3472,3,344,172,0,3470,3472,3,348,174,0,3471,3467,1,0,0,0, + 3471,3468,1,0,0,0,3471,3469,1,0,0,0,3471,3470,1,0,0,0,3472,329,1, + 0,0,0,3473,3474,7,52,0,0,3474,331,1,0,0,0,3475,3476,5,342,0,0,3476, + 333,1,0,0,0,3477,3479,5,317,0,0,3478,3477,1,0,0,0,3478,3479,1,0, + 0,0,3479,3480,1,0,0,0,3480,3518,5,336,0,0,3481,3483,5,317,0,0,3482, + 3481,1,0,0,0,3482,3483,1,0,0,0,3483,3484,1,0,0,0,3484,3518,5,337, + 0,0,3485,3487,5,317,0,0,3486,3485,1,0,0,0,3486,3487,1,0,0,0,3487, + 3488,1,0,0,0,3488,3518,7,53,0,0,3489,3491,5,317,0,0,3490,3489,1, + 0,0,0,3490,3491,1,0,0,0,3491,3492,1,0,0,0,3492,3518,5,335,0,0,3493, + 3495,5,317,0,0,3494,3493,1,0,0,0,3494,3495,1,0,0,0,3495,3496,1,0, + 0,0,3496,3518,5,332,0,0,3497,3499,5,317,0,0,3498,3497,1,0,0,0,3498, + 3499,1,0,0,0,3499,3500,1,0,0,0,3500,3518,5,333,0,0,3501,3503,5,317, + 0,0,3502,3501,1,0,0,0,3502,3503,1,0,0,0,3503,3504,1,0,0,0,3504,3518, + 5,334,0,0,3505,3507,5,317,0,0,3506,3505,1,0,0,0,3506,3507,1,0,0, + 0,3507,3508,1,0,0,0,3508,3518,5,339,0,0,3509,3511,5,317,0,0,3510, + 3509,1,0,0,0,3510,3511,1,0,0,0,3511,3512,1,0,0,0,3512,3518,5,338, + 0,0,3513,3515,5,317,0,0,3514,3513,1,0,0,0,3514,3515,1,0,0,0,3515, + 3516,1,0,0,0,3516,3518,5,340,0,0,3517,3478,1,0,0,0,3517,3482,1,0, + 0,0,3517,3486,1,0,0,0,3517,3490,1,0,0,0,3517,3494,1,0,0,0,3517,3498, + 1,0,0,0,3517,3502,1,0,0,0,3517,3506,1,0,0,0,3517,3510,1,0,0,0,3517, + 3514,1,0,0,0,3518,335,1,0,0,0,3519,3520,5,280,0,0,3520,3531,3,278, + 139,0,3521,3531,3,34,17,0,3522,3531,3,276,138,0,3523,3524,7,54,0, + 0,3524,3525,5,172,0,0,3525,3531,5,173,0,0,3526,3527,5,239,0,0,3527, + 3531,3,286,143,0,3528,3529,5,82,0,0,3529,3531,5,70,0,0,3530,3519, + 1,0,0,0,3530,3521,1,0,0,0,3530,3522,1,0,0,0,3530,3523,1,0,0,0,3530, + 3526,1,0,0,0,3530,3528,1,0,0,0,3531,337,1,0,0,0,3532,3533,7,55,0, + 0,3533,339,1,0,0,0,3534,3537,3,338,169,0,3535,3537,5,173,0,0,3536, + 3534,1,0,0,0,3536,3535,1,0,0,0,3537,341,1,0,0,0,3538,3541,5,335, + 0,0,3539,3541,3,338,169,0,3540,3538,1,0,0,0,3540,3539,1,0,0,0,3541, + 343,1,0,0,0,3542,3543,7,56,0,0,3543,345,1,0,0,0,3544,3545,7,57,0, + 0,3545,347,1,0,0,0,3546,3547,7,58,0,0,3547,349,1,0,0,0,460,354,379, + 392,399,407,409,429,433,439,442,445,452,455,459,462,469,480,482, + 490,493,497,500,506,517,523,528,562,575,600,609,613,619,623,628, + 634,646,654,660,673,678,694,701,705,711,726,730,736,742,745,748, + 754,758,766,768,777,780,789,794,800,807,810,816,827,830,834,839, + 844,851,854,857,864,869,878,886,892,895,898,904,908,913,916,920, + 922,930,938,941,946,952,958,961,965,968,972,1000,1003,1007,1013, + 1016,1019,1025,1033,1038,1044,1050,1053,1060,1067,1075,1092,1106, + 1109,1115,1124,1133,1141,1146,1151,1158,1164,1169,1177,1180,1184, + 1196,1200,1207,1323,1331,1339,1348,1358,1362,1365,1371,1377,1389, + 1401,1406,1417,1424,1426,1429,1434,1438,1443,1446,1451,1460,1465, + 1468,1473,1477,1482,1484,1488,1497,1505,1511,1522,1529,1538,1542, + 1549,1552,1564,1569,1574,1579,1583,1586,1589,1592,1596,1601,1605, + 1608,1611,1614,1616,1627,1645,1647,1656,1663,1666,1673,1677,1683, + 1691,1702,1713,1721,1727,1739,1746,1753,1765,1773,1779,1785,1788, + 1797,1800,1809,1812,1821,1824,1833,1836,1839,1844,1846,1850,1857, + 1861,1867,1871,1879,1883,1886,1889,1892,1896,1902,1909,1914,1917, + 1920,1924,1934,1938,1940,1943,1947,1953,1957,1968,1978,1982,1994, + 2006,2021,2026,2032,2039,2055,2060,2073,2078,2086,2092,2096,2099, + 2102,2109,2115,2124,2134,2149,2154,2156,2160,2169,2182,2187,2191, + 2199,2202,2206,2220,2233,2238,2242,2245,2249,2255,2258,2265,2277, + 2288,2301,2312,2317,2325,2330,2337,2346,2349,2354,2361,2364,2369, + 2375,2381,2386,2390,2396,2400,2403,2408,2411,2416,2420,2423,2426, + 2432,2437,2444,2447,2465,2467,2470,2481,2490,2497,2505,2512,2516, + 2519,2527,2535,2541,2549,2561,2564,2570,2574,2576,2585,2597,2599, + 2606,2613,2619,2625,2627,2634,2642,2650,2656,2661,2668,2674,2678, + 2680,2687,2696,2703,2713,2718,2722,2731,2744,2746,2754,2756,2760, + 2768,2777,2783,2791,2796,2808,2813,2816,2822,2826,2831,2836,2841, + 2847,2868,2870,2899,2903,2912,2916,2934,2937,2945,2954,2963,2986, + 2997,3004,3007,3016,3020,3024,3036,3061,3068,3071,3086,3107,3111, + 3113,3123,3125,3140,3142,3155,3159,3166,3171,3179,3184,3193,3210, + 3214,3220,3226,3235,3239,3241,3248,3256,3264,3274,3281,3284,3291, + 3299,3307,3321,3326,3331,3334,3347,3367,3377,3380,3389,3392,3394, + 3397,3400,3418,3427,3434,3441,3448,3458,3461,3465,3471,3478,3482, + 3486,3490,3494,3498,3502,3506,3510,3514,3517,3530,3536,3540 + ] + +class SqlBaseParser ( Parser ): + + grammarFileName = "SqlBaseParser.g4" + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + sharedContextCache = PredictionContextCache() + + literalNames = [ "", "';'", "'('", "')'", "','", "'.'", "'['", + "']'", "'ADD'", "'AFTER'", "'ALL'", "'ALTER'", "'ALWAYS'", + "'ANALYZE'", "'AND'", "'ANTI'", "'ANY'", "'ANY_VALUE'", + "'ARCHIVE'", "'ARRAY'", "'AS'", "'ASC'", "'AT'", "'AUTHORIZATION'", + "'BETWEEN'", "'BOTH'", "'BUCKET'", "'BUCKETS'", "'BY'", + "'CACHE'", "'CASCADE'", "'CASE'", "'CAST'", "'CATALOG'", + "'CATALOGS'", "'CHANGE'", "'CHECK'", "'CLEAR'", "'CLUSTER'", + "'CLUSTERED'", "'CODEGEN'", "'COLLATE'", "'COLLECTION'", + "'COLUMN'", "'COLUMNS'", "'COMMENT'", "'COMMIT'", "'COMPACT'", + "'COMPACTIONS'", "'COMPUTE'", "'CONCATENATE'", "'CONSTRAINT'", + "'COST'", "'CREATE'", "'CROSS'", "'CUBE'", "'CURRENT'", + "'CURRENT_DATE'", "'CURRENT_TIME'", "'CURRENT_TIMESTAMP'", + "'CURRENT_USER'", "'DAY'", "'DAYS'", "'DAYOFYEAR'", + "'DATA'", "'DATABASE'", "'DATABASES'", "'DATEADD'", + "'DATEDIFF'", "'DBPROPERTIES'", "'DEFAULT'", "'DEFINED'", + "'DELETE'", "'DELIMITED'", "'DESC'", "'DESCRIBE'", + "'DFS'", "'DIRECTORIES'", "'DIRECTORY'", "'DISTINCT'", + "'DISTRIBUTE'", "'DIV'", "'DROP'", "'ELSE'", "'END'", + "'ESCAPE'", "'ESCAPED'", "'EXCEPT'", "'EXCHANGE'", + "'EXCLUDE'", "'EXISTS'", "'EXPLAIN'", "'EXPORT'", "'EXTENDED'", + "'EXTERNAL'", "'EXTRACT'", "'FALSE'", "'FETCH'", "'FIELDS'", + "'FILTER'", "'FILEFORMAT'", "'FIRST'", "'FOLLOWING'", + "'FOR'", "'FOREIGN'", "'FORMAT'", "'FORMATTED'", "'FROM'", + "'FULL'", "'FUNCTION'", "'FUNCTIONS'", "'GENERATED'", + "'GLOBAL'", "'GRANT'", "'GROUP'", "'GROUPING'", "'HAVING'", + "'HOUR'", "'HOURS'", "'IF'", "'IGNORE'", "'IMPORT'", + "'IN'", "'INCLUDE'", "'INDEX'", "'INDEXES'", "'INNER'", + "'INPATH'", "'INPUTFORMAT'", "'INSERT'", "'INTERSECT'", + "'INTERVAL'", "'INTO'", "'IS'", "'ITEMS'", "'JOIN'", + "'KEYS'", "'LAST'", "'LATERAL'", "'LAZY'", "'LEADING'", + "'LEFT'", "'LIKE'", "'ILIKE'", "'LIMIT'", "'LINES'", + "'LIST'", "'LOAD'", "'LOCAL'", "'LOCATION'", "'LOCK'", + "'LOCKS'", "'LOGICAL'", "'MACRO'", "'MAP'", "'MATCHED'", + "'MERGE'", "'MICROSECOND'", "'MICROSECONDS'", "'MILLISECOND'", + "'MILLISECONDS'", "'MINUTE'", "'MINUTES'", "'MONTH'", + "'MONTHS'", "'MSCK'", "'NAMESPACE'", "'NAMESPACES'", + "'NANOSECOND'", "'NANOSECONDS'", "'NATURAL'", "'NO'", + "", "'NULL'", "'NULLS'", "'OF'", "'OFFSET'", + "'ON'", "'ONLY'", "'OPTION'", "'OPTIONS'", "'OR'", + "'ORDER'", "'OUT'", "'OUTER'", "'OUTPUTFORMAT'", "'OVER'", + "'OVERLAPS'", "'OVERLAY'", "'OVERWRITE'", "'PARTITION'", + "'PARTITIONED'", "'PARTITIONS'", "'PERCENTILE_CONT'", + "'PERCENTILE_DISC'", "'PERCENT'", "'PIVOT'", "'PLACING'", + "'POSITION'", "'PRECEDING'", "'PRIMARY'", "'PRINCIPALS'", + "'PROPERTIES'", "'PURGE'", "'QUARTER'", "'QUERY'", + "'RANGE'", "'RECORDREADER'", "'RECORDWRITER'", "'RECOVER'", + "'REDUCE'", "'REFERENCES'", "'REFRESH'", "'RENAME'", + "'REPAIR'", "'REPEATABLE'", "'REPLACE'", "'RESET'", + "'RESPECT'", "'RESTRICT'", "'REVOKE'", "'RIGHT'", "", + "'ROLE'", "'ROLES'", "'ROLLBACK'", "'ROLLUP'", "'ROW'", + "'ROWS'", "'SECOND'", "'SECONDS'", "'SCHEMA'", "'SCHEMAS'", + "'SELECT'", "'SEMI'", "'SEPARATED'", "'SERDE'", "'SERDEPROPERTIES'", + "'SESSION_USER'", "'SET'", "'MINUS'", "'SETS'", "'SHOW'", + "'SKEWED'", "'SOME'", "'SORT'", "'SORTED'", "'SOURCE'", + "'START'", "'STATISTICS'", "'STORED'", "'STRATIFY'", + "'STRUCT'", "'SUBSTR'", "'SUBSTRING'", "'SYNC'", "'SYSTEM_TIME'", + "'SYSTEM_VERSION'", "'TABLE'", "'TABLES'", "'TABLESAMPLE'", + "'TARGET'", "'TBLPROPERTIES'", "", "'TERMINATED'", + "'THEN'", "'TIME'", "'TIMESTAMP'", "'TIMESTAMPADD'", + "'TIMESTAMPDIFF'", "'TO'", "'TOUCH'", "'TRAILING'", + "'TRANSACTION'", "'TRANSACTIONS'", "'TRANSFORM'", "'TRIM'", + "'TRUE'", "'TRUNCATE'", "'TRY_CAST'", "'TYPE'", "'UNARCHIVE'", + "'UNBOUNDED'", "'UNCACHE'", "'UNION'", "'UNIQUE'", + "'UNKNOWN'", "'UNLOCK'", "'UNPIVOT'", "'UNSET'", "'UPDATE'", + "'USE'", "'USER'", "'USING'", "'VALUES'", "'VERSION'", + "'VIEW'", "'VIEWS'", "'WEEK'", "'WEEKS'", "'WHEN'", + "'WHERE'", "'WINDOW'", "'WITH'", "'WITHIN'", "'YEAR'", + "'YEARS'", "'ZONE'", "", "'<=>'", "'<>'", + "'!='", "'<'", "", "'>'", "", "'+'", + "'-'", "'*'", "'/'", "'%'", "'~'", "'&'", "'|'", "'||'", + "'^'", "':'", "'->'", "'/*+'", "'*/'" ] + + symbolicNames = [ "", "SEMICOLON", "LEFT_PAREN", "RIGHT_PAREN", + "COMMA", "DOT", "LEFT_BRACKET", "RIGHT_BRACKET", "ADD", + "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", "AND", + "ANTI", "ANY", "ANY_VALUE", "ARCHIVE", "ARRAY", "AS", + "ASC", "AT", "AUTHORIZATION", "BETWEEN", "BOTH", "BUCKET", + "BUCKETS", "BY", "CACHE", "CASCADE", "CASE", "CAST", + "CATALOG", "CATALOGS", "CHANGE", "CHECK", "CLEAR", + "CLUSTER", "CLUSTERED", "CODEGEN", "COLLATE", "COLLECTION", + "COLUMN", "COLUMNS", "COMMENT", "COMMIT", "COMPACT", + "COMPACTIONS", "COMPUTE", "CONCATENATE", "CONSTRAINT", + "COST", "CREATE", "CROSS", "CUBE", "CURRENT", "CURRENT_DATE", + "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER", + "DAY", "DAYS", "DAYOFYEAR", "DATA", "DATABASE", "DATABASES", + "DATEADD", "DATEDIFF", "DBPROPERTIES", "DEFAULT", + "DEFINED", "DELETE", "DELIMITED", "DESC", "DESCRIBE", + "DFS", "DIRECTORIES", "DIRECTORY", "DISTINCT", "DISTRIBUTE", + "DIV", "DROP", "ELSE", "END", "ESCAPE", "ESCAPED", + "EXCEPT", "EXCHANGE", "EXCLUDE", "EXISTS", "EXPLAIN", + "EXPORT", "EXTENDED", "EXTERNAL", "EXTRACT", "FALSE", + "FETCH", "FIELDS", "FILTER", "FILEFORMAT", "FIRST", + "FOLLOWING", "FOR", "FOREIGN", "FORMAT", "FORMATTED", + "FROM", "FULL", "FUNCTION", "FUNCTIONS", "GENERATED", + "GLOBAL", "GRANT", "GROUP", "GROUPING", "HAVING", + "HOUR", "HOURS", "IF", "IGNORE", "IMPORT", "IN", "INCLUDE", + "INDEX", "INDEXES", "INNER", "INPATH", "INPUTFORMAT", + "INSERT", "INTERSECT", "INTERVAL", "INTO", "IS", "ITEMS", + "JOIN", "KEYS", "LAST", "LATERAL", "LAZY", "LEADING", + "LEFT", "LIKE", "ILIKE", "LIMIT", "LINES", "LIST", + "LOAD", "LOCAL", "LOCATION", "LOCK", "LOCKS", "LOGICAL", + "MACRO", "MAP", "MATCHED", "MERGE", "MICROSECOND", + "MICROSECONDS", "MILLISECOND", "MILLISECONDS", "MINUTE", + "MINUTES", "MONTH", "MONTHS", "MSCK", "NAMESPACE", + "NAMESPACES", "NANOSECOND", "NANOSECONDS", "NATURAL", + "NO", "NOT", "NULL", "NULLS", "OF", "OFFSET", "ON", + "ONLY", "OPTION", "OPTIONS", "OR", "ORDER", "OUT", + "OUTER", "OUTPUTFORMAT", "OVER", "OVERLAPS", "OVERLAY", + "OVERWRITE", "PARTITION", "PARTITIONED", "PARTITIONS", + "PERCENTILE_CONT", "PERCENTILE_DISC", "PERCENTLIT", + "PIVOT", "PLACING", "POSITION", "PRECEDING", "PRIMARY", + "PRINCIPALS", "PROPERTIES", "PURGE", "QUARTER", "QUERY", + "RANGE", "RECORDREADER", "RECORDWRITER", "RECOVER", + "REDUCE", "REFERENCES", "REFRESH", "RENAME", "REPAIR", + "REPEATABLE", "REPLACE", "RESET", "RESPECT", "RESTRICT", + "REVOKE", "RIGHT", "RLIKE", "ROLE", "ROLES", "ROLLBACK", + "ROLLUP", "ROW", "ROWS", "SECOND", "SECONDS", "SCHEMA", + "SCHEMAS", "SELECT", "SEMI", "SEPARATED", "SERDE", + "SERDEPROPERTIES", "SESSION_USER", "SET", "SETMINUS", + "SETS", "SHOW", "SKEWED", "SOME", "SORT", "SORTED", + "SOURCE", "START", "STATISTICS", "STORED", "STRATIFY", + "STRUCT", "SUBSTR", "SUBSTRING", "SYNC", "SYSTEM_TIME", + "SYSTEM_VERSION", "TABLE", "TABLES", "TABLESAMPLE", + "TARGET", "TBLPROPERTIES", "TEMPORARY", "TERMINATED", + "THEN", "TIME", "TIMESTAMP", "TIMESTAMPADD", "TIMESTAMPDIFF", + "TO", "TOUCH", "TRAILING", "TRANSACTION", "TRANSACTIONS", + "TRANSFORM", "TRIM", "TRUE", "TRUNCATE", "TRY_CAST", + "TYPE", "UNARCHIVE", "UNBOUNDED", "UNCACHE", "UNION", + "UNIQUE", "UNKNOWN", "UNLOCK", "UNPIVOT", "UNSET", + "UPDATE", "USE", "USER", "USING", "VALUES", "VERSION", + "VIEW", "VIEWS", "WEEK", "WEEKS", "WHEN", "WHERE", + "WINDOW", "WITH", "WITHIN", "YEAR", "YEARS", "ZONE", + "EQ", "NSEQ", "NEQ", "NEQJ", "LT", "LTE", "GT", "GTE", + "PLUS", "MINUS", "ASTERISK", "SLASH", "PERCENT", "TILDE", + "AMPERSAND", "PIPE", "CONCAT_PIPE", "HAT", "COLON", + "ARROW", "HENT_START", "HENT_END", "STRING", "DOUBLEQUOTED_STRING", + "BIGINT_LITERAL", "SMALLINT_LITERAL", "TINYINT_LITERAL", + "INTEGER_VALUE", "EXPONENT_VALUE", "DECIMAL_VALUE", + "FLOAT_LITERAL", "DOUBLE_LITERAL", "BIGDECIMAL_LITERAL", + "IDENTIFIER", "BACKQUOTED_IDENTIFIER", "SIMPLE_COMMENT", + "BRACKETED_COMMENT", "WS", "UNRECOGNIZED" ] + + RULE_singleStatement = 0 + RULE_singleExpression = 1 + RULE_singleTableIdentifier = 2 + RULE_singleMultipartIdentifier = 3 + RULE_singleFunctionIdentifier = 4 + RULE_singleDataType = 5 + RULE_singleTableSchema = 6 + RULE_statement = 7 + RULE_timezone = 8 + RULE_configKey = 9 + RULE_configValue = 10 + RULE_unsupportedHiveNativeCommands = 11 + RULE_createTableHeader = 12 + RULE_replaceTableHeader = 13 + RULE_bucketSpec = 14 + RULE_skewSpec = 15 + RULE_locationSpec = 16 + RULE_commentSpec = 17 + RULE_insertInto = 18 + RULE_partitionSpecLocation = 19 + RULE_partitionSpec = 20 + RULE_partitionVal = 21 + RULE_namespace = 22 + RULE_namespaces = 23 + RULE_describeFuncName = 24 + RULE_describeColName = 25 + RULE_ctes = 26 + RULE_query = 27 + RULE_namedQuery = 28 + RULE_queryTerm = 29 + RULE_querySpecification = 30 + RULE_queryPrimary = 31 + RULE_tableProvider = 32 + RULE_createTableClauses = 33 + RULE_propertyList = 34 + RULE_property = 35 + RULE_propertyKey = 36 + RULE_propertyValue = 37 + RULE_constantList = 38 + RULE_nestedConstantList = 39 + RULE_createFileFormat = 40 + RULE_fileFormat = 41 + RULE_storageHandler = 42 + RULE_resource = 43 + RULE_dmlStatementNoWith = 44 + RULE_queryOrganization = 45 + RULE_multiInsertQueryBody = 46 + RULE_sortItem = 47 + RULE_fromStatement = 48 + RULE_fromStatementBody = 49 + RULE_transformClause = 50 + RULE_selectClause = 51 + RULE_setClause = 52 + RULE_matchedClause = 53 + RULE_notMatchedClause = 54 + RULE_notMatchedBySourceClause = 55 + RULE_matchedAction = 56 + RULE_notMatchedAction = 57 + RULE_notMatchedBySourceAction = 58 + RULE_assignmentList = 59 + RULE_assignment = 60 + RULE_whereClause = 61 + RULE_havingClause = 62 + RULE_hint = 63 + RULE_hintStatement = 64 + RULE_fromClause = 65 + RULE_temporalClause = 66 + RULE_aggregationClause = 67 + RULE_groupByClause = 68 + RULE_groupingAnalytics = 69 + RULE_groupingElement = 70 + RULE_groupingSet = 71 + RULE_pivotClause = 72 + RULE_pivotColumn = 73 + RULE_pivotValue = 74 + RULE_unpivotClause = 75 + RULE_unpivotNullClause = 76 + RULE_unpivotOperator = 77 + RULE_unpivotSingleValueColumnClause = 78 + RULE_unpivotMultiValueColumnClause = 79 + RULE_unpivotColumnSet = 80 + RULE_unpivotValueColumn = 81 + RULE_unpivotNameColumn = 82 + RULE_unpivotColumnAndAlias = 83 + RULE_unpivotColumn = 84 + RULE_unpivotAlias = 85 + RULE_lateralView = 86 + RULE_setQuantifier = 87 + RULE_relation = 88 + RULE_relationExtension = 89 + RULE_joinRelation = 90 + RULE_joinType = 91 + RULE_joinCriteria = 92 + RULE_sample = 93 + RULE_sampleMethod = 94 + RULE_identifierList = 95 + RULE_identifierSeq = 96 + RULE_orderedIdentifierList = 97 + RULE_orderedIdentifier = 98 + RULE_identifierCommentList = 99 + RULE_identifierComment = 100 + RULE_relationPrimary = 101 + RULE_inlineTable = 102 + RULE_functionTable = 103 + RULE_tableAlias = 104 + RULE_rowFormat = 105 + RULE_multipartIdentifierList = 106 + RULE_multipartIdentifier = 107 + RULE_multipartIdentifierPropertyList = 108 + RULE_multipartIdentifierProperty = 109 + RULE_tableIdentifier = 110 + RULE_functionIdentifier = 111 + RULE_namedExpression = 112 + RULE_namedExpressionSeq = 113 + RULE_partitionFieldList = 114 + RULE_partitionField = 115 + RULE_transform = 116 + RULE_transformArgument = 117 + RULE_expression = 118 + RULE_expressionSeq = 119 + RULE_booleanExpression = 120 + RULE_predicate = 121 + RULE_valueExpression = 122 + RULE_datetimeUnit = 123 + RULE_primaryExpression = 124 + RULE_constant = 125 + RULE_comparisonOperator = 126 + RULE_arithmeticOperator = 127 + RULE_predicateOperator = 128 + RULE_booleanValue = 129 + RULE_interval = 130 + RULE_errorCapturingMultiUnitsInterval = 131 + RULE_multiUnitsInterval = 132 + RULE_errorCapturingUnitToUnitInterval = 133 + RULE_unitToUnitInterval = 134 + RULE_intervalValue = 135 + RULE_unitInMultiUnits = 136 + RULE_unitInUnitToUnit = 137 + RULE_colPosition = 138 + RULE_dataType = 139 + RULE_qualifiedColTypeWithPositionList = 140 + RULE_qualifiedColTypeWithPosition = 141 + RULE_colDefinitionDescriptorWithPosition = 142 + RULE_defaultExpression = 143 + RULE_colTypeList = 144 + RULE_colType = 145 + RULE_createOrReplaceTableColTypeList = 146 + RULE_createOrReplaceTableColType = 147 + RULE_colDefinitionOption = 148 + RULE_generationExpression = 149 + RULE_complexColTypeList = 150 + RULE_complexColType = 151 + RULE_whenClause = 152 + RULE_windowClause = 153 + RULE_namedWindow = 154 + RULE_windowSpec = 155 + RULE_windowFrame = 156 + RULE_frameBound = 157 + RULE_qualifiedNameList = 158 + RULE_functionName = 159 + RULE_qualifiedName = 160 + RULE_errorCapturingIdentifier = 161 + RULE_errorCapturingIdentifierExtra = 162 + RULE_identifier = 163 + RULE_strictIdentifier = 164 + RULE_quotedIdentifier = 165 + RULE_backQuotedIdentifier = 166 + RULE_number = 167 + RULE_alterColumnAction = 168 + RULE_stringLit = 169 + RULE_comment = 170 + RULE_version = 171 + RULE_ansiNonReserved = 172 + RULE_strictNonReserved = 173 + RULE_nonReserved = 174 + + ruleNames = [ "singleStatement", "singleExpression", "singleTableIdentifier", + "singleMultipartIdentifier", "singleFunctionIdentifier", + "singleDataType", "singleTableSchema", "statement", "timezone", + "configKey", "configValue", "unsupportedHiveNativeCommands", + "createTableHeader", "replaceTableHeader", "bucketSpec", + "skewSpec", "locationSpec", "commentSpec", "insertInto", + "partitionSpecLocation", "partitionSpec", "partitionVal", + "namespace", "namespaces", "describeFuncName", "describeColName", + "ctes", "query", "namedQuery", "queryTerm", "querySpecification", + "queryPrimary", "tableProvider", "createTableClauses", + "propertyList", "property", "propertyKey", "propertyValue", + "constantList", "nestedConstantList", "createFileFormat", + "fileFormat", "storageHandler", "resource", "dmlStatementNoWith", + "queryOrganization", "multiInsertQueryBody", "sortItem", + "fromStatement", "fromStatementBody", "transformClause", + "selectClause", "setClause", "matchedClause", "notMatchedClause", + "notMatchedBySourceClause", "matchedAction", "notMatchedAction", + "notMatchedBySourceAction", "assignmentList", "assignment", + "whereClause", "havingClause", "hint", "hintStatement", + "fromClause", "temporalClause", "aggregationClause", + "groupByClause", "groupingAnalytics", "groupingElement", + "groupingSet", "pivotClause", "pivotColumn", "pivotValue", + "unpivotClause", "unpivotNullClause", "unpivotOperator", + "unpivotSingleValueColumnClause", "unpivotMultiValueColumnClause", + "unpivotColumnSet", "unpivotValueColumn", "unpivotNameColumn", + "unpivotColumnAndAlias", "unpivotColumn", "unpivotAlias", + "lateralView", "setQuantifier", "relation", "relationExtension", + "joinRelation", "joinType", "joinCriteria", "sample", + "sampleMethod", "identifierList", "identifierSeq", "orderedIdentifierList", + "orderedIdentifier", "identifierCommentList", "identifierComment", + "relationPrimary", "inlineTable", "functionTable", "tableAlias", + "rowFormat", "multipartIdentifierList", "multipartIdentifier", + "multipartIdentifierPropertyList", "multipartIdentifierProperty", + "tableIdentifier", "functionIdentifier", "namedExpression", + "namedExpressionSeq", "partitionFieldList", "partitionField", + "transform", "transformArgument", "expression", "expressionSeq", + "booleanExpression", "predicate", "valueExpression", + "datetimeUnit", "primaryExpression", "constant", "comparisonOperator", + "arithmeticOperator", "predicateOperator", "booleanValue", + "interval", "errorCapturingMultiUnitsInterval", "multiUnitsInterval", + "errorCapturingUnitToUnitInterval", "unitToUnitInterval", + "intervalValue", "unitInMultiUnits", "unitInUnitToUnit", + "colPosition", "dataType", "qualifiedColTypeWithPositionList", + "qualifiedColTypeWithPosition", "colDefinitionDescriptorWithPosition", + "defaultExpression", "colTypeList", "colType", "createOrReplaceTableColTypeList", + "createOrReplaceTableColType", "colDefinitionOption", + "generationExpression", "complexColTypeList", "complexColType", + "whenClause", "windowClause", "namedWindow", "windowSpec", + "windowFrame", "frameBound", "qualifiedNameList", "functionName", + "qualifiedName", "errorCapturingIdentifier", "errorCapturingIdentifierExtra", + "identifier", "strictIdentifier", "quotedIdentifier", + "backQuotedIdentifier", "number", "alterColumnAction", + "stringLit", "comment", "version", "ansiNonReserved", + "strictNonReserved", "nonReserved" ] + + EOF = Token.EOF + SEMICOLON=1 + LEFT_PAREN=2 + RIGHT_PAREN=3 + COMMA=4 + DOT=5 + LEFT_BRACKET=6 + RIGHT_BRACKET=7 + ADD=8 + AFTER=9 + ALL=10 + ALTER=11 + ALWAYS=12 + ANALYZE=13 + AND=14 + ANTI=15 + ANY=16 + ANY_VALUE=17 + ARCHIVE=18 + ARRAY=19 + AS=20 + ASC=21 + AT=22 + AUTHORIZATION=23 + BETWEEN=24 + BOTH=25 + BUCKET=26 + BUCKETS=27 + BY=28 + CACHE=29 + CASCADE=30 + CASE=31 + CAST=32 + CATALOG=33 + CATALOGS=34 + CHANGE=35 + CHECK=36 + CLEAR=37 + CLUSTER=38 + CLUSTERED=39 + CODEGEN=40 + COLLATE=41 + COLLECTION=42 + COLUMN=43 + COLUMNS=44 + COMMENT=45 + COMMIT=46 + COMPACT=47 + COMPACTIONS=48 + COMPUTE=49 + CONCATENATE=50 + CONSTRAINT=51 + COST=52 + CREATE=53 + CROSS=54 + CUBE=55 + CURRENT=56 + CURRENT_DATE=57 + CURRENT_TIME=58 + CURRENT_TIMESTAMP=59 + CURRENT_USER=60 + DAY=61 + DAYS=62 + DAYOFYEAR=63 + DATA=64 + DATABASE=65 + DATABASES=66 + DATEADD=67 + DATEDIFF=68 + DBPROPERTIES=69 + DEFAULT=70 + DEFINED=71 + DELETE=72 + DELIMITED=73 + DESC=74 + DESCRIBE=75 + DFS=76 + DIRECTORIES=77 + DIRECTORY=78 + DISTINCT=79 + DISTRIBUTE=80 + DIV=81 + DROP=82 + ELSE=83 + END=84 + ESCAPE=85 + ESCAPED=86 + EXCEPT=87 + EXCHANGE=88 + EXCLUDE=89 + EXISTS=90 + EXPLAIN=91 + EXPORT=92 + EXTENDED=93 + EXTERNAL=94 + EXTRACT=95 + FALSE=96 + FETCH=97 + FIELDS=98 + FILTER=99 + FILEFORMAT=100 + FIRST=101 + FOLLOWING=102 + FOR=103 + FOREIGN=104 + FORMAT=105 + FORMATTED=106 + FROM=107 + FULL=108 + FUNCTION=109 + FUNCTIONS=110 + GENERATED=111 + GLOBAL=112 + GRANT=113 + GROUP=114 + GROUPING=115 + HAVING=116 + HOUR=117 + HOURS=118 + IF=119 + IGNORE=120 + IMPORT=121 + IN=122 + INCLUDE=123 + INDEX=124 + INDEXES=125 + INNER=126 + INPATH=127 + INPUTFORMAT=128 + INSERT=129 + INTERSECT=130 + INTERVAL=131 + INTO=132 + IS=133 + ITEMS=134 + JOIN=135 + KEYS=136 + LAST=137 + LATERAL=138 + LAZY=139 + LEADING=140 + LEFT=141 + LIKE=142 + ILIKE=143 + LIMIT=144 + LINES=145 + LIST=146 + LOAD=147 + LOCAL=148 + LOCATION=149 + LOCK=150 + LOCKS=151 + LOGICAL=152 + MACRO=153 + MAP=154 + MATCHED=155 + MERGE=156 + MICROSECOND=157 + MICROSECONDS=158 + MILLISECOND=159 + MILLISECONDS=160 + MINUTE=161 + MINUTES=162 + MONTH=163 + MONTHS=164 + MSCK=165 + NAMESPACE=166 + NAMESPACES=167 + NANOSECOND=168 + NANOSECONDS=169 + NATURAL=170 + NO=171 + NOT=172 + NULL=173 + NULLS=174 + OF=175 + OFFSET=176 + ON=177 + ONLY=178 + OPTION=179 + OPTIONS=180 + OR=181 + ORDER=182 + OUT=183 + OUTER=184 + OUTPUTFORMAT=185 + OVER=186 + OVERLAPS=187 + OVERLAY=188 + OVERWRITE=189 + PARTITION=190 + PARTITIONED=191 + PARTITIONS=192 + PERCENTILE_CONT=193 + PERCENTILE_DISC=194 + PERCENTLIT=195 + PIVOT=196 + PLACING=197 + POSITION=198 + PRECEDING=199 + PRIMARY=200 + PRINCIPALS=201 + PROPERTIES=202 + PURGE=203 + QUARTER=204 + QUERY=205 + RANGE=206 + RECORDREADER=207 + RECORDWRITER=208 + RECOVER=209 + REDUCE=210 + REFERENCES=211 + REFRESH=212 + RENAME=213 + REPAIR=214 + REPEATABLE=215 + REPLACE=216 + RESET=217 + RESPECT=218 + RESTRICT=219 + REVOKE=220 + RIGHT=221 + RLIKE=222 + ROLE=223 + ROLES=224 + ROLLBACK=225 + ROLLUP=226 + ROW=227 + ROWS=228 + SECOND=229 + SECONDS=230 + SCHEMA=231 + SCHEMAS=232 + SELECT=233 + SEMI=234 + SEPARATED=235 + SERDE=236 + SERDEPROPERTIES=237 + SESSION_USER=238 + SET=239 + SETMINUS=240 + SETS=241 + SHOW=242 + SKEWED=243 + SOME=244 + SORT=245 + SORTED=246 + SOURCE=247 + START=248 + STATISTICS=249 + STORED=250 + STRATIFY=251 + STRUCT=252 + SUBSTR=253 + SUBSTRING=254 + SYNC=255 + SYSTEM_TIME=256 + SYSTEM_VERSION=257 + TABLE=258 + TABLES=259 + TABLESAMPLE=260 + TARGET=261 + TBLPROPERTIES=262 + TEMPORARY=263 + TERMINATED=264 + THEN=265 + TIME=266 + TIMESTAMP=267 + TIMESTAMPADD=268 + TIMESTAMPDIFF=269 + TO=270 + TOUCH=271 + TRAILING=272 + TRANSACTION=273 + TRANSACTIONS=274 + TRANSFORM=275 + TRIM=276 + TRUE=277 + TRUNCATE=278 + TRY_CAST=279 + TYPE=280 + UNARCHIVE=281 + UNBOUNDED=282 + UNCACHE=283 + UNION=284 + UNIQUE=285 + UNKNOWN=286 + UNLOCK=287 + UNPIVOT=288 + UNSET=289 + UPDATE=290 + USE=291 + USER=292 + USING=293 + VALUES=294 + VERSION=295 + VIEW=296 + VIEWS=297 + WEEK=298 + WEEKS=299 + WHEN=300 + WHERE=301 + WINDOW=302 + WITH=303 + WITHIN=304 + YEAR=305 + YEARS=306 + ZONE=307 + EQ=308 + NSEQ=309 + NEQ=310 + NEQJ=311 + LT=312 + LTE=313 + GT=314 + GTE=315 + PLUS=316 + MINUS=317 + ASTERISK=318 + SLASH=319 + PERCENT=320 + TILDE=321 + AMPERSAND=322 + PIPE=323 + CONCAT_PIPE=324 + HAT=325 + COLON=326 + ARROW=327 + HENT_START=328 + HENT_END=329 + STRING=330 + DOUBLEQUOTED_STRING=331 + BIGINT_LITERAL=332 + SMALLINT_LITERAL=333 + TINYINT_LITERAL=334 + INTEGER_VALUE=335 + EXPONENT_VALUE=336 + DECIMAL_VALUE=337 + FLOAT_LITERAL=338 + DOUBLE_LITERAL=339 + BIGDECIMAL_LITERAL=340 + IDENTIFIER=341 + BACKQUOTED_IDENTIFIER=342 + SIMPLE_COMMENT=343 + BRACKETED_COMMENT=344 + WS=345 + UNRECOGNIZED=346 + + def __init__(self, input:TokenStream, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.13.1") + self._interp = ParserATNSimulator(self, self.atn, self.decisionsToDFA, self.sharedContextCache) + self._predicates = None + + + + + class SingleStatementContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def statement(self): + return self.getTypedRuleContext(SqlBaseParser.StatementContext,0) + + + def EOF(self): + return self.getToken(SqlBaseParser.EOF, 0) + + def SEMICOLON(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.SEMICOLON) + else: + return self.getToken(SqlBaseParser.SEMICOLON, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_singleStatement + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSingleStatement" ): + listener.enterSingleStatement(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSingleStatement" ): + listener.exitSingleStatement(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSingleStatement" ): + return visitor.visitSingleStatement(self) + else: + return visitor.visitChildren(self) + + + + + def singleStatement(self): + + localctx = SqlBaseParser.SingleStatementContext(self, self._ctx, self.state) + self.enterRule(localctx, 0, self.RULE_singleStatement) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 350 + self.statement() + self.state = 354 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==1: + self.state = 351 + self.match(SqlBaseParser.SEMICOLON) + self.state = 356 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 357 + self.match(SqlBaseParser.EOF) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SingleExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def namedExpression(self): + return self.getTypedRuleContext(SqlBaseParser.NamedExpressionContext,0) + + + def EOF(self): + return self.getToken(SqlBaseParser.EOF, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_singleExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSingleExpression" ): + listener.enterSingleExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSingleExpression" ): + listener.exitSingleExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSingleExpression" ): + return visitor.visitSingleExpression(self) + else: + return visitor.visitChildren(self) + + + + + def singleExpression(self): + + localctx = SqlBaseParser.SingleExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 2, self.RULE_singleExpression) + try: + self.enterOuterAlt(localctx, 1) + self.state = 359 + self.namedExpression() + self.state = 360 + self.match(SqlBaseParser.EOF) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SingleTableIdentifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def tableIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.TableIdentifierContext,0) + + + def EOF(self): + return self.getToken(SqlBaseParser.EOF, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_singleTableIdentifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSingleTableIdentifier" ): + listener.enterSingleTableIdentifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSingleTableIdentifier" ): + listener.exitSingleTableIdentifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSingleTableIdentifier" ): + return visitor.visitSingleTableIdentifier(self) + else: + return visitor.visitChildren(self) + + + + + def singleTableIdentifier(self): + + localctx = SqlBaseParser.SingleTableIdentifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 4, self.RULE_singleTableIdentifier) + try: + self.enterOuterAlt(localctx, 1) + self.state = 362 + self.tableIdentifier() + self.state = 363 + self.match(SqlBaseParser.EOF) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SingleMultipartIdentifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def EOF(self): + return self.getToken(SqlBaseParser.EOF, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_singleMultipartIdentifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSingleMultipartIdentifier" ): + listener.enterSingleMultipartIdentifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSingleMultipartIdentifier" ): + listener.exitSingleMultipartIdentifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSingleMultipartIdentifier" ): + return visitor.visitSingleMultipartIdentifier(self) + else: + return visitor.visitChildren(self) + + + + + def singleMultipartIdentifier(self): + + localctx = SqlBaseParser.SingleMultipartIdentifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 6, self.RULE_singleMultipartIdentifier) + try: + self.enterOuterAlt(localctx, 1) + self.state = 365 + self.multipartIdentifier() + self.state = 366 + self.match(SqlBaseParser.EOF) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SingleFunctionIdentifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def functionIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.FunctionIdentifierContext,0) + + + def EOF(self): + return self.getToken(SqlBaseParser.EOF, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_singleFunctionIdentifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSingleFunctionIdentifier" ): + listener.enterSingleFunctionIdentifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSingleFunctionIdentifier" ): + listener.exitSingleFunctionIdentifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSingleFunctionIdentifier" ): + return visitor.visitSingleFunctionIdentifier(self) + else: + return visitor.visitChildren(self) + + + + + def singleFunctionIdentifier(self): + + localctx = SqlBaseParser.SingleFunctionIdentifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 8, self.RULE_singleFunctionIdentifier) + try: + self.enterOuterAlt(localctx, 1) + self.state = 368 + self.functionIdentifier() + self.state = 369 + self.match(SqlBaseParser.EOF) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SingleDataTypeContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def dataType(self): + return self.getTypedRuleContext(SqlBaseParser.DataTypeContext,0) + + + def EOF(self): + return self.getToken(SqlBaseParser.EOF, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_singleDataType + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSingleDataType" ): + listener.enterSingleDataType(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSingleDataType" ): + listener.exitSingleDataType(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSingleDataType" ): + return visitor.visitSingleDataType(self) + else: + return visitor.visitChildren(self) + + + + + def singleDataType(self): + + localctx = SqlBaseParser.SingleDataTypeContext(self, self._ctx, self.state) + self.enterRule(localctx, 10, self.RULE_singleDataType) + try: + self.enterOuterAlt(localctx, 1) + self.state = 371 + self.dataType() + self.state = 372 + self.match(SqlBaseParser.EOF) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SingleTableSchemaContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def colTypeList(self): + return self.getTypedRuleContext(SqlBaseParser.ColTypeListContext,0) + + + def EOF(self): + return self.getToken(SqlBaseParser.EOF, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_singleTableSchema + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSingleTableSchema" ): + listener.enterSingleTableSchema(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSingleTableSchema" ): + listener.exitSingleTableSchema(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSingleTableSchema" ): + return visitor.visitSingleTableSchema(self) + else: + return visitor.visitChildren(self) + + + + + def singleTableSchema(self): + + localctx = SqlBaseParser.SingleTableSchemaContext(self, self._ctx, self.state) + self.enterRule(localctx, 12, self.RULE_singleTableSchema) + try: + self.enterOuterAlt(localctx, 1) + self.state = 374 + self.colTypeList() + self.state = 375 + self.match(SqlBaseParser.EOF) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class StatementContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_statement + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class ExplainContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def EXPLAIN(self): + return self.getToken(SqlBaseParser.EXPLAIN, 0) + def statement(self): + return self.getTypedRuleContext(SqlBaseParser.StatementContext,0) + + def LOGICAL(self): + return self.getToken(SqlBaseParser.LOGICAL, 0) + def FORMATTED(self): + return self.getToken(SqlBaseParser.FORMATTED, 0) + def EXTENDED(self): + return self.getToken(SqlBaseParser.EXTENDED, 0) + def CODEGEN(self): + return self.getToken(SqlBaseParser.CODEGEN, 0) + def COST(self): + return self.getToken(SqlBaseParser.COST, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterExplain" ): + listener.enterExplain(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitExplain" ): + listener.exitExplain(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitExplain" ): + return visitor.visitExplain(self) + else: + return visitor.visitChildren(self) + + + class ResetConfigurationContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def RESET(self): + return self.getToken(SqlBaseParser.RESET, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterResetConfiguration" ): + listener.enterResetConfiguration(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitResetConfiguration" ): + listener.exitResetConfiguration(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitResetConfiguration" ): + return visitor.visitResetConfiguration(self) + else: + return visitor.visitChildren(self) + + + class AlterViewQueryContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def VIEW(self): + return self.getToken(SqlBaseParser.VIEW, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAlterViewQuery" ): + listener.enterAlterViewQuery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAlterViewQuery" ): + listener.exitAlterViewQuery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAlterViewQuery" ): + return visitor.visitAlterViewQuery(self) + else: + return visitor.visitChildren(self) + + + class UseContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def USE(self): + return self.getToken(SqlBaseParser.USE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUse" ): + listener.enterUse(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUse" ): + listener.exitUse(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUse" ): + return visitor.visitUse(self) + else: + return visitor.visitChildren(self) + + + class DropNamespaceContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def DROP(self): + return self.getToken(SqlBaseParser.DROP, 0) + def namespace(self): + return self.getTypedRuleContext(SqlBaseParser.NamespaceContext,0) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + def RESTRICT(self): + return self.getToken(SqlBaseParser.RESTRICT, 0) + def CASCADE(self): + return self.getToken(SqlBaseParser.CASCADE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDropNamespace" ): + listener.enterDropNamespace(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDropNamespace" ): + listener.exitDropNamespace(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDropNamespace" ): + return visitor.visitDropNamespace(self) + else: + return visitor.visitChildren(self) + + + class CreateTempViewUsingContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def CREATE(self): + return self.getToken(SqlBaseParser.CREATE, 0) + def TEMPORARY(self): + return self.getToken(SqlBaseParser.TEMPORARY, 0) + def VIEW(self): + return self.getToken(SqlBaseParser.VIEW, 0) + def tableIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.TableIdentifierContext,0) + + def tableProvider(self): + return self.getTypedRuleContext(SqlBaseParser.TableProviderContext,0) + + def OR(self): + return self.getToken(SqlBaseParser.OR, 0) + def REPLACE(self): + return self.getToken(SqlBaseParser.REPLACE, 0) + def GLOBAL(self): + return self.getToken(SqlBaseParser.GLOBAL, 0) + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def colTypeList(self): + return self.getTypedRuleContext(SqlBaseParser.ColTypeListContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def OPTIONS(self): + return self.getToken(SqlBaseParser.OPTIONS, 0) + def propertyList(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCreateTempViewUsing" ): + listener.enterCreateTempViewUsing(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCreateTempViewUsing" ): + listener.exitCreateTempViewUsing(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCreateTempViewUsing" ): + return visitor.visitCreateTempViewUsing(self) + else: + return visitor.visitChildren(self) + + + class RenameTableContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.from_ = None # MultipartIdentifierContext + self.to = None # MultipartIdentifierContext + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def RENAME(self): + return self.getToken(SqlBaseParser.RENAME, 0) + def TO(self): + return self.getToken(SqlBaseParser.TO, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def VIEW(self): + return self.getToken(SqlBaseParser.VIEW, 0) + def multipartIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.MultipartIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRenameTable" ): + listener.enterRenameTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRenameTable" ): + listener.exitRenameTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRenameTable" ): + return visitor.visitRenameTable(self) + else: + return visitor.visitChildren(self) + + + class FailNativeCommandContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + def ROLE(self): + return self.getToken(SqlBaseParser.ROLE, 0) + def unsupportedHiveNativeCommands(self): + return self.getTypedRuleContext(SqlBaseParser.UnsupportedHiveNativeCommandsContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFailNativeCommand" ): + listener.enterFailNativeCommand(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFailNativeCommand" ): + listener.exitFailNativeCommand(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFailNativeCommand" ): + return visitor.visitFailNativeCommand(self) + else: + return visitor.visitChildren(self) + + + class SetCatalogContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + def CATALOG(self): + return self.getToken(SqlBaseParser.CATALOG, 0) + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSetCatalog" ): + listener.enterSetCatalog(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSetCatalog" ): + listener.exitSetCatalog(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSetCatalog" ): + return visitor.visitSetCatalog(self) + else: + return visitor.visitChildren(self) + + + class ClearCacheContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def CLEAR(self): + return self.getToken(SqlBaseParser.CLEAR, 0) + def CACHE(self): + return self.getToken(SqlBaseParser.CACHE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterClearCache" ): + listener.enterClearCache(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitClearCache" ): + listener.exitClearCache(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitClearCache" ): + return visitor.visitClearCache(self) + else: + return visitor.visitChildren(self) + + + class DropViewContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def DROP(self): + return self.getToken(SqlBaseParser.DROP, 0) + def VIEW(self): + return self.getToken(SqlBaseParser.VIEW, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDropView" ): + listener.enterDropView(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDropView" ): + listener.exitDropView(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDropView" ): + return visitor.visitDropView(self) + else: + return visitor.visitChildren(self) + + + class ShowTablesContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.pattern = None # StringLitContext + self.copyFrom(ctx) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + def TABLES(self): + return self.getToken(SqlBaseParser.TABLES, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + def LIKE(self): + return self.getToken(SqlBaseParser.LIKE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterShowTables" ): + listener.enterShowTables(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitShowTables" ): + listener.exitShowTables(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitShowTables" ): + return visitor.visitShowTables(self) + else: + return visitor.visitChildren(self) + + + class RecoverPartitionsContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def RECOVER(self): + return self.getToken(SqlBaseParser.RECOVER, 0) + def PARTITIONS(self): + return self.getToken(SqlBaseParser.PARTITIONS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRecoverPartitions" ): + listener.enterRecoverPartitions(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRecoverPartitions" ): + listener.exitRecoverPartitions(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRecoverPartitions" ): + return visitor.visitRecoverPartitions(self) + else: + return visitor.visitChildren(self) + + + class DropIndexContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def DROP(self): + return self.getToken(SqlBaseParser.DROP, 0) + def INDEX(self): + return self.getToken(SqlBaseParser.INDEX, 0) + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + def ON(self): + return self.getToken(SqlBaseParser.ON, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDropIndex" ): + listener.enterDropIndex(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDropIndex" ): + listener.exitDropIndex(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDropIndex" ): + return visitor.visitDropIndex(self) + else: + return visitor.visitChildren(self) + + + class ShowCatalogsContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.pattern = None # StringLitContext + self.copyFrom(ctx) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + def CATALOGS(self): + return self.getToken(SqlBaseParser.CATALOGS, 0) + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + def LIKE(self): + return self.getToken(SqlBaseParser.LIKE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterShowCatalogs" ): + listener.enterShowCatalogs(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitShowCatalogs" ): + listener.exitShowCatalogs(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitShowCatalogs" ): + return visitor.visitShowCatalogs(self) + else: + return visitor.visitChildren(self) + + + class ShowCurrentNamespaceContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + def CURRENT(self): + return self.getToken(SqlBaseParser.CURRENT, 0) + def namespace(self): + return self.getTypedRuleContext(SqlBaseParser.NamespaceContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterShowCurrentNamespace" ): + listener.enterShowCurrentNamespace(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitShowCurrentNamespace" ): + listener.exitShowCurrentNamespace(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitShowCurrentNamespace" ): + return visitor.visitShowCurrentNamespace(self) + else: + return visitor.visitChildren(self) + + + class RenameTablePartitionContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.from_ = None # PartitionSpecContext + self.to = None # PartitionSpecContext + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def RENAME(self): + return self.getToken(SqlBaseParser.RENAME, 0) + def TO(self): + return self.getToken(SqlBaseParser.TO, 0) + def partitionSpec(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PartitionSpecContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRenameTablePartition" ): + listener.enterRenameTablePartition(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRenameTablePartition" ): + listener.exitRenameTablePartition(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRenameTablePartition" ): + return visitor.visitRenameTablePartition(self) + else: + return visitor.visitChildren(self) + + + class RepairTableContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.option = None # Token + self.copyFrom(ctx) + + def REPAIR(self): + return self.getToken(SqlBaseParser.REPAIR, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def MSCK(self): + return self.getToken(SqlBaseParser.MSCK, 0) + def PARTITIONS(self): + return self.getToken(SqlBaseParser.PARTITIONS, 0) + def ADD(self): + return self.getToken(SqlBaseParser.ADD, 0) + def DROP(self): + return self.getToken(SqlBaseParser.DROP, 0) + def SYNC(self): + return self.getToken(SqlBaseParser.SYNC, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRepairTable" ): + listener.enterRepairTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRepairTable" ): + listener.exitRepairTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRepairTable" ): + return visitor.visitRepairTable(self) + else: + return visitor.visitChildren(self) + + + class RefreshResourceContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def REFRESH(self): + return self.getToken(SqlBaseParser.REFRESH, 0) + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRefreshResource" ): + listener.enterRefreshResource(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRefreshResource" ): + listener.exitRefreshResource(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRefreshResource" ): + return visitor.visitRefreshResource(self) + else: + return visitor.visitChildren(self) + + + class ShowCreateTableContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + def CREATE(self): + return self.getToken(SqlBaseParser.CREATE, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + def SERDE(self): + return self.getToken(SqlBaseParser.SERDE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterShowCreateTable" ): + listener.enterShowCreateTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitShowCreateTable" ): + listener.exitShowCreateTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitShowCreateTable" ): + return visitor.visitShowCreateTable(self) + else: + return visitor.visitChildren(self) + + + class ShowNamespacesContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.pattern = None # StringLitContext + self.copyFrom(ctx) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + def namespaces(self): + return self.getTypedRuleContext(SqlBaseParser.NamespacesContext,0) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + def LIKE(self): + return self.getToken(SqlBaseParser.LIKE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterShowNamespaces" ): + listener.enterShowNamespaces(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitShowNamespaces" ): + listener.exitShowNamespaces(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitShowNamespaces" ): + return visitor.visitShowNamespaces(self) + else: + return visitor.visitChildren(self) + + + class ShowColumnsContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.table = None # MultipartIdentifierContext + self.ns = None # MultipartIdentifierContext + self.copyFrom(ctx) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + def COLUMNS(self): + return self.getToken(SqlBaseParser.COLUMNS, 0) + def FROM(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.FROM) + else: + return self.getToken(SqlBaseParser.FROM, i) + def IN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.IN) + else: + return self.getToken(SqlBaseParser.IN, i) + def multipartIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.MultipartIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterShowColumns" ): + listener.enterShowColumns(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitShowColumns" ): + listener.exitShowColumns(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitShowColumns" ): + return visitor.visitShowColumns(self) + else: + return visitor.visitChildren(self) + + + class ReplaceTableContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def replaceTableHeader(self): + return self.getTypedRuleContext(SqlBaseParser.ReplaceTableHeaderContext,0) + + def createTableClauses(self): + return self.getTypedRuleContext(SqlBaseParser.CreateTableClausesContext,0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def createOrReplaceTableColTypeList(self): + return self.getTypedRuleContext(SqlBaseParser.CreateOrReplaceTableColTypeListContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def tableProvider(self): + return self.getTypedRuleContext(SqlBaseParser.TableProviderContext,0) + + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterReplaceTable" ): + listener.enterReplaceTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitReplaceTable" ): + listener.exitReplaceTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitReplaceTable" ): + return visitor.visitReplaceTable(self) + else: + return visitor.visitChildren(self) + + + class AnalyzeTablesContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def ANALYZE(self): + return self.getToken(SqlBaseParser.ANALYZE, 0) + def TABLES(self): + return self.getToken(SqlBaseParser.TABLES, 0) + def COMPUTE(self): + return self.getToken(SqlBaseParser.COMPUTE, 0) + def STATISTICS(self): + return self.getToken(SqlBaseParser.STATISTICS, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAnalyzeTables" ): + listener.enterAnalyzeTables(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAnalyzeTables" ): + listener.exitAnalyzeTables(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAnalyzeTables" ): + return visitor.visitAnalyzeTables(self) + else: + return visitor.visitChildren(self) + + + class AddTablePartitionContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def ADD(self): + return self.getToken(SqlBaseParser.ADD, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def VIEW(self): + return self.getToken(SqlBaseParser.VIEW, 0) + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + def partitionSpecLocation(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PartitionSpecLocationContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecLocationContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAddTablePartition" ): + listener.enterAddTablePartition(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAddTablePartition" ): + listener.exitAddTablePartition(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAddTablePartition" ): + return visitor.visitAddTablePartition(self) + else: + return visitor.visitChildren(self) + + + class SetNamespaceLocationContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def namespace(self): + return self.getTypedRuleContext(SqlBaseParser.NamespaceContext,0) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + def locationSpec(self): + return self.getTypedRuleContext(SqlBaseParser.LocationSpecContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSetNamespaceLocation" ): + listener.enterSetNamespaceLocation(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSetNamespaceLocation" ): + listener.exitSetNamespaceLocation(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSetNamespaceLocation" ): + return visitor.visitSetNamespaceLocation(self) + else: + return visitor.visitChildren(self) + + + class RefreshTableContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def REFRESH(self): + return self.getToken(SqlBaseParser.REFRESH, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRefreshTable" ): + listener.enterRefreshTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRefreshTable" ): + listener.exitRefreshTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRefreshTable" ): + return visitor.visitRefreshTable(self) + else: + return visitor.visitChildren(self) + + + class SetNamespacePropertiesContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def namespace(self): + return self.getTypedRuleContext(SqlBaseParser.NamespaceContext,0) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + def propertyList(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,0) + + def DBPROPERTIES(self): + return self.getToken(SqlBaseParser.DBPROPERTIES, 0) + def PROPERTIES(self): + return self.getToken(SqlBaseParser.PROPERTIES, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSetNamespaceProperties" ): + listener.enterSetNamespaceProperties(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSetNamespaceProperties" ): + listener.exitSetNamespaceProperties(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSetNamespaceProperties" ): + return visitor.visitSetNamespaceProperties(self) + else: + return visitor.visitChildren(self) + + + class ManageResourceContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.op = None # Token + self.copyFrom(ctx) + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + def ADD(self): + return self.getToken(SqlBaseParser.ADD, 0) + def LIST(self): + return self.getToken(SqlBaseParser.LIST, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterManageResource" ): + listener.enterManageResource(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitManageResource" ): + listener.exitManageResource(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitManageResource" ): + return visitor.visitManageResource(self) + else: + return visitor.visitChildren(self) + + + class SetQuotedConfigurationContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + def configKey(self): + return self.getTypedRuleContext(SqlBaseParser.ConfigKeyContext,0) + + def EQ(self): + return self.getToken(SqlBaseParser.EQ, 0) + def configValue(self): + return self.getTypedRuleContext(SqlBaseParser.ConfigValueContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSetQuotedConfiguration" ): + listener.enterSetQuotedConfiguration(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSetQuotedConfiguration" ): + listener.exitSetQuotedConfiguration(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSetQuotedConfiguration" ): + return visitor.visitSetQuotedConfiguration(self) + else: + return visitor.visitChildren(self) + + + class AnalyzeContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def ANALYZE(self): + return self.getToken(SqlBaseParser.ANALYZE, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def COMPUTE(self): + return self.getToken(SqlBaseParser.COMPUTE, 0) + def STATISTICS(self): + return self.getToken(SqlBaseParser.STATISTICS, 0) + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + def FOR(self): + return self.getToken(SqlBaseParser.FOR, 0) + def COLUMNS(self): + return self.getToken(SqlBaseParser.COLUMNS, 0) + def identifierSeq(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierSeqContext,0) + + def ALL(self): + return self.getToken(SqlBaseParser.ALL, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAnalyze" ): + listener.enterAnalyze(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAnalyze" ): + listener.exitAnalyze(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAnalyze" ): + return visitor.visitAnalyze(self) + else: + return visitor.visitChildren(self) + + + class CreateFunctionContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.className = None # StringLitContext + self.copyFrom(ctx) + + def CREATE(self): + return self.getToken(SqlBaseParser.CREATE, 0) + def FUNCTION(self): + return self.getToken(SqlBaseParser.FUNCTION, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + def OR(self): + return self.getToken(SqlBaseParser.OR, 0) + def REPLACE(self): + return self.getToken(SqlBaseParser.REPLACE, 0) + def TEMPORARY(self): + return self.getToken(SqlBaseParser.TEMPORARY, 0) + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + def USING(self): + return self.getToken(SqlBaseParser.USING, 0) + def resource(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ResourceContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ResourceContext,i) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCreateFunction" ): + listener.enterCreateFunction(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCreateFunction" ): + listener.exitCreateFunction(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCreateFunction" ): + return visitor.visitCreateFunction(self) + else: + return visitor.visitChildren(self) + + + class HiveReplaceColumnsContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.table = None # MultipartIdentifierContext + self.columns = None # QualifiedColTypeWithPositionListContext + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def REPLACE(self): + return self.getToken(SqlBaseParser.REPLACE, 0) + def COLUMNS(self): + return self.getToken(SqlBaseParser.COLUMNS, 0) + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def qualifiedColTypeWithPositionList(self): + return self.getTypedRuleContext(SqlBaseParser.QualifiedColTypeWithPositionListContext,0) + + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterHiveReplaceColumns" ): + listener.enterHiveReplaceColumns(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitHiveReplaceColumns" ): + listener.exitHiveReplaceColumns(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitHiveReplaceColumns" ): + return visitor.visitHiveReplaceColumns(self) + else: + return visitor.visitChildren(self) + + + class CommentNamespaceContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def COMMENT(self): + return self.getToken(SqlBaseParser.COMMENT, 0) + def ON(self): + return self.getToken(SqlBaseParser.ON, 0) + def namespace(self): + return self.getTypedRuleContext(SqlBaseParser.NamespaceContext,0) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def IS(self): + return self.getToken(SqlBaseParser.IS, 0) + def comment(self): + return self.getTypedRuleContext(SqlBaseParser.CommentContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCommentNamespace" ): + listener.enterCommentNamespace(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCommentNamespace" ): + listener.exitCommentNamespace(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCommentNamespace" ): + return visitor.visitCommentNamespace(self) + else: + return visitor.visitChildren(self) + + + class ResetQuotedConfigurationContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def RESET(self): + return self.getToken(SqlBaseParser.RESET, 0) + def configKey(self): + return self.getTypedRuleContext(SqlBaseParser.ConfigKeyContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterResetQuotedConfiguration" ): + listener.enterResetQuotedConfiguration(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitResetQuotedConfiguration" ): + listener.exitResetQuotedConfiguration(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitResetQuotedConfiguration" ): + return visitor.visitResetQuotedConfiguration(self) + else: + return visitor.visitChildren(self) + + + class CreateTableContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def createTableHeader(self): + return self.getTypedRuleContext(SqlBaseParser.CreateTableHeaderContext,0) + + def createTableClauses(self): + return self.getTypedRuleContext(SqlBaseParser.CreateTableClausesContext,0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def createOrReplaceTableColTypeList(self): + return self.getTypedRuleContext(SqlBaseParser.CreateOrReplaceTableColTypeListContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def tableProvider(self): + return self.getTypedRuleContext(SqlBaseParser.TableProviderContext,0) + + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCreateTable" ): + listener.enterCreateTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCreateTable" ): + listener.exitCreateTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCreateTable" ): + return visitor.visitCreateTable(self) + else: + return visitor.visitChildren(self) + + + class DmlStatementContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def dmlStatementNoWith(self): + return self.getTypedRuleContext(SqlBaseParser.DmlStatementNoWithContext,0) + + def ctes(self): + return self.getTypedRuleContext(SqlBaseParser.CtesContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDmlStatement" ): + listener.enterDmlStatement(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDmlStatement" ): + listener.exitDmlStatement(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDmlStatement" ): + return visitor.visitDmlStatement(self) + else: + return visitor.visitChildren(self) + + + class CreateTableLikeContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.target = None # TableIdentifierContext + self.source = None # TableIdentifierContext + self.tableProps = None # PropertyListContext + self.copyFrom(ctx) + + def CREATE(self): + return self.getToken(SqlBaseParser.CREATE, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def LIKE(self): + return self.getToken(SqlBaseParser.LIKE, 0) + def tableIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.TableIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.TableIdentifierContext,i) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + def tableProvider(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.TableProviderContext) + else: + return self.getTypedRuleContext(SqlBaseParser.TableProviderContext,i) + + def rowFormat(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.RowFormatContext) + else: + return self.getTypedRuleContext(SqlBaseParser.RowFormatContext,i) + + def createFileFormat(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.CreateFileFormatContext) + else: + return self.getTypedRuleContext(SqlBaseParser.CreateFileFormatContext,i) + + def locationSpec(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.LocationSpecContext) + else: + return self.getTypedRuleContext(SqlBaseParser.LocationSpecContext,i) + + def TBLPROPERTIES(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.TBLPROPERTIES) + else: + return self.getToken(SqlBaseParser.TBLPROPERTIES, i) + def propertyList(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PropertyListContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCreateTableLike" ): + listener.enterCreateTableLike(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCreateTableLike" ): + listener.exitCreateTableLike(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCreateTableLike" ): + return visitor.visitCreateTableLike(self) + else: + return visitor.visitChildren(self) + + + class UncacheTableContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def UNCACHE(self): + return self.getToken(SqlBaseParser.UNCACHE, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUncacheTable" ): + listener.enterUncacheTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUncacheTable" ): + listener.exitUncacheTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUncacheTable" ): + return visitor.visitUncacheTable(self) + else: + return visitor.visitChildren(self) + + + class DropFunctionContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def DROP(self): + return self.getToken(SqlBaseParser.DROP, 0) + def FUNCTION(self): + return self.getToken(SqlBaseParser.FUNCTION, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def TEMPORARY(self): + return self.getToken(SqlBaseParser.TEMPORARY, 0) + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDropFunction" ): + listener.enterDropFunction(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDropFunction" ): + listener.exitDropFunction(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDropFunction" ): + return visitor.visitDropFunction(self) + else: + return visitor.visitChildren(self) + + + class DescribeRelationContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.option = None # Token + self.copyFrom(ctx) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def DESC(self): + return self.getToken(SqlBaseParser.DESC, 0) + def DESCRIBE(self): + return self.getToken(SqlBaseParser.DESCRIBE, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + def describeColName(self): + return self.getTypedRuleContext(SqlBaseParser.DescribeColNameContext,0) + + def EXTENDED(self): + return self.getToken(SqlBaseParser.EXTENDED, 0) + def FORMATTED(self): + return self.getToken(SqlBaseParser.FORMATTED, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDescribeRelation" ): + listener.enterDescribeRelation(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDescribeRelation" ): + listener.exitDescribeRelation(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDescribeRelation" ): + return visitor.visitDescribeRelation(self) + else: + return visitor.visitChildren(self) + + + class LoadDataContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.path = None # StringLitContext + self.copyFrom(ctx) + + def LOAD(self): + return self.getToken(SqlBaseParser.LOAD, 0) + def DATA(self): + return self.getToken(SqlBaseParser.DATA, 0) + def INPATH(self): + return self.getToken(SqlBaseParser.INPATH, 0) + def INTO(self): + return self.getToken(SqlBaseParser.INTO, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + def LOCAL(self): + return self.getToken(SqlBaseParser.LOCAL, 0) + def OVERWRITE(self): + return self.getToken(SqlBaseParser.OVERWRITE, 0) + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterLoadData" ): + listener.enterLoadData(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitLoadData" ): + listener.exitLoadData(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitLoadData" ): + return visitor.visitLoadData(self) + else: + return visitor.visitChildren(self) + + + class ShowPartitionsContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + def PARTITIONS(self): + return self.getToken(SqlBaseParser.PARTITIONS, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterShowPartitions" ): + listener.enterShowPartitions(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitShowPartitions" ): + listener.exitShowPartitions(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitShowPartitions" ): + return visitor.visitShowPartitions(self) + else: + return visitor.visitChildren(self) + + + class DescribeFunctionContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def FUNCTION(self): + return self.getToken(SqlBaseParser.FUNCTION, 0) + def describeFuncName(self): + return self.getTypedRuleContext(SqlBaseParser.DescribeFuncNameContext,0) + + def DESC(self): + return self.getToken(SqlBaseParser.DESC, 0) + def DESCRIBE(self): + return self.getToken(SqlBaseParser.DESCRIBE, 0) + def EXTENDED(self): + return self.getToken(SqlBaseParser.EXTENDED, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDescribeFunction" ): + listener.enterDescribeFunction(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDescribeFunction" ): + listener.exitDescribeFunction(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDescribeFunction" ): + return visitor.visitDescribeFunction(self) + else: + return visitor.visitChildren(self) + + + class RenameTableColumnContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.table = None # MultipartIdentifierContext + self.from_ = None # MultipartIdentifierContext + self.to = None # ErrorCapturingIdentifierContext + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def RENAME(self): + return self.getToken(SqlBaseParser.RENAME, 0) + def COLUMN(self): + return self.getToken(SqlBaseParser.COLUMN, 0) + def TO(self): + return self.getToken(SqlBaseParser.TO, 0) + def multipartIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.MultipartIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,i) + + def errorCapturingIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRenameTableColumn" ): + listener.enterRenameTableColumn(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRenameTableColumn" ): + listener.exitRenameTableColumn(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRenameTableColumn" ): + return visitor.visitRenameTableColumn(self) + else: + return visitor.visitChildren(self) + + + class StatementDefaultContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterStatementDefault" ): + listener.enterStatementDefault(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitStatementDefault" ): + listener.exitStatementDefault(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitStatementDefault" ): + return visitor.visitStatementDefault(self) + else: + return visitor.visitChildren(self) + + + class HiveChangeColumnContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.table = None # MultipartIdentifierContext + self.colName = None # MultipartIdentifierContext + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def CHANGE(self): + return self.getToken(SqlBaseParser.CHANGE, 0) + def colType(self): + return self.getTypedRuleContext(SqlBaseParser.ColTypeContext,0) + + def multipartIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.MultipartIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,i) + + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + def COLUMN(self): + return self.getToken(SqlBaseParser.COLUMN, 0) + def colPosition(self): + return self.getTypedRuleContext(SqlBaseParser.ColPositionContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterHiveChangeColumn" ): + listener.enterHiveChangeColumn(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitHiveChangeColumn" ): + listener.exitHiveChangeColumn(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitHiveChangeColumn" ): + return visitor.visitHiveChangeColumn(self) + else: + return visitor.visitChildren(self) + + + class SetTimeZoneContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + def TIME(self): + return self.getToken(SqlBaseParser.TIME, 0) + def ZONE(self): + return self.getToken(SqlBaseParser.ZONE, 0) + def interval(self): + return self.getTypedRuleContext(SqlBaseParser.IntervalContext,0) + + def timezone(self): + return self.getTypedRuleContext(SqlBaseParser.TimezoneContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSetTimeZone" ): + listener.enterSetTimeZone(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSetTimeZone" ): + listener.exitSetTimeZone(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSetTimeZone" ): + return visitor.visitSetTimeZone(self) + else: + return visitor.visitChildren(self) + + + class DescribeQueryContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + def DESC(self): + return self.getToken(SqlBaseParser.DESC, 0) + def DESCRIBE(self): + return self.getToken(SqlBaseParser.DESCRIBE, 0) + def QUERY(self): + return self.getToken(SqlBaseParser.QUERY, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDescribeQuery" ): + listener.enterDescribeQuery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDescribeQuery" ): + listener.exitDescribeQuery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDescribeQuery" ): + return visitor.visitDescribeQuery(self) + else: + return visitor.visitChildren(self) + + + class TruncateTableContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def TRUNCATE(self): + return self.getToken(SqlBaseParser.TRUNCATE, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTruncateTable" ): + listener.enterTruncateTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTruncateTable" ): + listener.exitTruncateTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTruncateTable" ): + return visitor.visitTruncateTable(self) + else: + return visitor.visitChildren(self) + + + class SetTableSerDeContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + def SERDE(self): + return self.getToken(SqlBaseParser.SERDE, 0) + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + def WITH(self): + return self.getToken(SqlBaseParser.WITH, 0) + def SERDEPROPERTIES(self): + return self.getToken(SqlBaseParser.SERDEPROPERTIES, 0) + def propertyList(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSetTableSerDe" ): + listener.enterSetTableSerDe(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSetTableSerDe" ): + listener.exitSetTableSerDe(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSetTableSerDe" ): + return visitor.visitSetTableSerDe(self) + else: + return visitor.visitChildren(self) + + + class CreateViewContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def CREATE(self): + return self.getToken(SqlBaseParser.CREATE, 0) + def VIEW(self): + return self.getToken(SqlBaseParser.VIEW, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + def OR(self): + return self.getToken(SqlBaseParser.OR, 0) + def REPLACE(self): + return self.getToken(SqlBaseParser.REPLACE, 0) + def TEMPORARY(self): + return self.getToken(SqlBaseParser.TEMPORARY, 0) + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + def identifierCommentList(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierCommentListContext,0) + + def commentSpec(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.CommentSpecContext) + else: + return self.getTypedRuleContext(SqlBaseParser.CommentSpecContext,i) + + def PARTITIONED(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.PARTITIONED) + else: + return self.getToken(SqlBaseParser.PARTITIONED, i) + def ON(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.ON) + else: + return self.getToken(SqlBaseParser.ON, i) + def identifierList(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.IdentifierListContext) + else: + return self.getTypedRuleContext(SqlBaseParser.IdentifierListContext,i) + + def TBLPROPERTIES(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.TBLPROPERTIES) + else: + return self.getToken(SqlBaseParser.TBLPROPERTIES, i) + def propertyList(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PropertyListContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,i) + + def GLOBAL(self): + return self.getToken(SqlBaseParser.GLOBAL, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCreateView" ): + listener.enterCreateView(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCreateView" ): + listener.exitCreateView(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCreateView" ): + return visitor.visitCreateView(self) + else: + return visitor.visitChildren(self) + + + class DropTablePartitionsContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def DROP(self): + return self.getToken(SqlBaseParser.DROP, 0) + def partitionSpec(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PartitionSpecContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,i) + + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def VIEW(self): + return self.getToken(SqlBaseParser.VIEW, 0) + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + def PURGE(self): + return self.getToken(SqlBaseParser.PURGE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDropTablePartitions" ): + listener.enterDropTablePartitions(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDropTablePartitions" ): + listener.exitDropTablePartitions(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDropTablePartitions" ): + return visitor.visitDropTablePartitions(self) + else: + return visitor.visitChildren(self) + + + class SetConfigurationContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + def configKey(self): + return self.getTypedRuleContext(SqlBaseParser.ConfigKeyContext,0) + + def EQ(self): + return self.getToken(SqlBaseParser.EQ, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSetConfiguration" ): + listener.enterSetConfiguration(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSetConfiguration" ): + listener.exitSetConfiguration(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSetConfiguration" ): + return visitor.visitSetConfiguration(self) + else: + return visitor.visitChildren(self) + + + class DropTableContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def DROP(self): + return self.getToken(SqlBaseParser.DROP, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + def PURGE(self): + return self.getToken(SqlBaseParser.PURGE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDropTable" ): + listener.enterDropTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDropTable" ): + listener.exitDropTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDropTable" ): + return visitor.visitDropTable(self) + else: + return visitor.visitChildren(self) + + + class ShowTableExtendedContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.ns = None # MultipartIdentifierContext + self.pattern = None # StringLitContext + self.copyFrom(ctx) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def EXTENDED(self): + return self.getToken(SqlBaseParser.EXTENDED, 0) + def LIKE(self): + return self.getToken(SqlBaseParser.LIKE, 0) + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterShowTableExtended" ): + listener.enterShowTableExtended(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitShowTableExtended" ): + listener.exitShowTableExtended(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitShowTableExtended" ): + return visitor.visitShowTableExtended(self) + else: + return visitor.visitChildren(self) + + + class DescribeNamespaceContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def namespace(self): + return self.getTypedRuleContext(SqlBaseParser.NamespaceContext,0) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def DESC(self): + return self.getToken(SqlBaseParser.DESC, 0) + def DESCRIBE(self): + return self.getToken(SqlBaseParser.DESCRIBE, 0) + def EXTENDED(self): + return self.getToken(SqlBaseParser.EXTENDED, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDescribeNamespace" ): + listener.enterDescribeNamespace(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDescribeNamespace" ): + listener.exitDescribeNamespace(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDescribeNamespace" ): + return visitor.visitDescribeNamespace(self) + else: + return visitor.visitChildren(self) + + + class AlterTableAlterColumnContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.table = None # MultipartIdentifierContext + self.column = None # MultipartIdentifierContext + self.copyFrom(ctx) + + def ALTER(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.ALTER) + else: + return self.getToken(SqlBaseParser.ALTER, i) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.MultipartIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,i) + + def CHANGE(self): + return self.getToken(SqlBaseParser.CHANGE, 0) + def COLUMN(self): + return self.getToken(SqlBaseParser.COLUMN, 0) + def alterColumnAction(self): + return self.getTypedRuleContext(SqlBaseParser.AlterColumnActionContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAlterTableAlterColumn" ): + listener.enterAlterTableAlterColumn(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAlterTableAlterColumn" ): + listener.exitAlterTableAlterColumn(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAlterTableAlterColumn" ): + return visitor.visitAlterTableAlterColumn(self) + else: + return visitor.visitChildren(self) + + + class RefreshFunctionContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def REFRESH(self): + return self.getToken(SqlBaseParser.REFRESH, 0) + def FUNCTION(self): + return self.getToken(SqlBaseParser.FUNCTION, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRefreshFunction" ): + listener.enterRefreshFunction(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRefreshFunction" ): + listener.exitRefreshFunction(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRefreshFunction" ): + return visitor.visitRefreshFunction(self) + else: + return visitor.visitChildren(self) + + + class CommentTableContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def COMMENT(self): + return self.getToken(SqlBaseParser.COMMENT, 0) + def ON(self): + return self.getToken(SqlBaseParser.ON, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def IS(self): + return self.getToken(SqlBaseParser.IS, 0) + def comment(self): + return self.getTypedRuleContext(SqlBaseParser.CommentContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCommentTable" ): + listener.enterCommentTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCommentTable" ): + listener.exitCommentTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCommentTable" ): + return visitor.visitCommentTable(self) + else: + return visitor.visitChildren(self) + + + class CreateIndexContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.indexType = None # IdentifierContext + self.columns = None # MultipartIdentifierPropertyListContext + self.options = None # PropertyListContext + self.copyFrom(ctx) + + def CREATE(self): + return self.getToken(SqlBaseParser.CREATE, 0) + def INDEX(self): + return self.getToken(SqlBaseParser.INDEX, 0) + def identifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.IdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,i) + + def ON(self): + return self.getToken(SqlBaseParser.ON, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def multipartIdentifierPropertyList(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierPropertyListContext,0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def USING(self): + return self.getToken(SqlBaseParser.USING, 0) + def OPTIONS(self): + return self.getToken(SqlBaseParser.OPTIONS, 0) + def propertyList(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCreateIndex" ): + listener.enterCreateIndex(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCreateIndex" ): + listener.exitCreateIndex(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCreateIndex" ): + return visitor.visitCreateIndex(self) + else: + return visitor.visitChildren(self) + + + class UseNamespaceContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def USE(self): + return self.getToken(SqlBaseParser.USE, 0) + def namespace(self): + return self.getTypedRuleContext(SqlBaseParser.NamespaceContext,0) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUseNamespace" ): + listener.enterUseNamespace(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUseNamespace" ): + listener.exitUseNamespace(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUseNamespace" ): + return visitor.visitUseNamespace(self) + else: + return visitor.visitChildren(self) + + + class CreateNamespaceContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def CREATE(self): + return self.getToken(SqlBaseParser.CREATE, 0) + def namespace(self): + return self.getTypedRuleContext(SqlBaseParser.NamespaceContext,0) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + def commentSpec(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.CommentSpecContext) + else: + return self.getTypedRuleContext(SqlBaseParser.CommentSpecContext,i) + + def locationSpec(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.LocationSpecContext) + else: + return self.getTypedRuleContext(SqlBaseParser.LocationSpecContext,i) + + def WITH(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.WITH) + else: + return self.getToken(SqlBaseParser.WITH, i) + def propertyList(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PropertyListContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,i) + + def DBPROPERTIES(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.DBPROPERTIES) + else: + return self.getToken(SqlBaseParser.DBPROPERTIES, i) + def PROPERTIES(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.PROPERTIES) + else: + return self.getToken(SqlBaseParser.PROPERTIES, i) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCreateNamespace" ): + listener.enterCreateNamespace(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCreateNamespace" ): + listener.exitCreateNamespace(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCreateNamespace" ): + return visitor.visitCreateNamespace(self) + else: + return visitor.visitChildren(self) + + + class ShowTblPropertiesContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.table = None # MultipartIdentifierContext + self.key = None # PropertyKeyContext + self.copyFrom(ctx) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + def TBLPROPERTIES(self): + return self.getToken(SqlBaseParser.TBLPROPERTIES, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def propertyKey(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyKeyContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterShowTblProperties" ): + listener.enterShowTblProperties(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitShowTblProperties" ): + listener.exitShowTblProperties(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitShowTblProperties" ): + return visitor.visitShowTblProperties(self) + else: + return visitor.visitChildren(self) + + + class UnsetTablePropertiesContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def UNSET(self): + return self.getToken(SqlBaseParser.UNSET, 0) + def TBLPROPERTIES(self): + return self.getToken(SqlBaseParser.TBLPROPERTIES, 0) + def propertyList(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,0) + + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def VIEW(self): + return self.getToken(SqlBaseParser.VIEW, 0) + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnsetTableProperties" ): + listener.enterUnsetTableProperties(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnsetTableProperties" ): + listener.exitUnsetTableProperties(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnsetTableProperties" ): + return visitor.visitUnsetTableProperties(self) + else: + return visitor.visitChildren(self) + + + class SetTableLocationContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + def locationSpec(self): + return self.getTypedRuleContext(SqlBaseParser.LocationSpecContext,0) + + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSetTableLocation" ): + listener.enterSetTableLocation(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSetTableLocation" ): + listener.exitSetTableLocation(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSetTableLocation" ): + return visitor.visitSetTableLocation(self) + else: + return visitor.visitChildren(self) + + + class DropTableColumnsContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.columns = None # MultipartIdentifierListContext + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def DROP(self): + return self.getToken(SqlBaseParser.DROP, 0) + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def COLUMN(self): + return self.getToken(SqlBaseParser.COLUMN, 0) + def COLUMNS(self): + return self.getToken(SqlBaseParser.COLUMNS, 0) + def multipartIdentifierList(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierListContext,0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDropTableColumns" ): + listener.enterDropTableColumns(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDropTableColumns" ): + listener.exitDropTableColumns(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDropTableColumns" ): + return visitor.visitDropTableColumns(self) + else: + return visitor.visitChildren(self) + + + class ShowViewsContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.pattern = None # StringLitContext + self.copyFrom(ctx) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + def VIEWS(self): + return self.getToken(SqlBaseParser.VIEWS, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + def LIKE(self): + return self.getToken(SqlBaseParser.LIKE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterShowViews" ): + listener.enterShowViews(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitShowViews" ): + listener.exitShowViews(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitShowViews" ): + return visitor.visitShowViews(self) + else: + return visitor.visitChildren(self) + + + class ShowFunctionsContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.ns = None # MultipartIdentifierContext + self.legacy = None # MultipartIdentifierContext + self.pattern = None # StringLitContext + self.copyFrom(ctx) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + def FUNCTIONS(self): + return self.getToken(SqlBaseParser.FUNCTIONS, 0) + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + def multipartIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.MultipartIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,i) + + def LIKE(self): + return self.getToken(SqlBaseParser.LIKE, 0) + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterShowFunctions" ): + listener.enterShowFunctions(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitShowFunctions" ): + listener.exitShowFunctions(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitShowFunctions" ): + return visitor.visitShowFunctions(self) + else: + return visitor.visitChildren(self) + + + class CacheTableContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.options = None # PropertyListContext + self.copyFrom(ctx) + + def CACHE(self): + return self.getToken(SqlBaseParser.CACHE, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def LAZY(self): + return self.getToken(SqlBaseParser.LAZY, 0) + def OPTIONS(self): + return self.getToken(SqlBaseParser.OPTIONS, 0) + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + def propertyList(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCacheTable" ): + listener.enterCacheTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCacheTable" ): + listener.exitCacheTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCacheTable" ): + return visitor.visitCacheTable(self) + else: + return visitor.visitChildren(self) + + + class AddTableColumnsContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.columns = None # QualifiedColTypeWithPositionListContext + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def ADD(self): + return self.getToken(SqlBaseParser.ADD, 0) + def COLUMN(self): + return self.getToken(SqlBaseParser.COLUMN, 0) + def COLUMNS(self): + return self.getToken(SqlBaseParser.COLUMNS, 0) + def qualifiedColTypeWithPositionList(self): + return self.getTypedRuleContext(SqlBaseParser.QualifiedColTypeWithPositionListContext,0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAddTableColumns" ): + listener.enterAddTableColumns(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAddTableColumns" ): + listener.exitAddTableColumns(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAddTableColumns" ): + return visitor.visitAddTableColumns(self) + else: + return visitor.visitChildren(self) + + + class SetTablePropertiesContext(StatementContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StatementContext + super().__init__(parser) + self.copyFrom(ctx) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + def TBLPROPERTIES(self): + return self.getToken(SqlBaseParser.TBLPROPERTIES, 0) + def propertyList(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,0) + + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def VIEW(self): + return self.getToken(SqlBaseParser.VIEW, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSetTableProperties" ): + listener.enterSetTableProperties(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSetTableProperties" ): + listener.exitSetTableProperties(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSetTableProperties" ): + return visitor.visitSetTableProperties(self) + else: + return visitor.visitChildren(self) + + + + def statement(self): + + localctx = SqlBaseParser.StatementContext(self, self._ctx, self.state) + self.enterRule(localctx, 14, self.RULE_statement) + self._la = 0 # Token type + try: + self.state = 1180 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,121,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.StatementDefaultContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 377 + self.query() + pass + + elif la_ == 2: + localctx = SqlBaseParser.DmlStatementContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 379 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==303: + self.state = 378 + self.ctes() + + + self.state = 381 + self.dmlStatementNoWith() + pass + + elif la_ == 3: + localctx = SqlBaseParser.UseContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 382 + self.match(SqlBaseParser.USE) + self.state = 383 + self.multipartIdentifier() + pass + + elif la_ == 4: + localctx = SqlBaseParser.UseNamespaceContext(self, localctx) + self.enterOuterAlt(localctx, 4) + self.state = 384 + self.match(SqlBaseParser.USE) + self.state = 385 + self.namespace() + self.state = 386 + self.multipartIdentifier() + pass + + elif la_ == 5: + localctx = SqlBaseParser.SetCatalogContext(self, localctx) + self.enterOuterAlt(localctx, 5) + self.state = 388 + self.match(SqlBaseParser.SET) + self.state = 389 + self.match(SqlBaseParser.CATALOG) + self.state = 392 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,2,self._ctx) + if la_ == 1: + self.state = 390 + self.identifier() + pass + + elif la_ == 2: + self.state = 391 + self.stringLit() + pass + + + pass + + elif la_ == 6: + localctx = SqlBaseParser.CreateNamespaceContext(self, localctx) + self.enterOuterAlt(localctx, 6) + self.state = 394 + self.match(SqlBaseParser.CREATE) + self.state = 395 + self.namespace() + self.state = 399 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,3,self._ctx) + if la_ == 1: + self.state = 396 + self.match(SqlBaseParser.IF) + self.state = 397 + self.match(SqlBaseParser.NOT) + self.state = 398 + self.match(SqlBaseParser.EXISTS) + + + self.state = 401 + self.multipartIdentifier() + self.state = 409 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==45 or _la==149 or _la==303: + self.state = 407 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [45]: + self.state = 402 + self.commentSpec() + pass + elif token in [149]: + self.state = 403 + self.locationSpec() + pass + elif token in [303]: + self.state = 404 + self.match(SqlBaseParser.WITH) + self.state = 405 + _la = self._input.LA(1) + if not(_la==69 or _la==202): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 406 + self.propertyList() + pass + else: + raise NoViableAltException(self) + + self.state = 411 + self._errHandler.sync(self) + _la = self._input.LA(1) + + pass + + elif la_ == 7: + localctx = SqlBaseParser.SetNamespacePropertiesContext(self, localctx) + self.enterOuterAlt(localctx, 7) + self.state = 412 + self.match(SqlBaseParser.ALTER) + self.state = 413 + self.namespace() + self.state = 414 + self.multipartIdentifier() + self.state = 415 + self.match(SqlBaseParser.SET) + self.state = 416 + _la = self._input.LA(1) + if not(_la==69 or _la==202): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 417 + self.propertyList() + pass + + elif la_ == 8: + localctx = SqlBaseParser.SetNamespaceLocationContext(self, localctx) + self.enterOuterAlt(localctx, 8) + self.state = 419 + self.match(SqlBaseParser.ALTER) + self.state = 420 + self.namespace() + self.state = 421 + self.multipartIdentifier() + self.state = 422 + self.match(SqlBaseParser.SET) + self.state = 423 + self.locationSpec() + pass + + elif la_ == 9: + localctx = SqlBaseParser.DropNamespaceContext(self, localctx) + self.enterOuterAlt(localctx, 9) + self.state = 425 + self.match(SqlBaseParser.DROP) + self.state = 426 + self.namespace() + self.state = 429 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,6,self._ctx) + if la_ == 1: + self.state = 427 + self.match(SqlBaseParser.IF) + self.state = 428 + self.match(SqlBaseParser.EXISTS) + + + self.state = 431 + self.multipartIdentifier() + self.state = 433 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==30 or _la==219: + self.state = 432 + _la = self._input.LA(1) + if not(_la==30 or _la==219): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + pass + + elif la_ == 10: + localctx = SqlBaseParser.ShowNamespacesContext(self, localctx) + self.enterOuterAlt(localctx, 10) + self.state = 435 + self.match(SqlBaseParser.SHOW) + self.state = 436 + self.namespaces() + self.state = 439 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==107 or _la==122: + self.state = 437 + _la = self._input.LA(1) + if not(_la==107 or _la==122): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 438 + self.multipartIdentifier() + + + self.state = 445 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==142 or _la==330 or _la==331: + self.state = 442 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==142: + self.state = 441 + self.match(SqlBaseParser.LIKE) + + + self.state = 444 + localctx.pattern = self.stringLit() + + + pass + + elif la_ == 11: + localctx = SqlBaseParser.CreateTableContext(self, localctx) + self.enterOuterAlt(localctx, 11) + self.state = 447 + self.createTableHeader() + self.state = 452 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,11,self._ctx) + if la_ == 1: + self.state = 448 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 449 + self.createOrReplaceTableColTypeList() + self.state = 450 + self.match(SqlBaseParser.RIGHT_PAREN) + + + self.state = 455 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==293: + self.state = 454 + self.tableProvider() + + + self.state = 457 + self.createTableClauses() + self.state = 462 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==2 or _la==20 or _la==107 or _la==154 or ((((_la - 210)) & ~0x3f) == 0 and ((1 << (_la - 210)) & 281474985099265) != 0) or _la==294 or _la==303: + self.state = 459 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==20: + self.state = 458 + self.match(SqlBaseParser.AS) + + + self.state = 461 + self.query() + + + pass + + elif la_ == 12: + localctx = SqlBaseParser.CreateTableLikeContext(self, localctx) + self.enterOuterAlt(localctx, 12) + self.state = 464 + self.match(SqlBaseParser.CREATE) + self.state = 465 + self.match(SqlBaseParser.TABLE) + self.state = 469 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,15,self._ctx) + if la_ == 1: + self.state = 466 + self.match(SqlBaseParser.IF) + self.state = 467 + self.match(SqlBaseParser.NOT) + self.state = 468 + self.match(SqlBaseParser.EXISTS) + + + self.state = 471 + localctx.target = self.tableIdentifier() + self.state = 472 + self.match(SqlBaseParser.LIKE) + self.state = 473 + localctx.source = self.tableIdentifier() + self.state = 482 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==149 or ((((_la - 227)) & ~0x3f) == 0 and ((1 << (_la - 227)) & 34368126977) != 0) or _la==293: + self.state = 480 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [293]: + self.state = 474 + self.tableProvider() + pass + elif token in [227]: + self.state = 475 + self.rowFormat() + pass + elif token in [250]: + self.state = 476 + self.createFileFormat() + pass + elif token in [149]: + self.state = 477 + self.locationSpec() + pass + elif token in [262]: + self.state = 478 + self.match(SqlBaseParser.TBLPROPERTIES) + self.state = 479 + localctx.tableProps = self.propertyList() + pass + else: + raise NoViableAltException(self) + + self.state = 484 + self._errHandler.sync(self) + _la = self._input.LA(1) + + pass + + elif la_ == 13: + localctx = SqlBaseParser.ReplaceTableContext(self, localctx) + self.enterOuterAlt(localctx, 13) + self.state = 485 + self.replaceTableHeader() + self.state = 490 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,18,self._ctx) + if la_ == 1: + self.state = 486 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 487 + self.createOrReplaceTableColTypeList() + self.state = 488 + self.match(SqlBaseParser.RIGHT_PAREN) + + + self.state = 493 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==293: + self.state = 492 + self.tableProvider() + + + self.state = 495 + self.createTableClauses() + self.state = 500 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==2 or _la==20 or _la==107 or _la==154 or ((((_la - 210)) & ~0x3f) == 0 and ((1 << (_la - 210)) & 281474985099265) != 0) or _la==294 or _la==303: + self.state = 497 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==20: + self.state = 496 + self.match(SqlBaseParser.AS) + + + self.state = 499 + self.query() + + + pass + + elif la_ == 14: + localctx = SqlBaseParser.AnalyzeContext(self, localctx) + self.enterOuterAlt(localctx, 14) + self.state = 502 + self.match(SqlBaseParser.ANALYZE) + self.state = 503 + self.match(SqlBaseParser.TABLE) + self.state = 504 + self.multipartIdentifier() + self.state = 506 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 505 + self.partitionSpec() + + + self.state = 508 + self.match(SqlBaseParser.COMPUTE) + self.state = 509 + self.match(SqlBaseParser.STATISTICS) + self.state = 517 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,23,self._ctx) + if la_ == 1: + self.state = 510 + self.identifier() + + elif la_ == 2: + self.state = 511 + self.match(SqlBaseParser.FOR) + self.state = 512 + self.match(SqlBaseParser.COLUMNS) + self.state = 513 + self.identifierSeq() + + elif la_ == 3: + self.state = 514 + self.match(SqlBaseParser.FOR) + self.state = 515 + self.match(SqlBaseParser.ALL) + self.state = 516 + self.match(SqlBaseParser.COLUMNS) + + + pass + + elif la_ == 15: + localctx = SqlBaseParser.AnalyzeTablesContext(self, localctx) + self.enterOuterAlt(localctx, 15) + self.state = 519 + self.match(SqlBaseParser.ANALYZE) + self.state = 520 + self.match(SqlBaseParser.TABLES) + self.state = 523 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==107 or _la==122: + self.state = 521 + _la = self._input.LA(1) + if not(_la==107 or _la==122): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 522 + self.multipartIdentifier() + + + self.state = 525 + self.match(SqlBaseParser.COMPUTE) + self.state = 526 + self.match(SqlBaseParser.STATISTICS) + self.state = 528 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -256) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 4503599627370495) != 0) or ((((_la - 331)) & ~0x3f) == 0 and ((1 << (_la - 331)) & 3073) != 0): + self.state = 527 + self.identifier() + + + pass + + elif la_ == 16: + localctx = SqlBaseParser.AddTableColumnsContext(self, localctx) + self.enterOuterAlt(localctx, 16) + self.state = 530 + self.match(SqlBaseParser.ALTER) + self.state = 531 + self.match(SqlBaseParser.TABLE) + self.state = 532 + self.multipartIdentifier() + self.state = 533 + self.match(SqlBaseParser.ADD) + self.state = 534 + _la = self._input.LA(1) + if not(_la==43 or _la==44): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 535 + localctx.columns = self.qualifiedColTypeWithPositionList() + pass + + elif la_ == 17: + localctx = SqlBaseParser.AddTableColumnsContext(self, localctx) + self.enterOuterAlt(localctx, 17) + self.state = 537 + self.match(SqlBaseParser.ALTER) + self.state = 538 + self.match(SqlBaseParser.TABLE) + self.state = 539 + self.multipartIdentifier() + self.state = 540 + self.match(SqlBaseParser.ADD) + self.state = 541 + _la = self._input.LA(1) + if not(_la==43 or _la==44): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 542 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 543 + localctx.columns = self.qualifiedColTypeWithPositionList() + self.state = 544 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 18: + localctx = SqlBaseParser.RenameTableColumnContext(self, localctx) + self.enterOuterAlt(localctx, 18) + self.state = 546 + self.match(SqlBaseParser.ALTER) + self.state = 547 + self.match(SqlBaseParser.TABLE) + self.state = 548 + localctx.table = self.multipartIdentifier() + self.state = 549 + self.match(SqlBaseParser.RENAME) + self.state = 550 + self.match(SqlBaseParser.COLUMN) + self.state = 551 + localctx.from_ = self.multipartIdentifier() + self.state = 552 + self.match(SqlBaseParser.TO) + self.state = 553 + localctx.to = self.errorCapturingIdentifier() + pass + + elif la_ == 19: + localctx = SqlBaseParser.DropTableColumnsContext(self, localctx) + self.enterOuterAlt(localctx, 19) + self.state = 555 + self.match(SqlBaseParser.ALTER) + self.state = 556 + self.match(SqlBaseParser.TABLE) + self.state = 557 + self.multipartIdentifier() + self.state = 558 + self.match(SqlBaseParser.DROP) + self.state = 559 + _la = self._input.LA(1) + if not(_la==43 or _la==44): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 562 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==119: + self.state = 560 + self.match(SqlBaseParser.IF) + self.state = 561 + self.match(SqlBaseParser.EXISTS) + + + self.state = 564 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 565 + localctx.columns = self.multipartIdentifierList() + self.state = 566 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 20: + localctx = SqlBaseParser.DropTableColumnsContext(self, localctx) + self.enterOuterAlt(localctx, 20) + self.state = 568 + self.match(SqlBaseParser.ALTER) + self.state = 569 + self.match(SqlBaseParser.TABLE) + self.state = 570 + self.multipartIdentifier() + self.state = 571 + self.match(SqlBaseParser.DROP) + self.state = 572 + _la = self._input.LA(1) + if not(_la==43 or _la==44): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 575 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,27,self._ctx) + if la_ == 1: + self.state = 573 + self.match(SqlBaseParser.IF) + self.state = 574 + self.match(SqlBaseParser.EXISTS) + + + self.state = 577 + localctx.columns = self.multipartIdentifierList() + pass + + elif la_ == 21: + localctx = SqlBaseParser.RenameTableContext(self, localctx) + self.enterOuterAlt(localctx, 21) + self.state = 579 + self.match(SqlBaseParser.ALTER) + self.state = 580 + _la = self._input.LA(1) + if not(_la==258 or _la==296): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 581 + localctx.from_ = self.multipartIdentifier() + self.state = 582 + self.match(SqlBaseParser.RENAME) + self.state = 583 + self.match(SqlBaseParser.TO) + self.state = 584 + localctx.to = self.multipartIdentifier() + pass + + elif la_ == 22: + localctx = SqlBaseParser.SetTablePropertiesContext(self, localctx) + self.enterOuterAlt(localctx, 22) + self.state = 586 + self.match(SqlBaseParser.ALTER) + self.state = 587 + _la = self._input.LA(1) + if not(_la==258 or _la==296): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 588 + self.multipartIdentifier() + self.state = 589 + self.match(SqlBaseParser.SET) + self.state = 590 + self.match(SqlBaseParser.TBLPROPERTIES) + self.state = 591 + self.propertyList() + pass + + elif la_ == 23: + localctx = SqlBaseParser.UnsetTablePropertiesContext(self, localctx) + self.enterOuterAlt(localctx, 23) + self.state = 593 + self.match(SqlBaseParser.ALTER) + self.state = 594 + _la = self._input.LA(1) + if not(_la==258 or _la==296): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 595 + self.multipartIdentifier() + self.state = 596 + self.match(SqlBaseParser.UNSET) + self.state = 597 + self.match(SqlBaseParser.TBLPROPERTIES) + self.state = 600 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==119: + self.state = 598 + self.match(SqlBaseParser.IF) + self.state = 599 + self.match(SqlBaseParser.EXISTS) + + + self.state = 602 + self.propertyList() + pass + + elif la_ == 24: + localctx = SqlBaseParser.AlterTableAlterColumnContext(self, localctx) + self.enterOuterAlt(localctx, 24) + self.state = 604 + self.match(SqlBaseParser.ALTER) + self.state = 605 + self.match(SqlBaseParser.TABLE) + self.state = 606 + localctx.table = self.multipartIdentifier() + self.state = 607 + _la = self._input.LA(1) + if not(_la==11 or _la==35): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 609 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,29,self._ctx) + if la_ == 1: + self.state = 608 + self.match(SqlBaseParser.COLUMN) + + + self.state = 611 + localctx.column = self.multipartIdentifier() + self.state = 613 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==9 or _la==45 or _la==82 or _la==101 or _la==239 or _la==280: + self.state = 612 + self.alterColumnAction() + + + pass + + elif la_ == 25: + localctx = SqlBaseParser.HiveChangeColumnContext(self, localctx) + self.enterOuterAlt(localctx, 25) + self.state = 615 + self.match(SqlBaseParser.ALTER) + self.state = 616 + self.match(SqlBaseParser.TABLE) + self.state = 617 + localctx.table = self.multipartIdentifier() + self.state = 619 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 618 + self.partitionSpec() + + + self.state = 621 + self.match(SqlBaseParser.CHANGE) + self.state = 623 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,32,self._ctx) + if la_ == 1: + self.state = 622 + self.match(SqlBaseParser.COLUMN) + + + self.state = 625 + localctx.colName = self.multipartIdentifier() + self.state = 626 + self.colType() + self.state = 628 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==9 or _la==101: + self.state = 627 + self.colPosition() + + + pass + + elif la_ == 26: + localctx = SqlBaseParser.HiveReplaceColumnsContext(self, localctx) + self.enterOuterAlt(localctx, 26) + self.state = 630 + self.match(SqlBaseParser.ALTER) + self.state = 631 + self.match(SqlBaseParser.TABLE) + self.state = 632 + localctx.table = self.multipartIdentifier() + self.state = 634 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 633 + self.partitionSpec() + + + self.state = 636 + self.match(SqlBaseParser.REPLACE) + self.state = 637 + self.match(SqlBaseParser.COLUMNS) + self.state = 638 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 639 + localctx.columns = self.qualifiedColTypeWithPositionList() + self.state = 640 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 27: + localctx = SqlBaseParser.SetTableSerDeContext(self, localctx) + self.enterOuterAlt(localctx, 27) + self.state = 642 + self.match(SqlBaseParser.ALTER) + self.state = 643 + self.match(SqlBaseParser.TABLE) + self.state = 644 + self.multipartIdentifier() + self.state = 646 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 645 + self.partitionSpec() + + + self.state = 648 + self.match(SqlBaseParser.SET) + self.state = 649 + self.match(SqlBaseParser.SERDE) + self.state = 650 + self.stringLit() + self.state = 654 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==303: + self.state = 651 + self.match(SqlBaseParser.WITH) + self.state = 652 + self.match(SqlBaseParser.SERDEPROPERTIES) + self.state = 653 + self.propertyList() + + + pass + + elif la_ == 28: + localctx = SqlBaseParser.SetTableSerDeContext(self, localctx) + self.enterOuterAlt(localctx, 28) + self.state = 656 + self.match(SqlBaseParser.ALTER) + self.state = 657 + self.match(SqlBaseParser.TABLE) + self.state = 658 + self.multipartIdentifier() + self.state = 660 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 659 + self.partitionSpec() + + + self.state = 662 + self.match(SqlBaseParser.SET) + self.state = 663 + self.match(SqlBaseParser.SERDEPROPERTIES) + self.state = 664 + self.propertyList() + pass + + elif la_ == 29: + localctx = SqlBaseParser.AddTablePartitionContext(self, localctx) + self.enterOuterAlt(localctx, 29) + self.state = 666 + self.match(SqlBaseParser.ALTER) + self.state = 667 + _la = self._input.LA(1) + if not(_la==258 or _la==296): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 668 + self.multipartIdentifier() + self.state = 669 + self.match(SqlBaseParser.ADD) + self.state = 673 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==119: + self.state = 670 + self.match(SqlBaseParser.IF) + self.state = 671 + self.match(SqlBaseParser.NOT) + self.state = 672 + self.match(SqlBaseParser.EXISTS) + + + self.state = 676 + self._errHandler.sync(self) + _la = self._input.LA(1) + while True: + self.state = 675 + self.partitionSpecLocation() + self.state = 678 + self._errHandler.sync(self) + _la = self._input.LA(1) + if not (_la==190): + break + + pass + + elif la_ == 30: + localctx = SqlBaseParser.RenameTablePartitionContext(self, localctx) + self.enterOuterAlt(localctx, 30) + self.state = 680 + self.match(SqlBaseParser.ALTER) + self.state = 681 + self.match(SqlBaseParser.TABLE) + self.state = 682 + self.multipartIdentifier() + self.state = 683 + localctx.from_ = self.partitionSpec() + self.state = 684 + self.match(SqlBaseParser.RENAME) + self.state = 685 + self.match(SqlBaseParser.TO) + self.state = 686 + localctx.to = self.partitionSpec() + pass + + elif la_ == 31: + localctx = SqlBaseParser.DropTablePartitionsContext(self, localctx) + self.enterOuterAlt(localctx, 31) + self.state = 688 + self.match(SqlBaseParser.ALTER) + self.state = 689 + _la = self._input.LA(1) + if not(_la==258 or _la==296): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 690 + self.multipartIdentifier() + self.state = 691 + self.match(SqlBaseParser.DROP) + self.state = 694 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==119: + self.state = 692 + self.match(SqlBaseParser.IF) + self.state = 693 + self.match(SqlBaseParser.EXISTS) + + + self.state = 696 + self.partitionSpec() + self.state = 701 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 697 + self.match(SqlBaseParser.COMMA) + self.state = 698 + self.partitionSpec() + self.state = 703 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 705 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==203: + self.state = 704 + self.match(SqlBaseParser.PURGE) + + + pass + + elif la_ == 32: + localctx = SqlBaseParser.SetTableLocationContext(self, localctx) + self.enterOuterAlt(localctx, 32) + self.state = 707 + self.match(SqlBaseParser.ALTER) + self.state = 708 + self.match(SqlBaseParser.TABLE) + self.state = 709 + self.multipartIdentifier() + self.state = 711 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 710 + self.partitionSpec() + + + self.state = 713 + self.match(SqlBaseParser.SET) + self.state = 714 + self.locationSpec() + pass + + elif la_ == 33: + localctx = SqlBaseParser.RecoverPartitionsContext(self, localctx) + self.enterOuterAlt(localctx, 33) + self.state = 716 + self.match(SqlBaseParser.ALTER) + self.state = 717 + self.match(SqlBaseParser.TABLE) + self.state = 718 + self.multipartIdentifier() + self.state = 719 + self.match(SqlBaseParser.RECOVER) + self.state = 720 + self.match(SqlBaseParser.PARTITIONS) + pass + + elif la_ == 34: + localctx = SqlBaseParser.DropTableContext(self, localctx) + self.enterOuterAlt(localctx, 34) + self.state = 722 + self.match(SqlBaseParser.DROP) + self.state = 723 + self.match(SqlBaseParser.TABLE) + self.state = 726 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,44,self._ctx) + if la_ == 1: + self.state = 724 + self.match(SqlBaseParser.IF) + self.state = 725 + self.match(SqlBaseParser.EXISTS) + + + self.state = 728 + self.multipartIdentifier() + self.state = 730 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==203: + self.state = 729 + self.match(SqlBaseParser.PURGE) + + + pass + + elif la_ == 35: + localctx = SqlBaseParser.DropViewContext(self, localctx) + self.enterOuterAlt(localctx, 35) + self.state = 732 + self.match(SqlBaseParser.DROP) + self.state = 733 + self.match(SqlBaseParser.VIEW) + self.state = 736 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,46,self._ctx) + if la_ == 1: + self.state = 734 + self.match(SqlBaseParser.IF) + self.state = 735 + self.match(SqlBaseParser.EXISTS) + + + self.state = 738 + self.multipartIdentifier() + pass + + elif la_ == 36: + localctx = SqlBaseParser.CreateViewContext(self, localctx) + self.enterOuterAlt(localctx, 36) + self.state = 739 + self.match(SqlBaseParser.CREATE) + self.state = 742 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==181: + self.state = 740 + self.match(SqlBaseParser.OR) + self.state = 741 + self.match(SqlBaseParser.REPLACE) + + + self.state = 748 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==112 or _la==263: + self.state = 745 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==112: + self.state = 744 + self.match(SqlBaseParser.GLOBAL) + + + self.state = 747 + self.match(SqlBaseParser.TEMPORARY) + + + self.state = 750 + self.match(SqlBaseParser.VIEW) + self.state = 754 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,50,self._ctx) + if la_ == 1: + self.state = 751 + self.match(SqlBaseParser.IF) + self.state = 752 + self.match(SqlBaseParser.NOT) + self.state = 753 + self.match(SqlBaseParser.EXISTS) + + + self.state = 756 + self.multipartIdentifier() + self.state = 758 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==2: + self.state = 757 + self.identifierCommentList() + + + self.state = 768 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==45 or _la==191 or _la==262: + self.state = 766 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [45]: + self.state = 760 + self.commentSpec() + pass + elif token in [191]: + self.state = 761 + self.match(SqlBaseParser.PARTITIONED) + self.state = 762 + self.match(SqlBaseParser.ON) + self.state = 763 + self.identifierList() + pass + elif token in [262]: + self.state = 764 + self.match(SqlBaseParser.TBLPROPERTIES) + self.state = 765 + self.propertyList() + pass + else: + raise NoViableAltException(self) + + self.state = 770 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 771 + self.match(SqlBaseParser.AS) + self.state = 772 + self.query() + pass + + elif la_ == 37: + localctx = SqlBaseParser.CreateTempViewUsingContext(self, localctx) + self.enterOuterAlt(localctx, 37) + self.state = 774 + self.match(SqlBaseParser.CREATE) + self.state = 777 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==181: + self.state = 775 + self.match(SqlBaseParser.OR) + self.state = 776 + self.match(SqlBaseParser.REPLACE) + + + self.state = 780 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==112: + self.state = 779 + self.match(SqlBaseParser.GLOBAL) + + + self.state = 782 + self.match(SqlBaseParser.TEMPORARY) + self.state = 783 + self.match(SqlBaseParser.VIEW) + self.state = 784 + self.tableIdentifier() + self.state = 789 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==2: + self.state = 785 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 786 + self.colTypeList() + self.state = 787 + self.match(SqlBaseParser.RIGHT_PAREN) + + + self.state = 791 + self.tableProvider() + self.state = 794 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==180: + self.state = 792 + self.match(SqlBaseParser.OPTIONS) + self.state = 793 + self.propertyList() + + + pass + + elif la_ == 38: + localctx = SqlBaseParser.AlterViewQueryContext(self, localctx) + self.enterOuterAlt(localctx, 38) + self.state = 796 + self.match(SqlBaseParser.ALTER) + self.state = 797 + self.match(SqlBaseParser.VIEW) + self.state = 798 + self.multipartIdentifier() + self.state = 800 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==20: + self.state = 799 + self.match(SqlBaseParser.AS) + + + self.state = 802 + self.query() + pass + + elif la_ == 39: + localctx = SqlBaseParser.CreateFunctionContext(self, localctx) + self.enterOuterAlt(localctx, 39) + self.state = 804 + self.match(SqlBaseParser.CREATE) + self.state = 807 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==181: + self.state = 805 + self.match(SqlBaseParser.OR) + self.state = 806 + self.match(SqlBaseParser.REPLACE) + + + self.state = 810 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==263: + self.state = 809 + self.match(SqlBaseParser.TEMPORARY) + + + self.state = 812 + self.match(SqlBaseParser.FUNCTION) + self.state = 816 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,61,self._ctx) + if la_ == 1: + self.state = 813 + self.match(SqlBaseParser.IF) + self.state = 814 + self.match(SqlBaseParser.NOT) + self.state = 815 + self.match(SqlBaseParser.EXISTS) + + + self.state = 818 + self.multipartIdentifier() + self.state = 819 + self.match(SqlBaseParser.AS) + self.state = 820 + localctx.className = self.stringLit() + self.state = 830 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==293: + self.state = 821 + self.match(SqlBaseParser.USING) + self.state = 822 + self.resource() + self.state = 827 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 823 + self.match(SqlBaseParser.COMMA) + self.state = 824 + self.resource() + self.state = 829 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + pass + + elif la_ == 40: + localctx = SqlBaseParser.DropFunctionContext(self, localctx) + self.enterOuterAlt(localctx, 40) + self.state = 832 + self.match(SqlBaseParser.DROP) + self.state = 834 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==263: + self.state = 833 + self.match(SqlBaseParser.TEMPORARY) + + + self.state = 836 + self.match(SqlBaseParser.FUNCTION) + self.state = 839 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,65,self._ctx) + if la_ == 1: + self.state = 837 + self.match(SqlBaseParser.IF) + self.state = 838 + self.match(SqlBaseParser.EXISTS) + + + self.state = 841 + self.multipartIdentifier() + pass + + elif la_ == 41: + localctx = SqlBaseParser.ExplainContext(self, localctx) + self.enterOuterAlt(localctx, 41) + self.state = 842 + self.match(SqlBaseParser.EXPLAIN) + self.state = 844 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==40 or _la==52 or ((((_la - 93)) & ~0x3f) == 0 and ((1 << (_la - 93)) & 576460752303431681) != 0): + self.state = 843 + _la = self._input.LA(1) + if not(_la==40 or _la==52 or ((((_la - 93)) & ~0x3f) == 0 and ((1 << (_la - 93)) & 576460752303431681) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + self.state = 846 + self.statement() + pass + + elif la_ == 42: + localctx = SqlBaseParser.ShowTablesContext(self, localctx) + self.enterOuterAlt(localctx, 42) + self.state = 847 + self.match(SqlBaseParser.SHOW) + self.state = 848 + self.match(SqlBaseParser.TABLES) + self.state = 851 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==107 or _la==122: + self.state = 849 + _la = self._input.LA(1) + if not(_la==107 or _la==122): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 850 + self.multipartIdentifier() + + + self.state = 857 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==142 or _la==330 or _la==331: + self.state = 854 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==142: + self.state = 853 + self.match(SqlBaseParser.LIKE) + + + self.state = 856 + localctx.pattern = self.stringLit() + + + pass + + elif la_ == 43: + localctx = SqlBaseParser.ShowTableExtendedContext(self, localctx) + self.enterOuterAlt(localctx, 43) + self.state = 859 + self.match(SqlBaseParser.SHOW) + self.state = 860 + self.match(SqlBaseParser.TABLE) + self.state = 861 + self.match(SqlBaseParser.EXTENDED) + self.state = 864 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==107 or _la==122: + self.state = 862 + _la = self._input.LA(1) + if not(_la==107 or _la==122): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 863 + localctx.ns = self.multipartIdentifier() + + + self.state = 866 + self.match(SqlBaseParser.LIKE) + self.state = 867 + localctx.pattern = self.stringLit() + self.state = 869 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 868 + self.partitionSpec() + + + pass + + elif la_ == 44: + localctx = SqlBaseParser.ShowTblPropertiesContext(self, localctx) + self.enterOuterAlt(localctx, 44) + self.state = 871 + self.match(SqlBaseParser.SHOW) + self.state = 872 + self.match(SqlBaseParser.TBLPROPERTIES) + self.state = 873 + localctx.table = self.multipartIdentifier() + self.state = 878 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==2: + self.state = 874 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 875 + localctx.key = self.propertyKey() + self.state = 876 + self.match(SqlBaseParser.RIGHT_PAREN) + + + pass + + elif la_ == 45: + localctx = SqlBaseParser.ShowColumnsContext(self, localctx) + self.enterOuterAlt(localctx, 45) + self.state = 880 + self.match(SqlBaseParser.SHOW) + self.state = 881 + self.match(SqlBaseParser.COLUMNS) + self.state = 882 + _la = self._input.LA(1) + if not(_la==107 or _la==122): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 883 + localctx.table = self.multipartIdentifier() + self.state = 886 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==107 or _la==122: + self.state = 884 + _la = self._input.LA(1) + if not(_la==107 or _la==122): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 885 + localctx.ns = self.multipartIdentifier() + + + pass + + elif la_ == 46: + localctx = SqlBaseParser.ShowViewsContext(self, localctx) + self.enterOuterAlt(localctx, 46) + self.state = 888 + self.match(SqlBaseParser.SHOW) + self.state = 889 + self.match(SqlBaseParser.VIEWS) + self.state = 892 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==107 or _la==122: + self.state = 890 + _la = self._input.LA(1) + if not(_la==107 or _la==122): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 891 + self.multipartIdentifier() + + + self.state = 898 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==142 or _la==330 or _la==331: + self.state = 895 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==142: + self.state = 894 + self.match(SqlBaseParser.LIKE) + + + self.state = 897 + localctx.pattern = self.stringLit() + + + pass + + elif la_ == 47: + localctx = SqlBaseParser.ShowPartitionsContext(self, localctx) + self.enterOuterAlt(localctx, 47) + self.state = 900 + self.match(SqlBaseParser.SHOW) + self.state = 901 + self.match(SqlBaseParser.PARTITIONS) + self.state = 902 + self.multipartIdentifier() + self.state = 904 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 903 + self.partitionSpec() + + + pass + + elif la_ == 48: + localctx = SqlBaseParser.ShowFunctionsContext(self, localctx) + self.enterOuterAlt(localctx, 48) + self.state = 906 + self.match(SqlBaseParser.SHOW) + self.state = 908 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,78,self._ctx) + if la_ == 1: + self.state = 907 + self.identifier() + + + self.state = 910 + self.match(SqlBaseParser.FUNCTIONS) + self.state = 913 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,79,self._ctx) + if la_ == 1: + self.state = 911 + _la = self._input.LA(1) + if not(_la==107 or _la==122): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 912 + localctx.ns = self.multipartIdentifier() + + + self.state = 922 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -256) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 4503599627370495) != 0) or ((((_la - 330)) & ~0x3f) == 0 and ((1 << (_la - 330)) & 6147) != 0): + self.state = 916 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,80,self._ctx) + if la_ == 1: + self.state = 915 + self.match(SqlBaseParser.LIKE) + + + self.state = 920 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,81,self._ctx) + if la_ == 1: + self.state = 918 + localctx.legacy = self.multipartIdentifier() + pass + + elif la_ == 2: + self.state = 919 + localctx.pattern = self.stringLit() + pass + + + + + pass + + elif la_ == 49: + localctx = SqlBaseParser.ShowCreateTableContext(self, localctx) + self.enterOuterAlt(localctx, 49) + self.state = 924 + self.match(SqlBaseParser.SHOW) + self.state = 925 + self.match(SqlBaseParser.CREATE) + self.state = 926 + self.match(SqlBaseParser.TABLE) + self.state = 927 + self.multipartIdentifier() + self.state = 930 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==20: + self.state = 928 + self.match(SqlBaseParser.AS) + self.state = 929 + self.match(SqlBaseParser.SERDE) + + + pass + + elif la_ == 50: + localctx = SqlBaseParser.ShowCurrentNamespaceContext(self, localctx) + self.enterOuterAlt(localctx, 50) + self.state = 932 + self.match(SqlBaseParser.SHOW) + self.state = 933 + self.match(SqlBaseParser.CURRENT) + self.state = 934 + self.namespace() + pass + + elif la_ == 51: + localctx = SqlBaseParser.ShowCatalogsContext(self, localctx) + self.enterOuterAlt(localctx, 51) + self.state = 935 + self.match(SqlBaseParser.SHOW) + self.state = 936 + self.match(SqlBaseParser.CATALOGS) + self.state = 941 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==142 or _la==330 or _la==331: + self.state = 938 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==142: + self.state = 937 + self.match(SqlBaseParser.LIKE) + + + self.state = 940 + localctx.pattern = self.stringLit() + + + pass + + elif la_ == 52: + localctx = SqlBaseParser.DescribeFunctionContext(self, localctx) + self.enterOuterAlt(localctx, 52) + self.state = 943 + _la = self._input.LA(1) + if not(_la==74 or _la==75): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 944 + self.match(SqlBaseParser.FUNCTION) + self.state = 946 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,86,self._ctx) + if la_ == 1: + self.state = 945 + self.match(SqlBaseParser.EXTENDED) + + + self.state = 948 + self.describeFuncName() + pass + + elif la_ == 53: + localctx = SqlBaseParser.DescribeNamespaceContext(self, localctx) + self.enterOuterAlt(localctx, 53) + self.state = 949 + _la = self._input.LA(1) + if not(_la==74 or _la==75): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 950 + self.namespace() + self.state = 952 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,87,self._ctx) + if la_ == 1: + self.state = 951 + self.match(SqlBaseParser.EXTENDED) + + + self.state = 954 + self.multipartIdentifier() + pass + + elif la_ == 54: + localctx = SqlBaseParser.DescribeRelationContext(self, localctx) + self.enterOuterAlt(localctx, 54) + self.state = 956 + _la = self._input.LA(1) + if not(_la==74 or _la==75): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 958 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,88,self._ctx) + if la_ == 1: + self.state = 957 + self.match(SqlBaseParser.TABLE) + + + self.state = 961 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,89,self._ctx) + if la_ == 1: + self.state = 960 + localctx.option = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==93 or _la==106): + localctx.option = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + self.state = 963 + self.multipartIdentifier() + self.state = 965 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,90,self._ctx) + if la_ == 1: + self.state = 964 + self.partitionSpec() + + + self.state = 968 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -256) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 4503599627370495) != 0) or ((((_la - 331)) & ~0x3f) == 0 and ((1 << (_la - 331)) & 3073) != 0): + self.state = 967 + self.describeColName() + + + pass + + elif la_ == 55: + localctx = SqlBaseParser.DescribeQueryContext(self, localctx) + self.enterOuterAlt(localctx, 55) + self.state = 970 + _la = self._input.LA(1) + if not(_la==74 or _la==75): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 972 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==205: + self.state = 971 + self.match(SqlBaseParser.QUERY) + + + self.state = 974 + self.query() + pass + + elif la_ == 56: + localctx = SqlBaseParser.CommentNamespaceContext(self, localctx) + self.enterOuterAlt(localctx, 56) + self.state = 975 + self.match(SqlBaseParser.COMMENT) + self.state = 976 + self.match(SqlBaseParser.ON) + self.state = 977 + self.namespace() + self.state = 978 + self.multipartIdentifier() + self.state = 979 + self.match(SqlBaseParser.IS) + self.state = 980 + self.comment() + pass + + elif la_ == 57: + localctx = SqlBaseParser.CommentTableContext(self, localctx) + self.enterOuterAlt(localctx, 57) + self.state = 982 + self.match(SqlBaseParser.COMMENT) + self.state = 983 + self.match(SqlBaseParser.ON) + self.state = 984 + self.match(SqlBaseParser.TABLE) + self.state = 985 + self.multipartIdentifier() + self.state = 986 + self.match(SqlBaseParser.IS) + self.state = 987 + self.comment() + pass + + elif la_ == 58: + localctx = SqlBaseParser.RefreshTableContext(self, localctx) + self.enterOuterAlt(localctx, 58) + self.state = 989 + self.match(SqlBaseParser.REFRESH) + self.state = 990 + self.match(SqlBaseParser.TABLE) + self.state = 991 + self.multipartIdentifier() + pass + + elif la_ == 59: + localctx = SqlBaseParser.RefreshFunctionContext(self, localctx) + self.enterOuterAlt(localctx, 59) + self.state = 992 + self.match(SqlBaseParser.REFRESH) + self.state = 993 + self.match(SqlBaseParser.FUNCTION) + self.state = 994 + self.multipartIdentifier() + pass + + elif la_ == 60: + localctx = SqlBaseParser.RefreshResourceContext(self, localctx) + self.enterOuterAlt(localctx, 60) + self.state = 995 + self.match(SqlBaseParser.REFRESH) + self.state = 1003 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,94,self._ctx) + if la_ == 1: + self.state = 996 + self.stringLit() + pass + + elif la_ == 2: + self.state = 1000 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,93,self._ctx) + while _alt!=1 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1+1: + self.state = 997 + self.matchWildcard() + self.state = 1002 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,93,self._ctx) + + pass + + + pass + + elif la_ == 61: + localctx = SqlBaseParser.CacheTableContext(self, localctx) + self.enterOuterAlt(localctx, 61) + self.state = 1005 + self.match(SqlBaseParser.CACHE) + self.state = 1007 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==139: + self.state = 1006 + self.match(SqlBaseParser.LAZY) + + + self.state = 1009 + self.match(SqlBaseParser.TABLE) + self.state = 1010 + self.multipartIdentifier() + self.state = 1013 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==180: + self.state = 1011 + self.match(SqlBaseParser.OPTIONS) + self.state = 1012 + localctx.options = self.propertyList() + + + self.state = 1019 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==2 or _la==20 or _la==107 or _la==154 or ((((_la - 210)) & ~0x3f) == 0 and ((1 << (_la - 210)) & 281474985099265) != 0) or _la==294 or _la==303: + self.state = 1016 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==20: + self.state = 1015 + self.match(SqlBaseParser.AS) + + + self.state = 1018 + self.query() + + + pass + + elif la_ == 62: + localctx = SqlBaseParser.UncacheTableContext(self, localctx) + self.enterOuterAlt(localctx, 62) + self.state = 1021 + self.match(SqlBaseParser.UNCACHE) + self.state = 1022 + self.match(SqlBaseParser.TABLE) + self.state = 1025 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,99,self._ctx) + if la_ == 1: + self.state = 1023 + self.match(SqlBaseParser.IF) + self.state = 1024 + self.match(SqlBaseParser.EXISTS) + + + self.state = 1027 + self.multipartIdentifier() + pass + + elif la_ == 63: + localctx = SqlBaseParser.ClearCacheContext(self, localctx) + self.enterOuterAlt(localctx, 63) + self.state = 1028 + self.match(SqlBaseParser.CLEAR) + self.state = 1029 + self.match(SqlBaseParser.CACHE) + pass + + elif la_ == 64: + localctx = SqlBaseParser.LoadDataContext(self, localctx) + self.enterOuterAlt(localctx, 64) + self.state = 1030 + self.match(SqlBaseParser.LOAD) + self.state = 1031 + self.match(SqlBaseParser.DATA) + self.state = 1033 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==148: + self.state = 1032 + self.match(SqlBaseParser.LOCAL) + + + self.state = 1035 + self.match(SqlBaseParser.INPATH) + self.state = 1036 + localctx.path = self.stringLit() + self.state = 1038 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==189: + self.state = 1037 + self.match(SqlBaseParser.OVERWRITE) + + + self.state = 1040 + self.match(SqlBaseParser.INTO) + self.state = 1041 + self.match(SqlBaseParser.TABLE) + self.state = 1042 + self.multipartIdentifier() + self.state = 1044 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 1043 + self.partitionSpec() + + + pass + + elif la_ == 65: + localctx = SqlBaseParser.TruncateTableContext(self, localctx) + self.enterOuterAlt(localctx, 65) + self.state = 1046 + self.match(SqlBaseParser.TRUNCATE) + self.state = 1047 + self.match(SqlBaseParser.TABLE) + self.state = 1048 + self.multipartIdentifier() + self.state = 1050 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 1049 + self.partitionSpec() + + + pass + + elif la_ == 66: + localctx = SqlBaseParser.RepairTableContext(self, localctx) + self.enterOuterAlt(localctx, 66) + self.state = 1053 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==165: + self.state = 1052 + self.match(SqlBaseParser.MSCK) + + + self.state = 1055 + self.match(SqlBaseParser.REPAIR) + self.state = 1056 + self.match(SqlBaseParser.TABLE) + self.state = 1057 + self.multipartIdentifier() + self.state = 1060 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==8 or _la==82 or _la==255: + self.state = 1058 + localctx.option = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==8 or _la==82 or _la==255): + localctx.option = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 1059 + self.match(SqlBaseParser.PARTITIONS) + + + pass + + elif la_ == 67: + localctx = SqlBaseParser.ManageResourceContext(self, localctx) + self.enterOuterAlt(localctx, 67) + self.state = 1062 + localctx.op = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==8 or _la==146): + localctx.op = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 1063 + self.identifier() + self.state = 1067 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,106,self._ctx) + while _alt!=1 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1+1: + self.state = 1064 + self.matchWildcard() + self.state = 1069 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,106,self._ctx) + + pass + + elif la_ == 68: + localctx = SqlBaseParser.FailNativeCommandContext(self, localctx) + self.enterOuterAlt(localctx, 68) + self.state = 1070 + self.match(SqlBaseParser.SET) + self.state = 1071 + self.match(SqlBaseParser.ROLE) + self.state = 1075 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,107,self._ctx) + while _alt!=1 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1+1: + self.state = 1072 + self.matchWildcard() + self.state = 1077 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,107,self._ctx) + + pass + + elif la_ == 69: + localctx = SqlBaseParser.SetTimeZoneContext(self, localctx) + self.enterOuterAlt(localctx, 69) + self.state = 1078 + self.match(SqlBaseParser.SET) + self.state = 1079 + self.match(SqlBaseParser.TIME) + self.state = 1080 + self.match(SqlBaseParser.ZONE) + self.state = 1081 + self.interval() + pass + + elif la_ == 70: + localctx = SqlBaseParser.SetTimeZoneContext(self, localctx) + self.enterOuterAlt(localctx, 70) + self.state = 1082 + self.match(SqlBaseParser.SET) + self.state = 1083 + self.match(SqlBaseParser.TIME) + self.state = 1084 + self.match(SqlBaseParser.ZONE) + self.state = 1085 + self.timezone() + pass + + elif la_ == 71: + localctx = SqlBaseParser.SetTimeZoneContext(self, localctx) + self.enterOuterAlt(localctx, 71) + self.state = 1086 + self.match(SqlBaseParser.SET) + self.state = 1087 + self.match(SqlBaseParser.TIME) + self.state = 1088 + self.match(SqlBaseParser.ZONE) + self.state = 1092 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,108,self._ctx) + while _alt!=1 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1+1: + self.state = 1089 + self.matchWildcard() + self.state = 1094 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,108,self._ctx) + + pass + + elif la_ == 72: + localctx = SqlBaseParser.SetQuotedConfigurationContext(self, localctx) + self.enterOuterAlt(localctx, 72) + self.state = 1095 + self.match(SqlBaseParser.SET) + self.state = 1096 + self.configKey() + self.state = 1097 + self.match(SqlBaseParser.EQ) + self.state = 1098 + self.configValue() + pass + + elif la_ == 73: + localctx = SqlBaseParser.SetConfigurationContext(self, localctx) + self.enterOuterAlt(localctx, 73) + self.state = 1100 + self.match(SqlBaseParser.SET) + self.state = 1101 + self.configKey() + self.state = 1109 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==308: + self.state = 1102 + self.match(SqlBaseParser.EQ) + self.state = 1106 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,109,self._ctx) + while _alt!=1 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1+1: + self.state = 1103 + self.matchWildcard() + self.state = 1108 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,109,self._ctx) + + + + pass + + elif la_ == 74: + localctx = SqlBaseParser.SetQuotedConfigurationContext(self, localctx) + self.enterOuterAlt(localctx, 74) + self.state = 1111 + self.match(SqlBaseParser.SET) + self.state = 1115 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,111,self._ctx) + while _alt!=1 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1+1: + self.state = 1112 + self.matchWildcard() + self.state = 1117 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,111,self._ctx) + + self.state = 1118 + self.match(SqlBaseParser.EQ) + self.state = 1119 + self.configValue() + pass + + elif la_ == 75: + localctx = SqlBaseParser.SetConfigurationContext(self, localctx) + self.enterOuterAlt(localctx, 75) + self.state = 1120 + self.match(SqlBaseParser.SET) + self.state = 1124 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,112,self._ctx) + while _alt!=1 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1+1: + self.state = 1121 + self.matchWildcard() + self.state = 1126 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,112,self._ctx) + + pass + + elif la_ == 76: + localctx = SqlBaseParser.ResetQuotedConfigurationContext(self, localctx) + self.enterOuterAlt(localctx, 76) + self.state = 1127 + self.match(SqlBaseParser.RESET) + self.state = 1128 + self.configKey() + pass + + elif la_ == 77: + localctx = SqlBaseParser.ResetConfigurationContext(self, localctx) + self.enterOuterAlt(localctx, 77) + self.state = 1129 + self.match(SqlBaseParser.RESET) + self.state = 1133 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,113,self._ctx) + while _alt!=1 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1+1: + self.state = 1130 + self.matchWildcard() + self.state = 1135 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,113,self._ctx) + + pass + + elif la_ == 78: + localctx = SqlBaseParser.CreateIndexContext(self, localctx) + self.enterOuterAlt(localctx, 78) + self.state = 1136 + self.match(SqlBaseParser.CREATE) + self.state = 1137 + self.match(SqlBaseParser.INDEX) + self.state = 1141 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,114,self._ctx) + if la_ == 1: + self.state = 1138 + self.match(SqlBaseParser.IF) + self.state = 1139 + self.match(SqlBaseParser.NOT) + self.state = 1140 + self.match(SqlBaseParser.EXISTS) + + + self.state = 1143 + self.identifier() + self.state = 1144 + self.match(SqlBaseParser.ON) + self.state = 1146 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,115,self._ctx) + if la_ == 1: + self.state = 1145 + self.match(SqlBaseParser.TABLE) + + + self.state = 1148 + self.multipartIdentifier() + self.state = 1151 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==293: + self.state = 1149 + self.match(SqlBaseParser.USING) + self.state = 1150 + localctx.indexType = self.identifier() + + + self.state = 1153 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 1154 + localctx.columns = self.multipartIdentifierPropertyList() + self.state = 1155 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 1158 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==180: + self.state = 1156 + self.match(SqlBaseParser.OPTIONS) + self.state = 1157 + localctx.options = self.propertyList() + + + pass + + elif la_ == 79: + localctx = SqlBaseParser.DropIndexContext(self, localctx) + self.enterOuterAlt(localctx, 79) + self.state = 1160 + self.match(SqlBaseParser.DROP) + self.state = 1161 + self.match(SqlBaseParser.INDEX) + self.state = 1164 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,118,self._ctx) + if la_ == 1: + self.state = 1162 + self.match(SqlBaseParser.IF) + self.state = 1163 + self.match(SqlBaseParser.EXISTS) + + + self.state = 1166 + self.identifier() + self.state = 1167 + self.match(SqlBaseParser.ON) + self.state = 1169 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,119,self._ctx) + if la_ == 1: + self.state = 1168 + self.match(SqlBaseParser.TABLE) + + + self.state = 1171 + self.multipartIdentifier() + pass + + elif la_ == 80: + localctx = SqlBaseParser.FailNativeCommandContext(self, localctx) + self.enterOuterAlt(localctx, 80) + self.state = 1173 + self.unsupportedHiveNativeCommands() + self.state = 1177 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,120,self._ctx) + while _alt!=1 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1+1: + self.state = 1174 + self.matchWildcard() + self.state = 1179 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,120,self._ctx) + + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class TimezoneContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def LOCAL(self): + return self.getToken(SqlBaseParser.LOCAL, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_timezone + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTimezone" ): + listener.enterTimezone(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTimezone" ): + listener.exitTimezone(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTimezone" ): + return visitor.visitTimezone(self) + else: + return visitor.visitChildren(self) + + + + + def timezone(self): + + localctx = SqlBaseParser.TimezoneContext(self, self._ctx, self.state) + self.enterRule(localctx, 16, self.RULE_timezone) + try: + self.state = 1184 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [330, 331]: + self.enterOuterAlt(localctx, 1) + self.state = 1182 + self.stringLit() + pass + elif token in [148]: + self.enterOuterAlt(localctx, 2) + self.state = 1183 + self.match(SqlBaseParser.LOCAL) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ConfigKeyContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def quotedIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.QuotedIdentifierContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_configKey + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterConfigKey" ): + listener.enterConfigKey(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitConfigKey" ): + listener.exitConfigKey(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitConfigKey" ): + return visitor.visitConfigKey(self) + else: + return visitor.visitChildren(self) + + + + + def configKey(self): + + localctx = SqlBaseParser.ConfigKeyContext(self, self._ctx, self.state) + self.enterRule(localctx, 18, self.RULE_configKey) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1186 + self.quotedIdentifier() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ConfigValueContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def backQuotedIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.BackQuotedIdentifierContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_configValue + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterConfigValue" ): + listener.enterConfigValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitConfigValue" ): + listener.exitConfigValue(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitConfigValue" ): + return visitor.visitConfigValue(self) + else: + return visitor.visitChildren(self) + + + + + def configValue(self): + + localctx = SqlBaseParser.ConfigValueContext(self, self._ctx, self.state) + self.enterRule(localctx, 20, self.RULE_configValue) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1188 + self.backQuotedIdentifier() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnsupportedHiveNativeCommandsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.kw1 = None # Token + self.kw2 = None # Token + self.kw3 = None # Token + self.kw4 = None # Token + self.kw5 = None # Token + self.kw6 = None # Token + + def CREATE(self): + return self.getToken(SqlBaseParser.CREATE, 0) + + def ROLE(self): + return self.getToken(SqlBaseParser.ROLE, 0) + + def DROP(self): + return self.getToken(SqlBaseParser.DROP, 0) + + def GRANT(self): + return self.getToken(SqlBaseParser.GRANT, 0) + + def REVOKE(self): + return self.getToken(SqlBaseParser.REVOKE, 0) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + + def PRINCIPALS(self): + return self.getToken(SqlBaseParser.PRINCIPALS, 0) + + def ROLES(self): + return self.getToken(SqlBaseParser.ROLES, 0) + + def CURRENT(self): + return self.getToken(SqlBaseParser.CURRENT, 0) + + def EXPORT(self): + return self.getToken(SqlBaseParser.EXPORT, 0) + + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + + def IMPORT(self): + return self.getToken(SqlBaseParser.IMPORT, 0) + + def COMPACTIONS(self): + return self.getToken(SqlBaseParser.COMPACTIONS, 0) + + def TRANSACTIONS(self): + return self.getToken(SqlBaseParser.TRANSACTIONS, 0) + + def INDEXES(self): + return self.getToken(SqlBaseParser.INDEXES, 0) + + def LOCKS(self): + return self.getToken(SqlBaseParser.LOCKS, 0) + + def INDEX(self): + return self.getToken(SqlBaseParser.INDEX, 0) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + + def LOCK(self): + return self.getToken(SqlBaseParser.LOCK, 0) + + def DATABASE(self): + return self.getToken(SqlBaseParser.DATABASE, 0) + + def UNLOCK(self): + return self.getToken(SqlBaseParser.UNLOCK, 0) + + def TEMPORARY(self): + return self.getToken(SqlBaseParser.TEMPORARY, 0) + + def MACRO(self): + return self.getToken(SqlBaseParser.MACRO, 0) + + def tableIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.TableIdentifierContext,0) + + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + + def CLUSTERED(self): + return self.getToken(SqlBaseParser.CLUSTERED, 0) + + def BY(self): + return self.getToken(SqlBaseParser.BY, 0) + + def SORTED(self): + return self.getToken(SqlBaseParser.SORTED, 0) + + def SKEWED(self): + return self.getToken(SqlBaseParser.SKEWED, 0) + + def STORED(self): + return self.getToken(SqlBaseParser.STORED, 0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def DIRECTORIES(self): + return self.getToken(SqlBaseParser.DIRECTORIES, 0) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + + def LOCATION(self): + return self.getToken(SqlBaseParser.LOCATION, 0) + + def EXCHANGE(self): + return self.getToken(SqlBaseParser.EXCHANGE, 0) + + def PARTITION(self): + return self.getToken(SqlBaseParser.PARTITION, 0) + + def ARCHIVE(self): + return self.getToken(SqlBaseParser.ARCHIVE, 0) + + def UNARCHIVE(self): + return self.getToken(SqlBaseParser.UNARCHIVE, 0) + + def TOUCH(self): + return self.getToken(SqlBaseParser.TOUCH, 0) + + def COMPACT(self): + return self.getToken(SqlBaseParser.COMPACT, 0) + + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + + def CONCATENATE(self): + return self.getToken(SqlBaseParser.CONCATENATE, 0) + + def FILEFORMAT(self): + return self.getToken(SqlBaseParser.FILEFORMAT, 0) + + def REPLACE(self): + return self.getToken(SqlBaseParser.REPLACE, 0) + + def COLUMNS(self): + return self.getToken(SqlBaseParser.COLUMNS, 0) + + def START(self): + return self.getToken(SqlBaseParser.START, 0) + + def TRANSACTION(self): + return self.getToken(SqlBaseParser.TRANSACTION, 0) + + def COMMIT(self): + return self.getToken(SqlBaseParser.COMMIT, 0) + + def ROLLBACK(self): + return self.getToken(SqlBaseParser.ROLLBACK, 0) + + def DFS(self): + return self.getToken(SqlBaseParser.DFS, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_unsupportedHiveNativeCommands + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnsupportedHiveNativeCommands" ): + listener.enterUnsupportedHiveNativeCommands(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnsupportedHiveNativeCommands" ): + listener.exitUnsupportedHiveNativeCommands(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnsupportedHiveNativeCommands" ): + return visitor.visitUnsupportedHiveNativeCommands(self) + else: + return visitor.visitChildren(self) + + + + + def unsupportedHiveNativeCommands(self): + + localctx = SqlBaseParser.UnsupportedHiveNativeCommandsContext(self, self._ctx, self.state) + self.enterRule(localctx, 22, self.RULE_unsupportedHiveNativeCommands) + self._la = 0 # Token type + try: + self.state = 1358 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,130,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 1190 + localctx.kw1 = self.match(SqlBaseParser.CREATE) + self.state = 1191 + localctx.kw2 = self.match(SqlBaseParser.ROLE) + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 1192 + localctx.kw1 = self.match(SqlBaseParser.DROP) + self.state = 1193 + localctx.kw2 = self.match(SqlBaseParser.ROLE) + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 1194 + localctx.kw1 = self.match(SqlBaseParser.GRANT) + self.state = 1196 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,123,self._ctx) + if la_ == 1: + self.state = 1195 + localctx.kw2 = self.match(SqlBaseParser.ROLE) + + + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 1198 + localctx.kw1 = self.match(SqlBaseParser.REVOKE) + self.state = 1200 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,124,self._ctx) + if la_ == 1: + self.state = 1199 + localctx.kw2 = self.match(SqlBaseParser.ROLE) + + + pass + + elif la_ == 5: + self.enterOuterAlt(localctx, 5) + self.state = 1202 + localctx.kw1 = self.match(SqlBaseParser.SHOW) + self.state = 1203 + localctx.kw2 = self.match(SqlBaseParser.GRANT) + pass + + elif la_ == 6: + self.enterOuterAlt(localctx, 6) + self.state = 1204 + localctx.kw1 = self.match(SqlBaseParser.SHOW) + self.state = 1205 + localctx.kw2 = self.match(SqlBaseParser.ROLE) + self.state = 1207 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,125,self._ctx) + if la_ == 1: + self.state = 1206 + localctx.kw3 = self.match(SqlBaseParser.GRANT) + + + pass + + elif la_ == 7: + self.enterOuterAlt(localctx, 7) + self.state = 1209 + localctx.kw1 = self.match(SqlBaseParser.SHOW) + self.state = 1210 + localctx.kw2 = self.match(SqlBaseParser.PRINCIPALS) + pass + + elif la_ == 8: + self.enterOuterAlt(localctx, 8) + self.state = 1211 + localctx.kw1 = self.match(SqlBaseParser.SHOW) + self.state = 1212 + localctx.kw2 = self.match(SqlBaseParser.ROLES) + pass + + elif la_ == 9: + self.enterOuterAlt(localctx, 9) + self.state = 1213 + localctx.kw1 = self.match(SqlBaseParser.SHOW) + self.state = 1214 + localctx.kw2 = self.match(SqlBaseParser.CURRENT) + self.state = 1215 + localctx.kw3 = self.match(SqlBaseParser.ROLES) + pass + + elif la_ == 10: + self.enterOuterAlt(localctx, 10) + self.state = 1216 + localctx.kw1 = self.match(SqlBaseParser.EXPORT) + self.state = 1217 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + pass + + elif la_ == 11: + self.enterOuterAlt(localctx, 11) + self.state = 1218 + localctx.kw1 = self.match(SqlBaseParser.IMPORT) + self.state = 1219 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + pass + + elif la_ == 12: + self.enterOuterAlt(localctx, 12) + self.state = 1220 + localctx.kw1 = self.match(SqlBaseParser.SHOW) + self.state = 1221 + localctx.kw2 = self.match(SqlBaseParser.COMPACTIONS) + pass + + elif la_ == 13: + self.enterOuterAlt(localctx, 13) + self.state = 1222 + localctx.kw1 = self.match(SqlBaseParser.SHOW) + self.state = 1223 + localctx.kw2 = self.match(SqlBaseParser.CREATE) + self.state = 1224 + localctx.kw3 = self.match(SqlBaseParser.TABLE) + pass + + elif la_ == 14: + self.enterOuterAlt(localctx, 14) + self.state = 1225 + localctx.kw1 = self.match(SqlBaseParser.SHOW) + self.state = 1226 + localctx.kw2 = self.match(SqlBaseParser.TRANSACTIONS) + pass + + elif la_ == 15: + self.enterOuterAlt(localctx, 15) + self.state = 1227 + localctx.kw1 = self.match(SqlBaseParser.SHOW) + self.state = 1228 + localctx.kw2 = self.match(SqlBaseParser.INDEXES) + pass + + elif la_ == 16: + self.enterOuterAlt(localctx, 16) + self.state = 1229 + localctx.kw1 = self.match(SqlBaseParser.SHOW) + self.state = 1230 + localctx.kw2 = self.match(SqlBaseParser.LOCKS) + pass + + elif la_ == 17: + self.enterOuterAlt(localctx, 17) + self.state = 1231 + localctx.kw1 = self.match(SqlBaseParser.CREATE) + self.state = 1232 + localctx.kw2 = self.match(SqlBaseParser.INDEX) + pass + + elif la_ == 18: + self.enterOuterAlt(localctx, 18) + self.state = 1233 + localctx.kw1 = self.match(SqlBaseParser.DROP) + self.state = 1234 + localctx.kw2 = self.match(SqlBaseParser.INDEX) + pass + + elif la_ == 19: + self.enterOuterAlt(localctx, 19) + self.state = 1235 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1236 + localctx.kw2 = self.match(SqlBaseParser.INDEX) + pass + + elif la_ == 20: + self.enterOuterAlt(localctx, 20) + self.state = 1237 + localctx.kw1 = self.match(SqlBaseParser.LOCK) + self.state = 1238 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + pass + + elif la_ == 21: + self.enterOuterAlt(localctx, 21) + self.state = 1239 + localctx.kw1 = self.match(SqlBaseParser.LOCK) + self.state = 1240 + localctx.kw2 = self.match(SqlBaseParser.DATABASE) + pass + + elif la_ == 22: + self.enterOuterAlt(localctx, 22) + self.state = 1241 + localctx.kw1 = self.match(SqlBaseParser.UNLOCK) + self.state = 1242 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + pass + + elif la_ == 23: + self.enterOuterAlt(localctx, 23) + self.state = 1243 + localctx.kw1 = self.match(SqlBaseParser.UNLOCK) + self.state = 1244 + localctx.kw2 = self.match(SqlBaseParser.DATABASE) + pass + + elif la_ == 24: + self.enterOuterAlt(localctx, 24) + self.state = 1245 + localctx.kw1 = self.match(SqlBaseParser.CREATE) + self.state = 1246 + localctx.kw2 = self.match(SqlBaseParser.TEMPORARY) + self.state = 1247 + localctx.kw3 = self.match(SqlBaseParser.MACRO) + pass + + elif la_ == 25: + self.enterOuterAlt(localctx, 25) + self.state = 1248 + localctx.kw1 = self.match(SqlBaseParser.DROP) + self.state = 1249 + localctx.kw2 = self.match(SqlBaseParser.TEMPORARY) + self.state = 1250 + localctx.kw3 = self.match(SqlBaseParser.MACRO) + pass + + elif la_ == 26: + self.enterOuterAlt(localctx, 26) + self.state = 1251 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1252 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1253 + self.tableIdentifier() + self.state = 1254 + localctx.kw3 = self.match(SqlBaseParser.NOT) + self.state = 1255 + localctx.kw4 = self.match(SqlBaseParser.CLUSTERED) + pass + + elif la_ == 27: + self.enterOuterAlt(localctx, 27) + self.state = 1257 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1258 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1259 + self.tableIdentifier() + self.state = 1260 + localctx.kw3 = self.match(SqlBaseParser.CLUSTERED) + self.state = 1261 + localctx.kw4 = self.match(SqlBaseParser.BY) + pass + + elif la_ == 28: + self.enterOuterAlt(localctx, 28) + self.state = 1263 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1264 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1265 + self.tableIdentifier() + self.state = 1266 + localctx.kw3 = self.match(SqlBaseParser.NOT) + self.state = 1267 + localctx.kw4 = self.match(SqlBaseParser.SORTED) + pass + + elif la_ == 29: + self.enterOuterAlt(localctx, 29) + self.state = 1269 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1270 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1271 + self.tableIdentifier() + self.state = 1272 + localctx.kw3 = self.match(SqlBaseParser.SKEWED) + self.state = 1273 + localctx.kw4 = self.match(SqlBaseParser.BY) + pass + + elif la_ == 30: + self.enterOuterAlt(localctx, 30) + self.state = 1275 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1276 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1277 + self.tableIdentifier() + self.state = 1278 + localctx.kw3 = self.match(SqlBaseParser.NOT) + self.state = 1279 + localctx.kw4 = self.match(SqlBaseParser.SKEWED) + pass + + elif la_ == 31: + self.enterOuterAlt(localctx, 31) + self.state = 1281 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1282 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1283 + self.tableIdentifier() + self.state = 1284 + localctx.kw3 = self.match(SqlBaseParser.NOT) + self.state = 1285 + localctx.kw4 = self.match(SqlBaseParser.STORED) + self.state = 1286 + localctx.kw5 = self.match(SqlBaseParser.AS) + self.state = 1287 + localctx.kw6 = self.match(SqlBaseParser.DIRECTORIES) + pass + + elif la_ == 32: + self.enterOuterAlt(localctx, 32) + self.state = 1289 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1290 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1291 + self.tableIdentifier() + self.state = 1292 + localctx.kw3 = self.match(SqlBaseParser.SET) + self.state = 1293 + localctx.kw4 = self.match(SqlBaseParser.SKEWED) + self.state = 1294 + localctx.kw5 = self.match(SqlBaseParser.LOCATION) + pass + + elif la_ == 33: + self.enterOuterAlt(localctx, 33) + self.state = 1296 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1297 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1298 + self.tableIdentifier() + self.state = 1299 + localctx.kw3 = self.match(SqlBaseParser.EXCHANGE) + self.state = 1300 + localctx.kw4 = self.match(SqlBaseParser.PARTITION) + pass + + elif la_ == 34: + self.enterOuterAlt(localctx, 34) + self.state = 1302 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1303 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1304 + self.tableIdentifier() + self.state = 1305 + localctx.kw3 = self.match(SqlBaseParser.ARCHIVE) + self.state = 1306 + localctx.kw4 = self.match(SqlBaseParser.PARTITION) + pass + + elif la_ == 35: + self.enterOuterAlt(localctx, 35) + self.state = 1308 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1309 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1310 + self.tableIdentifier() + self.state = 1311 + localctx.kw3 = self.match(SqlBaseParser.UNARCHIVE) + self.state = 1312 + localctx.kw4 = self.match(SqlBaseParser.PARTITION) + pass + + elif la_ == 36: + self.enterOuterAlt(localctx, 36) + self.state = 1314 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1315 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1316 + self.tableIdentifier() + self.state = 1317 + localctx.kw3 = self.match(SqlBaseParser.TOUCH) + pass + + elif la_ == 37: + self.enterOuterAlt(localctx, 37) + self.state = 1319 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1320 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1321 + self.tableIdentifier() + self.state = 1323 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 1322 + self.partitionSpec() + + + self.state = 1325 + localctx.kw3 = self.match(SqlBaseParser.COMPACT) + pass + + elif la_ == 38: + self.enterOuterAlt(localctx, 38) + self.state = 1327 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1328 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1329 + self.tableIdentifier() + self.state = 1331 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 1330 + self.partitionSpec() + + + self.state = 1333 + localctx.kw3 = self.match(SqlBaseParser.CONCATENATE) + pass + + elif la_ == 39: + self.enterOuterAlt(localctx, 39) + self.state = 1335 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1336 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1337 + self.tableIdentifier() + self.state = 1339 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 1338 + self.partitionSpec() + + + self.state = 1341 + localctx.kw3 = self.match(SqlBaseParser.SET) + self.state = 1342 + localctx.kw4 = self.match(SqlBaseParser.FILEFORMAT) + pass + + elif la_ == 40: + self.enterOuterAlt(localctx, 40) + self.state = 1344 + localctx.kw1 = self.match(SqlBaseParser.ALTER) + self.state = 1345 + localctx.kw2 = self.match(SqlBaseParser.TABLE) + self.state = 1346 + self.tableIdentifier() + self.state = 1348 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 1347 + self.partitionSpec() + + + self.state = 1350 + localctx.kw3 = self.match(SqlBaseParser.REPLACE) + self.state = 1351 + localctx.kw4 = self.match(SqlBaseParser.COLUMNS) + pass + + elif la_ == 41: + self.enterOuterAlt(localctx, 41) + self.state = 1353 + localctx.kw1 = self.match(SqlBaseParser.START) + self.state = 1354 + localctx.kw2 = self.match(SqlBaseParser.TRANSACTION) + pass + + elif la_ == 42: + self.enterOuterAlt(localctx, 42) + self.state = 1355 + localctx.kw1 = self.match(SqlBaseParser.COMMIT) + pass + + elif la_ == 43: + self.enterOuterAlt(localctx, 43) + self.state = 1356 + localctx.kw1 = self.match(SqlBaseParser.ROLLBACK) + pass + + elif la_ == 44: + self.enterOuterAlt(localctx, 44) + self.state = 1357 + localctx.kw1 = self.match(SqlBaseParser.DFS) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class CreateTableHeaderContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def CREATE(self): + return self.getToken(SqlBaseParser.CREATE, 0) + + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def TEMPORARY(self): + return self.getToken(SqlBaseParser.TEMPORARY, 0) + + def EXTERNAL(self): + return self.getToken(SqlBaseParser.EXTERNAL, 0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_createTableHeader + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCreateTableHeader" ): + listener.enterCreateTableHeader(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCreateTableHeader" ): + listener.exitCreateTableHeader(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCreateTableHeader" ): + return visitor.visitCreateTableHeader(self) + else: + return visitor.visitChildren(self) + + + + + def createTableHeader(self): + + localctx = SqlBaseParser.CreateTableHeaderContext(self, self._ctx, self.state) + self.enterRule(localctx, 24, self.RULE_createTableHeader) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1360 + self.match(SqlBaseParser.CREATE) + self.state = 1362 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==263: + self.state = 1361 + self.match(SqlBaseParser.TEMPORARY) + + + self.state = 1365 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==94: + self.state = 1364 + self.match(SqlBaseParser.EXTERNAL) + + + self.state = 1367 + self.match(SqlBaseParser.TABLE) + self.state = 1371 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,133,self._ctx) + if la_ == 1: + self.state = 1368 + self.match(SqlBaseParser.IF) + self.state = 1369 + self.match(SqlBaseParser.NOT) + self.state = 1370 + self.match(SqlBaseParser.EXISTS) + + + self.state = 1373 + self.multipartIdentifier() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ReplaceTableHeaderContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def REPLACE(self): + return self.getToken(SqlBaseParser.REPLACE, 0) + + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def CREATE(self): + return self.getToken(SqlBaseParser.CREATE, 0) + + def OR(self): + return self.getToken(SqlBaseParser.OR, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_replaceTableHeader + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterReplaceTableHeader" ): + listener.enterReplaceTableHeader(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitReplaceTableHeader" ): + listener.exitReplaceTableHeader(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitReplaceTableHeader" ): + return visitor.visitReplaceTableHeader(self) + else: + return visitor.visitChildren(self) + + + + + def replaceTableHeader(self): + + localctx = SqlBaseParser.ReplaceTableHeaderContext(self, self._ctx, self.state) + self.enterRule(localctx, 26, self.RULE_replaceTableHeader) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1377 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==53: + self.state = 1375 + self.match(SqlBaseParser.CREATE) + self.state = 1376 + self.match(SqlBaseParser.OR) + + + self.state = 1379 + self.match(SqlBaseParser.REPLACE) + self.state = 1380 + self.match(SqlBaseParser.TABLE) + self.state = 1381 + self.multipartIdentifier() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class BucketSpecContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def CLUSTERED(self): + return self.getToken(SqlBaseParser.CLUSTERED, 0) + + def BY(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.BY) + else: + return self.getToken(SqlBaseParser.BY, i) + + def identifierList(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierListContext,0) + + + def INTO(self): + return self.getToken(SqlBaseParser.INTO, 0) + + def INTEGER_VALUE(self): + return self.getToken(SqlBaseParser.INTEGER_VALUE, 0) + + def BUCKETS(self): + return self.getToken(SqlBaseParser.BUCKETS, 0) + + def SORTED(self): + return self.getToken(SqlBaseParser.SORTED, 0) + + def orderedIdentifierList(self): + return self.getTypedRuleContext(SqlBaseParser.OrderedIdentifierListContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_bucketSpec + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterBucketSpec" ): + listener.enterBucketSpec(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitBucketSpec" ): + listener.exitBucketSpec(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitBucketSpec" ): + return visitor.visitBucketSpec(self) + else: + return visitor.visitChildren(self) + + + + + def bucketSpec(self): + + localctx = SqlBaseParser.BucketSpecContext(self, self._ctx, self.state) + self.enterRule(localctx, 28, self.RULE_bucketSpec) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1383 + self.match(SqlBaseParser.CLUSTERED) + self.state = 1384 + self.match(SqlBaseParser.BY) + self.state = 1385 + self.identifierList() + self.state = 1389 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==246: + self.state = 1386 + self.match(SqlBaseParser.SORTED) + self.state = 1387 + self.match(SqlBaseParser.BY) + self.state = 1388 + self.orderedIdentifierList() + + + self.state = 1391 + self.match(SqlBaseParser.INTO) + self.state = 1392 + self.match(SqlBaseParser.INTEGER_VALUE) + self.state = 1393 + self.match(SqlBaseParser.BUCKETS) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SkewSpecContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def SKEWED(self): + return self.getToken(SqlBaseParser.SKEWED, 0) + + def BY(self): + return self.getToken(SqlBaseParser.BY, 0) + + def identifierList(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierListContext,0) + + + def ON(self): + return self.getToken(SqlBaseParser.ON, 0) + + def constantList(self): + return self.getTypedRuleContext(SqlBaseParser.ConstantListContext,0) + + + def nestedConstantList(self): + return self.getTypedRuleContext(SqlBaseParser.NestedConstantListContext,0) + + + def STORED(self): + return self.getToken(SqlBaseParser.STORED, 0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def DIRECTORIES(self): + return self.getToken(SqlBaseParser.DIRECTORIES, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_skewSpec + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSkewSpec" ): + listener.enterSkewSpec(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSkewSpec" ): + listener.exitSkewSpec(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSkewSpec" ): + return visitor.visitSkewSpec(self) + else: + return visitor.visitChildren(self) + + + + + def skewSpec(self): + + localctx = SqlBaseParser.SkewSpecContext(self, self._ctx, self.state) + self.enterRule(localctx, 30, self.RULE_skewSpec) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1395 + self.match(SqlBaseParser.SKEWED) + self.state = 1396 + self.match(SqlBaseParser.BY) + self.state = 1397 + self.identifierList() + self.state = 1398 + self.match(SqlBaseParser.ON) + self.state = 1401 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,136,self._ctx) + if la_ == 1: + self.state = 1399 + self.constantList() + pass + + elif la_ == 2: + self.state = 1400 + self.nestedConstantList() + pass + + + self.state = 1406 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,137,self._ctx) + if la_ == 1: + self.state = 1403 + self.match(SqlBaseParser.STORED) + self.state = 1404 + self.match(SqlBaseParser.AS) + self.state = 1405 + self.match(SqlBaseParser.DIRECTORIES) + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class LocationSpecContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def LOCATION(self): + return self.getToken(SqlBaseParser.LOCATION, 0) + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_locationSpec + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterLocationSpec" ): + listener.enterLocationSpec(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitLocationSpec" ): + listener.exitLocationSpec(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitLocationSpec" ): + return visitor.visitLocationSpec(self) + else: + return visitor.visitChildren(self) + + + + + def locationSpec(self): + + localctx = SqlBaseParser.LocationSpecContext(self, self._ctx, self.state) + self.enterRule(localctx, 32, self.RULE_locationSpec) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1408 + self.match(SqlBaseParser.LOCATION) + self.state = 1409 + self.stringLit() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class CommentSpecContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def COMMENT(self): + return self.getToken(SqlBaseParser.COMMENT, 0) + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_commentSpec + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCommentSpec" ): + listener.enterCommentSpec(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCommentSpec" ): + listener.exitCommentSpec(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCommentSpec" ): + return visitor.visitCommentSpec(self) + else: + return visitor.visitChildren(self) + + + + + def commentSpec(self): + + localctx = SqlBaseParser.CommentSpecContext(self, self._ctx, self.state) + self.enterRule(localctx, 34, self.RULE_commentSpec) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1411 + self.match(SqlBaseParser.COMMENT) + self.state = 1412 + self.stringLit() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class InsertIntoContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_insertInto + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class InsertIntoReplaceWhereContext(InsertIntoContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.InsertIntoContext + super().__init__(parser) + self.copyFrom(ctx) + + def INSERT(self): + return self.getToken(SqlBaseParser.INSERT, 0) + def INTO(self): + return self.getToken(SqlBaseParser.INTO, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def REPLACE(self): + return self.getToken(SqlBaseParser.REPLACE, 0) + def whereClause(self): + return self.getTypedRuleContext(SqlBaseParser.WhereClauseContext,0) + + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInsertIntoReplaceWhere" ): + listener.enterInsertIntoReplaceWhere(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInsertIntoReplaceWhere" ): + listener.exitInsertIntoReplaceWhere(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitInsertIntoReplaceWhere" ): + return visitor.visitInsertIntoReplaceWhere(self) + else: + return visitor.visitChildren(self) + + + class InsertOverwriteHiveDirContext(InsertIntoContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.InsertIntoContext + super().__init__(parser) + self.path = None # StringLitContext + self.copyFrom(ctx) + + def INSERT(self): + return self.getToken(SqlBaseParser.INSERT, 0) + def OVERWRITE(self): + return self.getToken(SqlBaseParser.OVERWRITE, 0) + def DIRECTORY(self): + return self.getToken(SqlBaseParser.DIRECTORY, 0) + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + def LOCAL(self): + return self.getToken(SqlBaseParser.LOCAL, 0) + def rowFormat(self): + return self.getTypedRuleContext(SqlBaseParser.RowFormatContext,0) + + def createFileFormat(self): + return self.getTypedRuleContext(SqlBaseParser.CreateFileFormatContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInsertOverwriteHiveDir" ): + listener.enterInsertOverwriteHiveDir(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInsertOverwriteHiveDir" ): + listener.exitInsertOverwriteHiveDir(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitInsertOverwriteHiveDir" ): + return visitor.visitInsertOverwriteHiveDir(self) + else: + return visitor.visitChildren(self) + + + class InsertOverwriteDirContext(InsertIntoContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.InsertIntoContext + super().__init__(parser) + self.path = None # StringLitContext + self.options = None # PropertyListContext + self.copyFrom(ctx) + + def INSERT(self): + return self.getToken(SqlBaseParser.INSERT, 0) + def OVERWRITE(self): + return self.getToken(SqlBaseParser.OVERWRITE, 0) + def DIRECTORY(self): + return self.getToken(SqlBaseParser.DIRECTORY, 0) + def tableProvider(self): + return self.getTypedRuleContext(SqlBaseParser.TableProviderContext,0) + + def LOCAL(self): + return self.getToken(SqlBaseParser.LOCAL, 0) + def OPTIONS(self): + return self.getToken(SqlBaseParser.OPTIONS, 0) + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + def propertyList(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInsertOverwriteDir" ): + listener.enterInsertOverwriteDir(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInsertOverwriteDir" ): + listener.exitInsertOverwriteDir(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitInsertOverwriteDir" ): + return visitor.visitInsertOverwriteDir(self) + else: + return visitor.visitChildren(self) + + + class InsertOverwriteTableContext(InsertIntoContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.InsertIntoContext + super().__init__(parser) + self.copyFrom(ctx) + + def INSERT(self): + return self.getToken(SqlBaseParser.INSERT, 0) + def OVERWRITE(self): + return self.getToken(SqlBaseParser.OVERWRITE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + def identifierList(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierListContext,0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInsertOverwriteTable" ): + listener.enterInsertOverwriteTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInsertOverwriteTable" ): + listener.exitInsertOverwriteTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitInsertOverwriteTable" ): + return visitor.visitInsertOverwriteTable(self) + else: + return visitor.visitChildren(self) + + + class InsertIntoTableContext(InsertIntoContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.InsertIntoContext + super().__init__(parser) + self.copyFrom(ctx) + + def INSERT(self): + return self.getToken(SqlBaseParser.INSERT, 0) + def INTO(self): + return self.getToken(SqlBaseParser.INTO, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + def identifierList(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierListContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInsertIntoTable" ): + listener.enterInsertIntoTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInsertIntoTable" ): + listener.exitInsertIntoTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitInsertIntoTable" ): + return visitor.visitInsertIntoTable(self) + else: + return visitor.visitChildren(self) + + + + def insertInto(self): + + localctx = SqlBaseParser.InsertIntoContext(self, self._ctx, self.state) + self.enterRule(localctx, 36, self.RULE_insertInto) + self._la = 0 # Token type + try: + self.state = 1484 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,153,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.InsertOverwriteTableContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 1414 + self.match(SqlBaseParser.INSERT) + self.state = 1415 + self.match(SqlBaseParser.OVERWRITE) + self.state = 1417 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,138,self._ctx) + if la_ == 1: + self.state = 1416 + self.match(SqlBaseParser.TABLE) + + + self.state = 1419 + self.multipartIdentifier() + self.state = 1426 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 1420 + self.partitionSpec() + self.state = 1424 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==119: + self.state = 1421 + self.match(SqlBaseParser.IF) + self.state = 1422 + self.match(SqlBaseParser.NOT) + self.state = 1423 + self.match(SqlBaseParser.EXISTS) + + + + + self.state = 1429 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,141,self._ctx) + if la_ == 1: + self.state = 1428 + self.identifierList() + + + pass + + elif la_ == 2: + localctx = SqlBaseParser.InsertIntoTableContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 1431 + self.match(SqlBaseParser.INSERT) + self.state = 1432 + self.match(SqlBaseParser.INTO) + self.state = 1434 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,142,self._ctx) + if la_ == 1: + self.state = 1433 + self.match(SqlBaseParser.TABLE) + + + self.state = 1436 + self.multipartIdentifier() + self.state = 1438 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==190: + self.state = 1437 + self.partitionSpec() + + + self.state = 1443 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==119: + self.state = 1440 + self.match(SqlBaseParser.IF) + self.state = 1441 + self.match(SqlBaseParser.NOT) + self.state = 1442 + self.match(SqlBaseParser.EXISTS) + + + self.state = 1446 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,145,self._ctx) + if la_ == 1: + self.state = 1445 + self.identifierList() + + + pass + + elif la_ == 3: + localctx = SqlBaseParser.InsertIntoReplaceWhereContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 1448 + self.match(SqlBaseParser.INSERT) + self.state = 1449 + self.match(SqlBaseParser.INTO) + self.state = 1451 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,146,self._ctx) + if la_ == 1: + self.state = 1450 + self.match(SqlBaseParser.TABLE) + + + self.state = 1453 + self.multipartIdentifier() + self.state = 1454 + self.match(SqlBaseParser.REPLACE) + self.state = 1455 + self.whereClause() + pass + + elif la_ == 4: + localctx = SqlBaseParser.InsertOverwriteHiveDirContext(self, localctx) + self.enterOuterAlt(localctx, 4) + self.state = 1457 + self.match(SqlBaseParser.INSERT) + self.state = 1458 + self.match(SqlBaseParser.OVERWRITE) + self.state = 1460 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==148: + self.state = 1459 + self.match(SqlBaseParser.LOCAL) + + + self.state = 1462 + self.match(SqlBaseParser.DIRECTORY) + self.state = 1463 + localctx.path = self.stringLit() + self.state = 1465 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==227: + self.state = 1464 + self.rowFormat() + + + self.state = 1468 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==250: + self.state = 1467 + self.createFileFormat() + + + pass + + elif la_ == 5: + localctx = SqlBaseParser.InsertOverwriteDirContext(self, localctx) + self.enterOuterAlt(localctx, 5) + self.state = 1470 + self.match(SqlBaseParser.INSERT) + self.state = 1471 + self.match(SqlBaseParser.OVERWRITE) + self.state = 1473 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==148: + self.state = 1472 + self.match(SqlBaseParser.LOCAL) + + + self.state = 1475 + self.match(SqlBaseParser.DIRECTORY) + self.state = 1477 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==330 or _la==331: + self.state = 1476 + localctx.path = self.stringLit() + + + self.state = 1479 + self.tableProvider() + self.state = 1482 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==180: + self.state = 1480 + self.match(SqlBaseParser.OPTIONS) + self.state = 1481 + localctx.options = self.propertyList() + + + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PartitionSpecLocationContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def partitionSpec(self): + return self.getTypedRuleContext(SqlBaseParser.PartitionSpecContext,0) + + + def locationSpec(self): + return self.getTypedRuleContext(SqlBaseParser.LocationSpecContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_partitionSpecLocation + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPartitionSpecLocation" ): + listener.enterPartitionSpecLocation(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPartitionSpecLocation" ): + listener.exitPartitionSpecLocation(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPartitionSpecLocation" ): + return visitor.visitPartitionSpecLocation(self) + else: + return visitor.visitChildren(self) + + + + + def partitionSpecLocation(self): + + localctx = SqlBaseParser.PartitionSpecLocationContext(self, self._ctx, self.state) + self.enterRule(localctx, 38, self.RULE_partitionSpecLocation) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1486 + self.partitionSpec() + self.state = 1488 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==149: + self.state = 1487 + self.locationSpec() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PartitionSpecContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def PARTITION(self): + return self.getToken(SqlBaseParser.PARTITION, 0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def partitionVal(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PartitionValContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PartitionValContext,i) + + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_partitionSpec + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPartitionSpec" ): + listener.enterPartitionSpec(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPartitionSpec" ): + listener.exitPartitionSpec(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPartitionSpec" ): + return visitor.visitPartitionSpec(self) + else: + return visitor.visitChildren(self) + + + + + def partitionSpec(self): + + localctx = SqlBaseParser.PartitionSpecContext(self, self._ctx, self.state) + self.enterRule(localctx, 40, self.RULE_partitionSpec) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1490 + self.match(SqlBaseParser.PARTITION) + self.state = 1491 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 1492 + self.partitionVal() + self.state = 1497 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 1493 + self.match(SqlBaseParser.COMMA) + self.state = 1494 + self.partitionVal() + self.state = 1499 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 1500 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PartitionValContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def EQ(self): + return self.getToken(SqlBaseParser.EQ, 0) + + def constant(self): + return self.getTypedRuleContext(SqlBaseParser.ConstantContext,0) + + + def DEFAULT(self): + return self.getToken(SqlBaseParser.DEFAULT, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_partitionVal + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPartitionVal" ): + listener.enterPartitionVal(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPartitionVal" ): + listener.exitPartitionVal(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPartitionVal" ): + return visitor.visitPartitionVal(self) + else: + return visitor.visitChildren(self) + + + + + def partitionVal(self): + + localctx = SqlBaseParser.PartitionValContext(self, self._ctx, self.state) + self.enterRule(localctx, 42, self.RULE_partitionVal) + self._la = 0 # Token type + try: + self.state = 1511 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,157,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 1502 + self.identifier() + self.state = 1505 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==308: + self.state = 1503 + self.match(SqlBaseParser.EQ) + self.state = 1504 + self.constant() + + + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 1507 + self.identifier() + self.state = 1508 + self.match(SqlBaseParser.EQ) + self.state = 1509 + self.match(SqlBaseParser.DEFAULT) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NamespaceContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def NAMESPACE(self): + return self.getToken(SqlBaseParser.NAMESPACE, 0) + + def DATABASE(self): + return self.getToken(SqlBaseParser.DATABASE, 0) + + def SCHEMA(self): + return self.getToken(SqlBaseParser.SCHEMA, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_namespace + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNamespace" ): + listener.enterNamespace(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNamespace" ): + listener.exitNamespace(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNamespace" ): + return visitor.visitNamespace(self) + else: + return visitor.visitChildren(self) + + + + + def namespace(self): + + localctx = SqlBaseParser.NamespaceContext(self, self._ctx, self.state) + self.enterRule(localctx, 44, self.RULE_namespace) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1513 + _la = self._input.LA(1) + if not(_la==65 or _la==166 or _la==231): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NamespacesContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def NAMESPACES(self): + return self.getToken(SqlBaseParser.NAMESPACES, 0) + + def DATABASES(self): + return self.getToken(SqlBaseParser.DATABASES, 0) + + def SCHEMAS(self): + return self.getToken(SqlBaseParser.SCHEMAS, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_namespaces + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNamespaces" ): + listener.enterNamespaces(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNamespaces" ): + listener.exitNamespaces(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNamespaces" ): + return visitor.visitNamespaces(self) + else: + return visitor.visitChildren(self) + + + + + def namespaces(self): + + localctx = SqlBaseParser.NamespacesContext(self, self._ctx, self.state) + self.enterRule(localctx, 46, self.RULE_namespaces) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1515 + _la = self._input.LA(1) + if not(_la==66 or _la==167 or _la==232): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class DescribeFuncNameContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def qualifiedName(self): + return self.getTypedRuleContext(SqlBaseParser.QualifiedNameContext,0) + + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def comparisonOperator(self): + return self.getTypedRuleContext(SqlBaseParser.ComparisonOperatorContext,0) + + + def arithmeticOperator(self): + return self.getTypedRuleContext(SqlBaseParser.ArithmeticOperatorContext,0) + + + def predicateOperator(self): + return self.getTypedRuleContext(SqlBaseParser.PredicateOperatorContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_describeFuncName + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDescribeFuncName" ): + listener.enterDescribeFuncName(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDescribeFuncName" ): + listener.exitDescribeFuncName(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDescribeFuncName" ): + return visitor.visitDescribeFuncName(self) + else: + return visitor.visitChildren(self) + + + + + def describeFuncName(self): + + localctx = SqlBaseParser.DescribeFuncNameContext(self, self._ctx, self.state) + self.enterRule(localctx, 48, self.RULE_describeFuncName) + try: + self.state = 1522 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,158,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 1517 + self.qualifiedName() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 1518 + self.stringLit() + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 1519 + self.comparisonOperator() + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 1520 + self.arithmeticOperator() + pass + + elif la_ == 5: + self.enterOuterAlt(localctx, 5) + self.state = 1521 + self.predicateOperator() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class DescribeColNameContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._identifier = None # IdentifierContext + self.nameParts = list() # of IdentifierContexts + + def identifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.IdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,i) + + + def DOT(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.DOT) + else: + return self.getToken(SqlBaseParser.DOT, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_describeColName + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDescribeColName" ): + listener.enterDescribeColName(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDescribeColName" ): + listener.exitDescribeColName(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDescribeColName" ): + return visitor.visitDescribeColName(self) + else: + return visitor.visitChildren(self) + + + + + def describeColName(self): + + localctx = SqlBaseParser.DescribeColNameContext(self, self._ctx, self.state) + self.enterRule(localctx, 50, self.RULE_describeColName) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1524 + localctx._identifier = self.identifier() + localctx.nameParts.append(localctx._identifier) + self.state = 1529 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==5: + self.state = 1525 + self.match(SqlBaseParser.DOT) + self.state = 1526 + localctx._identifier = self.identifier() + localctx.nameParts.append(localctx._identifier) + self.state = 1531 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class CtesContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def WITH(self): + return self.getToken(SqlBaseParser.WITH, 0) + + def namedQuery(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.NamedQueryContext) + else: + return self.getTypedRuleContext(SqlBaseParser.NamedQueryContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_ctes + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCtes" ): + listener.enterCtes(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCtes" ): + listener.exitCtes(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCtes" ): + return visitor.visitCtes(self) + else: + return visitor.visitChildren(self) + + + + + def ctes(self): + + localctx = SqlBaseParser.CtesContext(self, self._ctx, self.state) + self.enterRule(localctx, 52, self.RULE_ctes) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1532 + self.match(SqlBaseParser.WITH) + self.state = 1533 + self.namedQuery() + self.state = 1538 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 1534 + self.match(SqlBaseParser.COMMA) + self.state = 1535 + self.namedQuery() + self.state = 1540 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class QueryContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def queryTerm(self): + return self.getTypedRuleContext(SqlBaseParser.QueryTermContext,0) + + + def queryOrganization(self): + return self.getTypedRuleContext(SqlBaseParser.QueryOrganizationContext,0) + + + def ctes(self): + return self.getTypedRuleContext(SqlBaseParser.CtesContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_query + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterQuery" ): + listener.enterQuery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitQuery" ): + listener.exitQuery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitQuery" ): + return visitor.visitQuery(self) + else: + return visitor.visitChildren(self) + + + + + def query(self): + + localctx = SqlBaseParser.QueryContext(self, self._ctx, self.state) + self.enterRule(localctx, 54, self.RULE_query) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1542 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==303: + self.state = 1541 + self.ctes() + + + self.state = 1544 + self.queryTerm(0) + self.state = 1545 + self.queryOrganization() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NamedQueryContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.name = None # ErrorCapturingIdentifierContext + self.columnAliases = None # IdentifierListContext + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def errorCapturingIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,0) + + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def identifierList(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierListContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_namedQuery + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNamedQuery" ): + listener.enterNamedQuery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNamedQuery" ): + listener.exitNamedQuery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNamedQuery" ): + return visitor.visitNamedQuery(self) + else: + return visitor.visitChildren(self) + + + + + def namedQuery(self): + + localctx = SqlBaseParser.NamedQueryContext(self, self._ctx, self.state) + self.enterRule(localctx, 56, self.RULE_namedQuery) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1547 + localctx.name = self.errorCapturingIdentifier() + self.state = 1549 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,162,self._ctx) + if la_ == 1: + self.state = 1548 + localctx.columnAliases = self.identifierList() + + + self.state = 1552 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==20: + self.state = 1551 + self.match(SqlBaseParser.AS) + + + self.state = 1554 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 1555 + self.query() + self.state = 1556 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class QueryTermContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_queryTerm + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + class QueryTermDefaultContext(QueryTermContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.QueryTermContext + super().__init__(parser) + self.copyFrom(ctx) + + def queryPrimary(self): + return self.getTypedRuleContext(SqlBaseParser.QueryPrimaryContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterQueryTermDefault" ): + listener.enterQueryTermDefault(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitQueryTermDefault" ): + listener.exitQueryTermDefault(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitQueryTermDefault" ): + return visitor.visitQueryTermDefault(self) + else: + return visitor.visitChildren(self) + + + class SetOperationContext(QueryTermContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.QueryTermContext + super().__init__(parser) + self.left = None # QueryTermContext + self.operator = None # Token + self.right = None # QueryTermContext + self.copyFrom(ctx) + + def queryTerm(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.QueryTermContext) + else: + return self.getTypedRuleContext(SqlBaseParser.QueryTermContext,i) + + def INTERSECT(self): + return self.getToken(SqlBaseParser.INTERSECT, 0) + def UNION(self): + return self.getToken(SqlBaseParser.UNION, 0) + def EXCEPT(self): + return self.getToken(SqlBaseParser.EXCEPT, 0) + def SETMINUS(self): + return self.getToken(SqlBaseParser.SETMINUS, 0) + def setQuantifier(self): + return self.getTypedRuleContext(SqlBaseParser.SetQuantifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSetOperation" ): + listener.enterSetOperation(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSetOperation" ): + listener.exitSetOperation(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSetOperation" ): + return visitor.visitSetOperation(self) + else: + return visitor.visitChildren(self) + + + + def queryTerm(self, _p:int=0): + _parentctx = self._ctx + _parentState = self.state + localctx = SqlBaseParser.QueryTermContext(self, self._ctx, _parentState) + _prevctx = localctx + _startState = 58 + self.enterRecursionRule(localctx, 58, self.RULE_queryTerm, _p) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + localctx = SqlBaseParser.QueryTermDefaultContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + + self.state = 1559 + self.queryPrimary() + self._ctx.stop = self._input.LT(-1) + self.state = 1569 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,165,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + if self._parseListeners is not None: + self.triggerExitRuleEvent() + _prevctx = localctx + localctx = SqlBaseParser.SetOperationContext(self, SqlBaseParser.QueryTermContext(self, _parentctx, _parentState)) + localctx.left = _prevctx + self.pushNewRecursionContext(localctx, _startState, self.RULE_queryTerm) + self.state = 1561 + if not self.precpred(self._ctx, 1): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 1)") + self.state = 1562 + localctx.operator = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==87 or _la==130 or _la==240 or _la==284): + localctx.operator = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 1564 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==10 or _la==79: + self.state = 1563 + self.setQuantifier() + + + self.state = 1566 + localctx.right = self.queryTerm(2) + self.state = 1571 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,165,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.unrollRecursionContexts(_parentctx) + return localctx + + + class QuerySpecificationContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_querySpecification + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class RegularQuerySpecificationContext(QuerySpecificationContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.QuerySpecificationContext + super().__init__(parser) + self.copyFrom(ctx) + + def selectClause(self): + return self.getTypedRuleContext(SqlBaseParser.SelectClauseContext,0) + + def fromClause(self): + return self.getTypedRuleContext(SqlBaseParser.FromClauseContext,0) + + def lateralView(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.LateralViewContext) + else: + return self.getTypedRuleContext(SqlBaseParser.LateralViewContext,i) + + def whereClause(self): + return self.getTypedRuleContext(SqlBaseParser.WhereClauseContext,0) + + def aggregationClause(self): + return self.getTypedRuleContext(SqlBaseParser.AggregationClauseContext,0) + + def havingClause(self): + return self.getTypedRuleContext(SqlBaseParser.HavingClauseContext,0) + + def windowClause(self): + return self.getTypedRuleContext(SqlBaseParser.WindowClauseContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRegularQuerySpecification" ): + listener.enterRegularQuerySpecification(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRegularQuerySpecification" ): + listener.exitRegularQuerySpecification(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRegularQuerySpecification" ): + return visitor.visitRegularQuerySpecification(self) + else: + return visitor.visitChildren(self) + + + class TransformQuerySpecificationContext(QuerySpecificationContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.QuerySpecificationContext + super().__init__(parser) + self.copyFrom(ctx) + + def transformClause(self): + return self.getTypedRuleContext(SqlBaseParser.TransformClauseContext,0) + + def fromClause(self): + return self.getTypedRuleContext(SqlBaseParser.FromClauseContext,0) + + def lateralView(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.LateralViewContext) + else: + return self.getTypedRuleContext(SqlBaseParser.LateralViewContext,i) + + def whereClause(self): + return self.getTypedRuleContext(SqlBaseParser.WhereClauseContext,0) + + def aggregationClause(self): + return self.getTypedRuleContext(SqlBaseParser.AggregationClauseContext,0) + + def havingClause(self): + return self.getTypedRuleContext(SqlBaseParser.HavingClauseContext,0) + + def windowClause(self): + return self.getTypedRuleContext(SqlBaseParser.WindowClauseContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTransformQuerySpecification" ): + listener.enterTransformQuerySpecification(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTransformQuerySpecification" ): + listener.exitTransformQuerySpecification(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTransformQuerySpecification" ): + return visitor.visitTransformQuerySpecification(self) + else: + return visitor.visitChildren(self) + + + + def querySpecification(self): + + localctx = SqlBaseParser.QuerySpecificationContext(self, self._ctx, self.state) + self.enterRule(localctx, 60, self.RULE_querySpecification) + try: + self.state = 1616 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,178,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.TransformQuerySpecificationContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 1572 + self.transformClause() + self.state = 1574 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,166,self._ctx) + if la_ == 1: + self.state = 1573 + self.fromClause() + + + self.state = 1579 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,167,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1576 + self.lateralView() + self.state = 1581 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,167,self._ctx) + + self.state = 1583 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,168,self._ctx) + if la_ == 1: + self.state = 1582 + self.whereClause() + + + self.state = 1586 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,169,self._ctx) + if la_ == 1: + self.state = 1585 + self.aggregationClause() + + + self.state = 1589 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,170,self._ctx) + if la_ == 1: + self.state = 1588 + self.havingClause() + + + self.state = 1592 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,171,self._ctx) + if la_ == 1: + self.state = 1591 + self.windowClause() + + + pass + + elif la_ == 2: + localctx = SqlBaseParser.RegularQuerySpecificationContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 1594 + self.selectClause() + self.state = 1596 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,172,self._ctx) + if la_ == 1: + self.state = 1595 + self.fromClause() + + + self.state = 1601 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,173,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1598 + self.lateralView() + self.state = 1603 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,173,self._ctx) + + self.state = 1605 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,174,self._ctx) + if la_ == 1: + self.state = 1604 + self.whereClause() + + + self.state = 1608 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,175,self._ctx) + if la_ == 1: + self.state = 1607 + self.aggregationClause() + + + self.state = 1611 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,176,self._ctx) + if la_ == 1: + self.state = 1610 + self.havingClause() + + + self.state = 1614 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,177,self._ctx) + if la_ == 1: + self.state = 1613 + self.windowClause() + + + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class QueryPrimaryContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_queryPrimary + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class SubqueryContext(QueryPrimaryContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.QueryPrimaryContext + super().__init__(parser) + self.copyFrom(ctx) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSubquery" ): + listener.enterSubquery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSubquery" ): + listener.exitSubquery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSubquery" ): + return visitor.visitSubquery(self) + else: + return visitor.visitChildren(self) + + + class QueryPrimaryDefaultContext(QueryPrimaryContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.QueryPrimaryContext + super().__init__(parser) + self.copyFrom(ctx) + + def querySpecification(self): + return self.getTypedRuleContext(SqlBaseParser.QuerySpecificationContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterQueryPrimaryDefault" ): + listener.enterQueryPrimaryDefault(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitQueryPrimaryDefault" ): + listener.exitQueryPrimaryDefault(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitQueryPrimaryDefault" ): + return visitor.visitQueryPrimaryDefault(self) + else: + return visitor.visitChildren(self) + + + class InlineTableDefault1Context(QueryPrimaryContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.QueryPrimaryContext + super().__init__(parser) + self.copyFrom(ctx) + + def inlineTable(self): + return self.getTypedRuleContext(SqlBaseParser.InlineTableContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInlineTableDefault1" ): + listener.enterInlineTableDefault1(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInlineTableDefault1" ): + listener.exitInlineTableDefault1(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitInlineTableDefault1" ): + return visitor.visitInlineTableDefault1(self) + else: + return visitor.visitChildren(self) + + + class FromStmtContext(QueryPrimaryContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.QueryPrimaryContext + super().__init__(parser) + self.copyFrom(ctx) + + def fromStatement(self): + return self.getTypedRuleContext(SqlBaseParser.FromStatementContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFromStmt" ): + listener.enterFromStmt(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFromStmt" ): + listener.exitFromStmt(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFromStmt" ): + return visitor.visitFromStmt(self) + else: + return visitor.visitChildren(self) + + + class TableContext(QueryPrimaryContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.QueryPrimaryContext + super().__init__(parser) + self.copyFrom(ctx) + + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTable" ): + listener.enterTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTable" ): + listener.exitTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTable" ): + return visitor.visitTable(self) + else: + return visitor.visitChildren(self) + + + + def queryPrimary(self): + + localctx = SqlBaseParser.QueryPrimaryContext(self, self._ctx, self.state) + self.enterRule(localctx, 62, self.RULE_queryPrimary) + try: + self.state = 1627 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [154, 210, 233]: + localctx = SqlBaseParser.QueryPrimaryDefaultContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 1618 + self.querySpecification() + pass + elif token in [107]: + localctx = SqlBaseParser.FromStmtContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 1619 + self.fromStatement() + pass + elif token in [258]: + localctx = SqlBaseParser.TableContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 1620 + self.match(SqlBaseParser.TABLE) + self.state = 1621 + self.multipartIdentifier() + pass + elif token in [294]: + localctx = SqlBaseParser.InlineTableDefault1Context(self, localctx) + self.enterOuterAlt(localctx, 4) + self.state = 1622 + self.inlineTable() + pass + elif token in [2]: + localctx = SqlBaseParser.SubqueryContext(self, localctx) + self.enterOuterAlt(localctx, 5) + self.state = 1623 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 1624 + self.query() + self.state = 1625 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class TableProviderContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def USING(self): + return self.getToken(SqlBaseParser.USING, 0) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_tableProvider + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTableProvider" ): + listener.enterTableProvider(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTableProvider" ): + listener.exitTableProvider(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTableProvider" ): + return visitor.visitTableProvider(self) + else: + return visitor.visitChildren(self) + + + + + def tableProvider(self): + + localctx = SqlBaseParser.TableProviderContext(self, self._ctx, self.state) + self.enterRule(localctx, 64, self.RULE_tableProvider) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1629 + self.match(SqlBaseParser.USING) + self.state = 1630 + self.multipartIdentifier() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class CreateTableClausesContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.options = None # PropertyListContext + self.partitioning = None # PartitionFieldListContext + self.tableProps = None # PropertyListContext + + def skewSpec(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.SkewSpecContext) + else: + return self.getTypedRuleContext(SqlBaseParser.SkewSpecContext,i) + + + def bucketSpec(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.BucketSpecContext) + else: + return self.getTypedRuleContext(SqlBaseParser.BucketSpecContext,i) + + + def rowFormat(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.RowFormatContext) + else: + return self.getTypedRuleContext(SqlBaseParser.RowFormatContext,i) + + + def createFileFormat(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.CreateFileFormatContext) + else: + return self.getTypedRuleContext(SqlBaseParser.CreateFileFormatContext,i) + + + def locationSpec(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.LocationSpecContext) + else: + return self.getTypedRuleContext(SqlBaseParser.LocationSpecContext,i) + + + def commentSpec(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.CommentSpecContext) + else: + return self.getTypedRuleContext(SqlBaseParser.CommentSpecContext,i) + + + def OPTIONS(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.OPTIONS) + else: + return self.getToken(SqlBaseParser.OPTIONS, i) + + def PARTITIONED(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.PARTITIONED) + else: + return self.getToken(SqlBaseParser.PARTITIONED, i) + + def BY(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.BY) + else: + return self.getToken(SqlBaseParser.BY, i) + + def TBLPROPERTIES(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.TBLPROPERTIES) + else: + return self.getToken(SqlBaseParser.TBLPROPERTIES, i) + + def propertyList(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PropertyListContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,i) + + + def partitionFieldList(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PartitionFieldListContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PartitionFieldListContext,i) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_createTableClauses + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCreateTableClauses" ): + listener.enterCreateTableClauses(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCreateTableClauses" ): + listener.exitCreateTableClauses(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCreateTableClauses" ): + return visitor.visitCreateTableClauses(self) + else: + return visitor.visitChildren(self) + + + + + def createTableClauses(self): + + localctx = SqlBaseParser.CreateTableClausesContext(self, self._ctx, self.state) + self.enterRule(localctx, 66, self.RULE_createTableClauses) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1647 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==39 or _la==45 or ((((_la - 149)) & ~0x3f) == 0 and ((1 << (_la - 149)) & 4400193994753) != 0) or ((((_la - 227)) & ~0x3f) == 0 and ((1 << (_la - 227)) & 34368192513) != 0): + self.state = 1645 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [180]: + self.state = 1632 + self.match(SqlBaseParser.OPTIONS) + self.state = 1633 + localctx.options = self.propertyList() + pass + elif token in [191]: + self.state = 1634 + self.match(SqlBaseParser.PARTITIONED) + self.state = 1635 + self.match(SqlBaseParser.BY) + self.state = 1636 + localctx.partitioning = self.partitionFieldList() + pass + elif token in [243]: + self.state = 1637 + self.skewSpec() + pass + elif token in [39]: + self.state = 1638 + self.bucketSpec() + pass + elif token in [227]: + self.state = 1639 + self.rowFormat() + pass + elif token in [250]: + self.state = 1640 + self.createFileFormat() + pass + elif token in [149]: + self.state = 1641 + self.locationSpec() + pass + elif token in [45]: + self.state = 1642 + self.commentSpec() + pass + elif token in [262]: + self.state = 1643 + self.match(SqlBaseParser.TBLPROPERTIES) + self.state = 1644 + localctx.tableProps = self.propertyList() + pass + else: + raise NoViableAltException(self) + + self.state = 1649 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PropertyListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def property_(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PropertyContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PropertyContext,i) + + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_propertyList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPropertyList" ): + listener.enterPropertyList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPropertyList" ): + listener.exitPropertyList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPropertyList" ): + return visitor.visitPropertyList(self) + else: + return visitor.visitChildren(self) + + + + + def propertyList(self): + + localctx = SqlBaseParser.PropertyListContext(self, self._ctx, self.state) + self.enterRule(localctx, 68, self.RULE_propertyList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1650 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 1651 + self.property_() + self.state = 1656 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 1652 + self.match(SqlBaseParser.COMMA) + self.state = 1653 + self.property_() + self.state = 1658 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 1659 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PropertyContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.key = None # PropertyKeyContext + self.value = None # PropertyValueContext + + def propertyKey(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyKeyContext,0) + + + def propertyValue(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyValueContext,0) + + + def EQ(self): + return self.getToken(SqlBaseParser.EQ, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_property + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterProperty" ): + listener.enterProperty(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitProperty" ): + listener.exitProperty(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitProperty" ): + return visitor.visitProperty(self) + else: + return visitor.visitChildren(self) + + + + + def property_(self): + + localctx = SqlBaseParser.PropertyContext(self, self._ctx, self.state) + self.enterRule(localctx, 70, self.RULE_property) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1661 + localctx.key = self.propertyKey() + self.state = 1666 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==96 or ((((_la - 277)) & ~0x3f) == 0 and ((1 << (_la - 277)) & 1468173480670265345) != 0): + self.state = 1663 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==308: + self.state = 1662 + self.match(SqlBaseParser.EQ) + + + self.state = 1665 + localctx.value = self.propertyValue() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PropertyKeyContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def identifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.IdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,i) + + + def DOT(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.DOT) + else: + return self.getToken(SqlBaseParser.DOT, i) + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_propertyKey + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPropertyKey" ): + listener.enterPropertyKey(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPropertyKey" ): + listener.exitPropertyKey(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPropertyKey" ): + return visitor.visitPropertyKey(self) + else: + return visitor.visitChildren(self) + + + + + def propertyKey(self): + + localctx = SqlBaseParser.PropertyKeyContext(self, self._ctx, self.state) + self.enterRule(localctx, 72, self.RULE_propertyKey) + self._la = 0 # Token type + try: + self.state = 1677 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,186,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 1668 + self.identifier() + self.state = 1673 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==5: + self.state = 1669 + self.match(SqlBaseParser.DOT) + self.state = 1670 + self.identifier() + self.state = 1675 + self._errHandler.sync(self) + _la = self._input.LA(1) + + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 1676 + self.stringLit() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PropertyValueContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def INTEGER_VALUE(self): + return self.getToken(SqlBaseParser.INTEGER_VALUE, 0) + + def DECIMAL_VALUE(self): + return self.getToken(SqlBaseParser.DECIMAL_VALUE, 0) + + def booleanValue(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanValueContext,0) + + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_propertyValue + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPropertyValue" ): + listener.enterPropertyValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPropertyValue" ): + listener.exitPropertyValue(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPropertyValue" ): + return visitor.visitPropertyValue(self) + else: + return visitor.visitChildren(self) + + + + + def propertyValue(self): + + localctx = SqlBaseParser.PropertyValueContext(self, self._ctx, self.state) + self.enterRule(localctx, 74, self.RULE_propertyValue) + try: + self.state = 1683 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [335]: + self.enterOuterAlt(localctx, 1) + self.state = 1679 + self.match(SqlBaseParser.INTEGER_VALUE) + pass + elif token in [337]: + self.enterOuterAlt(localctx, 2) + self.state = 1680 + self.match(SqlBaseParser.DECIMAL_VALUE) + pass + elif token in [96, 277]: + self.enterOuterAlt(localctx, 3) + self.state = 1681 + self.booleanValue() + pass + elif token in [330, 331]: + self.enterOuterAlt(localctx, 4) + self.state = 1682 + self.stringLit() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ConstantListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def constant(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ConstantContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ConstantContext,i) + + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_constantList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterConstantList" ): + listener.enterConstantList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitConstantList" ): + listener.exitConstantList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitConstantList" ): + return visitor.visitConstantList(self) + else: + return visitor.visitChildren(self) + + + + + def constantList(self): + + localctx = SqlBaseParser.ConstantListContext(self, self._ctx, self.state) + self.enterRule(localctx, 76, self.RULE_constantList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1685 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 1686 + self.constant() + self.state = 1691 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 1687 + self.match(SqlBaseParser.COMMA) + self.state = 1688 + self.constant() + self.state = 1693 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 1694 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NestedConstantListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def constantList(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ConstantListContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ConstantListContext,i) + + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_nestedConstantList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNestedConstantList" ): + listener.enterNestedConstantList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNestedConstantList" ): + listener.exitNestedConstantList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNestedConstantList" ): + return visitor.visitNestedConstantList(self) + else: + return visitor.visitChildren(self) + + + + + def nestedConstantList(self): + + localctx = SqlBaseParser.NestedConstantListContext(self, self._ctx, self.state) + self.enterRule(localctx, 78, self.RULE_nestedConstantList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1696 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 1697 + self.constantList() + self.state = 1702 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 1698 + self.match(SqlBaseParser.COMMA) + self.state = 1699 + self.constantList() + self.state = 1704 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 1705 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class CreateFileFormatContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def STORED(self): + return self.getToken(SqlBaseParser.STORED, 0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def fileFormat(self): + return self.getTypedRuleContext(SqlBaseParser.FileFormatContext,0) + + + def BY(self): + return self.getToken(SqlBaseParser.BY, 0) + + def storageHandler(self): + return self.getTypedRuleContext(SqlBaseParser.StorageHandlerContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_createFileFormat + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCreateFileFormat" ): + listener.enterCreateFileFormat(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCreateFileFormat" ): + listener.exitCreateFileFormat(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCreateFileFormat" ): + return visitor.visitCreateFileFormat(self) + else: + return visitor.visitChildren(self) + + + + + def createFileFormat(self): + + localctx = SqlBaseParser.CreateFileFormatContext(self, self._ctx, self.state) + self.enterRule(localctx, 80, self.RULE_createFileFormat) + try: + self.state = 1713 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,190,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 1707 + self.match(SqlBaseParser.STORED) + self.state = 1708 + self.match(SqlBaseParser.AS) + self.state = 1709 + self.fileFormat() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 1710 + self.match(SqlBaseParser.STORED) + self.state = 1711 + self.match(SqlBaseParser.BY) + self.state = 1712 + self.storageHandler() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FileFormatContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_fileFormat + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class TableFileFormatContext(FileFormatContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.FileFormatContext + super().__init__(parser) + self.inFmt = None # StringLitContext + self.outFmt = None # StringLitContext + self.copyFrom(ctx) + + def INPUTFORMAT(self): + return self.getToken(SqlBaseParser.INPUTFORMAT, 0) + def OUTPUTFORMAT(self): + return self.getToken(SqlBaseParser.OUTPUTFORMAT, 0) + def stringLit(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.StringLitContext) + else: + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTableFileFormat" ): + listener.enterTableFileFormat(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTableFileFormat" ): + listener.exitTableFileFormat(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTableFileFormat" ): + return visitor.visitTableFileFormat(self) + else: + return visitor.visitChildren(self) + + + class GenericFileFormatContext(FileFormatContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.FileFormatContext + super().__init__(parser) + self.copyFrom(ctx) + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterGenericFileFormat" ): + listener.enterGenericFileFormat(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitGenericFileFormat" ): + listener.exitGenericFileFormat(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitGenericFileFormat" ): + return visitor.visitGenericFileFormat(self) + else: + return visitor.visitChildren(self) + + + + def fileFormat(self): + + localctx = SqlBaseParser.FileFormatContext(self, self._ctx, self.state) + self.enterRule(localctx, 82, self.RULE_fileFormat) + try: + self.state = 1721 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,191,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.TableFileFormatContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 1715 + self.match(SqlBaseParser.INPUTFORMAT) + self.state = 1716 + localctx.inFmt = self.stringLit() + self.state = 1717 + self.match(SqlBaseParser.OUTPUTFORMAT) + self.state = 1718 + localctx.outFmt = self.stringLit() + pass + + elif la_ == 2: + localctx = SqlBaseParser.GenericFileFormatContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 1720 + self.identifier() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class StorageHandlerContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def WITH(self): + return self.getToken(SqlBaseParser.WITH, 0) + + def SERDEPROPERTIES(self): + return self.getToken(SqlBaseParser.SERDEPROPERTIES, 0) + + def propertyList(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_storageHandler + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterStorageHandler" ): + listener.enterStorageHandler(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitStorageHandler" ): + listener.exitStorageHandler(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitStorageHandler" ): + return visitor.visitStorageHandler(self) + else: + return visitor.visitChildren(self) + + + + + def storageHandler(self): + + localctx = SqlBaseParser.StorageHandlerContext(self, self._ctx, self.state) + self.enterRule(localctx, 84, self.RULE_storageHandler) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1723 + self.stringLit() + self.state = 1727 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,192,self._ctx) + if la_ == 1: + self.state = 1724 + self.match(SqlBaseParser.WITH) + self.state = 1725 + self.match(SqlBaseParser.SERDEPROPERTIES) + self.state = 1726 + self.propertyList() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ResourceContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_resource + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterResource" ): + listener.enterResource(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitResource" ): + listener.exitResource(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitResource" ): + return visitor.visitResource(self) + else: + return visitor.visitChildren(self) + + + + + def resource(self): + + localctx = SqlBaseParser.ResourceContext(self, self._ctx, self.state) + self.enterRule(localctx, 86, self.RULE_resource) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1729 + self.identifier() + self.state = 1730 + self.stringLit() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class DmlStatementNoWithContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_dmlStatementNoWith + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class DeleteFromTableContext(DmlStatementNoWithContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.DmlStatementNoWithContext + super().__init__(parser) + self.copyFrom(ctx) + + def DELETE(self): + return self.getToken(SqlBaseParser.DELETE, 0) + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def tableAlias(self): + return self.getTypedRuleContext(SqlBaseParser.TableAliasContext,0) + + def whereClause(self): + return self.getTypedRuleContext(SqlBaseParser.WhereClauseContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDeleteFromTable" ): + listener.enterDeleteFromTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDeleteFromTable" ): + listener.exitDeleteFromTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDeleteFromTable" ): + return visitor.visitDeleteFromTable(self) + else: + return visitor.visitChildren(self) + + + class SingleInsertQueryContext(DmlStatementNoWithContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.DmlStatementNoWithContext + super().__init__(parser) + self.copyFrom(ctx) + + def insertInto(self): + return self.getTypedRuleContext(SqlBaseParser.InsertIntoContext,0) + + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSingleInsertQuery" ): + listener.enterSingleInsertQuery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSingleInsertQuery" ): + listener.exitSingleInsertQuery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSingleInsertQuery" ): + return visitor.visitSingleInsertQuery(self) + else: + return visitor.visitChildren(self) + + + class MultiInsertQueryContext(DmlStatementNoWithContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.DmlStatementNoWithContext + super().__init__(parser) + self.copyFrom(ctx) + + def fromClause(self): + return self.getTypedRuleContext(SqlBaseParser.FromClauseContext,0) + + def multiInsertQueryBody(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.MultiInsertQueryBodyContext) + else: + return self.getTypedRuleContext(SqlBaseParser.MultiInsertQueryBodyContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMultiInsertQuery" ): + listener.enterMultiInsertQuery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMultiInsertQuery" ): + listener.exitMultiInsertQuery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMultiInsertQuery" ): + return visitor.visitMultiInsertQuery(self) + else: + return visitor.visitChildren(self) + + + class UpdateTableContext(DmlStatementNoWithContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.DmlStatementNoWithContext + super().__init__(parser) + self.copyFrom(ctx) + + def UPDATE(self): + return self.getToken(SqlBaseParser.UPDATE, 0) + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def tableAlias(self): + return self.getTypedRuleContext(SqlBaseParser.TableAliasContext,0) + + def setClause(self): + return self.getTypedRuleContext(SqlBaseParser.SetClauseContext,0) + + def whereClause(self): + return self.getTypedRuleContext(SqlBaseParser.WhereClauseContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUpdateTable" ): + listener.enterUpdateTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUpdateTable" ): + listener.exitUpdateTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUpdateTable" ): + return visitor.visitUpdateTable(self) + else: + return visitor.visitChildren(self) + + + class MergeIntoTableContext(DmlStatementNoWithContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.DmlStatementNoWithContext + super().__init__(parser) + self.target = None # MultipartIdentifierContext + self.targetAlias = None # TableAliasContext + self.source = None # MultipartIdentifierContext + self.sourceQuery = None # QueryContext + self.sourceAlias = None # TableAliasContext + self.mergeCondition = None # BooleanExpressionContext + self.copyFrom(ctx) + + def MERGE(self): + return self.getToken(SqlBaseParser.MERGE, 0) + def INTO(self): + return self.getToken(SqlBaseParser.INTO, 0) + def USING(self): + return self.getToken(SqlBaseParser.USING, 0) + def ON(self): + return self.getToken(SqlBaseParser.ON, 0) + def multipartIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.MultipartIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,i) + + def tableAlias(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.TableAliasContext) + else: + return self.getTypedRuleContext(SqlBaseParser.TableAliasContext,i) + + def booleanExpression(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanExpressionContext,0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + def matchedClause(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.MatchedClauseContext) + else: + return self.getTypedRuleContext(SqlBaseParser.MatchedClauseContext,i) + + def notMatchedClause(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.NotMatchedClauseContext) + else: + return self.getTypedRuleContext(SqlBaseParser.NotMatchedClauseContext,i) + + def notMatchedBySourceClause(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.NotMatchedBySourceClauseContext) + else: + return self.getTypedRuleContext(SqlBaseParser.NotMatchedBySourceClauseContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMergeIntoTable" ): + listener.enterMergeIntoTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMergeIntoTable" ): + listener.exitMergeIntoTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMergeIntoTable" ): + return visitor.visitMergeIntoTable(self) + else: + return visitor.visitChildren(self) + + + + def dmlStatementNoWith(self): + + localctx = SqlBaseParser.DmlStatementNoWithContext(self, self._ctx, self.state) + self.enterRule(localctx, 88, self.RULE_dmlStatementNoWith) + self._la = 0 # Token type + try: + self.state = 1788 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [129]: + localctx = SqlBaseParser.SingleInsertQueryContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 1732 + self.insertInto() + self.state = 1733 + self.query() + pass + elif token in [107]: + localctx = SqlBaseParser.MultiInsertQueryContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 1735 + self.fromClause() + self.state = 1737 + self._errHandler.sync(self) + _la = self._input.LA(1) + while True: + self.state = 1736 + self.multiInsertQueryBody() + self.state = 1739 + self._errHandler.sync(self) + _la = self._input.LA(1) + if not (_la==129): + break + + pass + elif token in [72]: + localctx = SqlBaseParser.DeleteFromTableContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 1741 + self.match(SqlBaseParser.DELETE) + self.state = 1742 + self.match(SqlBaseParser.FROM) + self.state = 1743 + self.multipartIdentifier() + self.state = 1744 + self.tableAlias() + self.state = 1746 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==301: + self.state = 1745 + self.whereClause() + + + pass + elif token in [290]: + localctx = SqlBaseParser.UpdateTableContext(self, localctx) + self.enterOuterAlt(localctx, 4) + self.state = 1748 + self.match(SqlBaseParser.UPDATE) + self.state = 1749 + self.multipartIdentifier() + self.state = 1750 + self.tableAlias() + self.state = 1751 + self.setClause() + self.state = 1753 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==301: + self.state = 1752 + self.whereClause() + + + pass + elif token in [156]: + localctx = SqlBaseParser.MergeIntoTableContext(self, localctx) + self.enterOuterAlt(localctx, 5) + self.state = 1755 + self.match(SqlBaseParser.MERGE) + self.state = 1756 + self.match(SqlBaseParser.INTO) + self.state = 1757 + localctx.target = self.multipartIdentifier() + self.state = 1758 + localctx.targetAlias = self.tableAlias() + self.state = 1759 + self.match(SqlBaseParser.USING) + self.state = 1765 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 331, 341, 342]: + self.state = 1760 + localctx.source = self.multipartIdentifier() + pass + elif token in [2]: + self.state = 1761 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 1762 + localctx.sourceQuery = self.query() + self.state = 1763 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + else: + raise NoViableAltException(self) + + self.state = 1767 + localctx.sourceAlias = self.tableAlias() + self.state = 1768 + self.match(SqlBaseParser.ON) + self.state = 1769 + localctx.mergeCondition = self.booleanExpression(0) + self.state = 1773 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,197,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1770 + self.matchedClause() + self.state = 1775 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,197,self._ctx) + + self.state = 1779 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,198,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1776 + self.notMatchedClause() + self.state = 1781 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,198,self._ctx) + + self.state = 1785 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==300: + self.state = 1782 + self.notMatchedBySourceClause() + self.state = 1787 + self._errHandler.sync(self) + _la = self._input.LA(1) + + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class QueryOrganizationContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._sortItem = None # SortItemContext + self.order = list() # of SortItemContexts + self._expression = None # ExpressionContext + self.clusterBy = list() # of ExpressionContexts + self.distributeBy = list() # of ExpressionContexts + self.sort = list() # of SortItemContexts + self.limit = None # ExpressionContext + self.offset = None # ExpressionContext + + def ORDER(self): + return self.getToken(SqlBaseParser.ORDER, 0) + + def BY(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.BY) + else: + return self.getToken(SqlBaseParser.BY, i) + + def CLUSTER(self): + return self.getToken(SqlBaseParser.CLUSTER, 0) + + def DISTRIBUTE(self): + return self.getToken(SqlBaseParser.DISTRIBUTE, 0) + + def SORT(self): + return self.getToken(SqlBaseParser.SORT, 0) + + def windowClause(self): + return self.getTypedRuleContext(SqlBaseParser.WindowClauseContext,0) + + + def LIMIT(self): + return self.getToken(SqlBaseParser.LIMIT, 0) + + def OFFSET(self): + return self.getToken(SqlBaseParser.OFFSET, 0) + + def sortItem(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.SortItemContext) + else: + return self.getTypedRuleContext(SqlBaseParser.SortItemContext,i) + + + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + + def ALL(self): + return self.getToken(SqlBaseParser.ALL, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_queryOrganization + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterQueryOrganization" ): + listener.enterQueryOrganization(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitQueryOrganization" ): + listener.exitQueryOrganization(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitQueryOrganization" ): + return visitor.visitQueryOrganization(self) + else: + return visitor.visitChildren(self) + + + + + def queryOrganization(self): + + localctx = SqlBaseParser.QueryOrganizationContext(self, self._ctx, self.state) + self.enterRule(localctx, 90, self.RULE_queryOrganization) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1800 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,202,self._ctx) + if la_ == 1: + self.state = 1790 + self.match(SqlBaseParser.ORDER) + self.state = 1791 + self.match(SqlBaseParser.BY) + self.state = 1792 + localctx._sortItem = self.sortItem() + localctx.order.append(localctx._sortItem) + self.state = 1797 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,201,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1793 + self.match(SqlBaseParser.COMMA) + self.state = 1794 + localctx._sortItem = self.sortItem() + localctx.order.append(localctx._sortItem) + self.state = 1799 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,201,self._ctx) + + + + self.state = 1812 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,204,self._ctx) + if la_ == 1: + self.state = 1802 + self.match(SqlBaseParser.CLUSTER) + self.state = 1803 + self.match(SqlBaseParser.BY) + self.state = 1804 + localctx._expression = self.expression() + localctx.clusterBy.append(localctx._expression) + self.state = 1809 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,203,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1805 + self.match(SqlBaseParser.COMMA) + self.state = 1806 + localctx._expression = self.expression() + localctx.clusterBy.append(localctx._expression) + self.state = 1811 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,203,self._ctx) + + + + self.state = 1824 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,206,self._ctx) + if la_ == 1: + self.state = 1814 + self.match(SqlBaseParser.DISTRIBUTE) + self.state = 1815 + self.match(SqlBaseParser.BY) + self.state = 1816 + localctx._expression = self.expression() + localctx.distributeBy.append(localctx._expression) + self.state = 1821 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,205,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1817 + self.match(SqlBaseParser.COMMA) + self.state = 1818 + localctx._expression = self.expression() + localctx.distributeBy.append(localctx._expression) + self.state = 1823 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,205,self._ctx) + + + + self.state = 1836 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,208,self._ctx) + if la_ == 1: + self.state = 1826 + self.match(SqlBaseParser.SORT) + self.state = 1827 + self.match(SqlBaseParser.BY) + self.state = 1828 + localctx._sortItem = self.sortItem() + localctx.sort.append(localctx._sortItem) + self.state = 1833 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,207,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1829 + self.match(SqlBaseParser.COMMA) + self.state = 1830 + localctx._sortItem = self.sortItem() + localctx.sort.append(localctx._sortItem) + self.state = 1835 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,207,self._ctx) + + + + self.state = 1839 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,209,self._ctx) + if la_ == 1: + self.state = 1838 + self.windowClause() + + + self.state = 1846 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,211,self._ctx) + if la_ == 1: + self.state = 1841 + self.match(SqlBaseParser.LIMIT) + self.state = 1844 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,210,self._ctx) + if la_ == 1: + self.state = 1842 + self.match(SqlBaseParser.ALL) + pass + + elif la_ == 2: + self.state = 1843 + localctx.limit = self.expression() + pass + + + + + self.state = 1850 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,212,self._ctx) + if la_ == 1: + self.state = 1848 + self.match(SqlBaseParser.OFFSET) + self.state = 1849 + localctx.offset = self.expression() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MultiInsertQueryBodyContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def insertInto(self): + return self.getTypedRuleContext(SqlBaseParser.InsertIntoContext,0) + + + def fromStatementBody(self): + return self.getTypedRuleContext(SqlBaseParser.FromStatementBodyContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_multiInsertQueryBody + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMultiInsertQueryBody" ): + listener.enterMultiInsertQueryBody(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMultiInsertQueryBody" ): + listener.exitMultiInsertQueryBody(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMultiInsertQueryBody" ): + return visitor.visitMultiInsertQueryBody(self) + else: + return visitor.visitChildren(self) + + + + + def multiInsertQueryBody(self): + + localctx = SqlBaseParser.MultiInsertQueryBodyContext(self, self._ctx, self.state) + self.enterRule(localctx, 92, self.RULE_multiInsertQueryBody) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1852 + self.insertInto() + self.state = 1853 + self.fromStatementBody() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SortItemContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.ordering = None # Token + self.nullOrder = None # Token + + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + + def NULLS(self): + return self.getToken(SqlBaseParser.NULLS, 0) + + def ASC(self): + return self.getToken(SqlBaseParser.ASC, 0) + + def DESC(self): + return self.getToken(SqlBaseParser.DESC, 0) + + def LAST(self): + return self.getToken(SqlBaseParser.LAST, 0) + + def FIRST(self): + return self.getToken(SqlBaseParser.FIRST, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_sortItem + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSortItem" ): + listener.enterSortItem(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSortItem" ): + listener.exitSortItem(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSortItem" ): + return visitor.visitSortItem(self) + else: + return visitor.visitChildren(self) + + + + + def sortItem(self): + + localctx = SqlBaseParser.SortItemContext(self, self._ctx, self.state) + self.enterRule(localctx, 94, self.RULE_sortItem) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1855 + self.expression() + self.state = 1857 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,213,self._ctx) + if la_ == 1: + self.state = 1856 + localctx.ordering = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==21 or _la==74): + localctx.ordering = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + self.state = 1861 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,214,self._ctx) + if la_ == 1: + self.state = 1859 + self.match(SqlBaseParser.NULLS) + self.state = 1860 + localctx.nullOrder = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==101 or _la==137): + localctx.nullOrder = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FromStatementContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def fromClause(self): + return self.getTypedRuleContext(SqlBaseParser.FromClauseContext,0) + + + def fromStatementBody(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.FromStatementBodyContext) + else: + return self.getTypedRuleContext(SqlBaseParser.FromStatementBodyContext,i) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_fromStatement + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFromStatement" ): + listener.enterFromStatement(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFromStatement" ): + listener.exitFromStatement(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFromStatement" ): + return visitor.visitFromStatement(self) + else: + return visitor.visitChildren(self) + + + + + def fromStatement(self): + + localctx = SqlBaseParser.FromStatementContext(self, self._ctx, self.state) + self.enterRule(localctx, 96, self.RULE_fromStatement) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1863 + self.fromClause() + self.state = 1865 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 1864 + self.fromStatementBody() + + else: + raise NoViableAltException(self) + self.state = 1867 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,215,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FromStatementBodyContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def transformClause(self): + return self.getTypedRuleContext(SqlBaseParser.TransformClauseContext,0) + + + def queryOrganization(self): + return self.getTypedRuleContext(SqlBaseParser.QueryOrganizationContext,0) + + + def whereClause(self): + return self.getTypedRuleContext(SqlBaseParser.WhereClauseContext,0) + + + def selectClause(self): + return self.getTypedRuleContext(SqlBaseParser.SelectClauseContext,0) + + + def lateralView(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.LateralViewContext) + else: + return self.getTypedRuleContext(SqlBaseParser.LateralViewContext,i) + + + def aggregationClause(self): + return self.getTypedRuleContext(SqlBaseParser.AggregationClauseContext,0) + + + def havingClause(self): + return self.getTypedRuleContext(SqlBaseParser.HavingClauseContext,0) + + + def windowClause(self): + return self.getTypedRuleContext(SqlBaseParser.WindowClauseContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_fromStatementBody + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFromStatementBody" ): + listener.enterFromStatementBody(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFromStatementBody" ): + listener.exitFromStatementBody(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFromStatementBody" ): + return visitor.visitFromStatementBody(self) + else: + return visitor.visitChildren(self) + + + + + def fromStatementBody(self): + + localctx = SqlBaseParser.FromStatementBodyContext(self, self._ctx, self.state) + self.enterRule(localctx, 98, self.RULE_fromStatementBody) + try: + self.state = 1896 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,222,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 1869 + self.transformClause() + self.state = 1871 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,216,self._ctx) + if la_ == 1: + self.state = 1870 + self.whereClause() + + + self.state = 1873 + self.queryOrganization() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 1875 + self.selectClause() + self.state = 1879 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,217,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 1876 + self.lateralView() + self.state = 1881 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,217,self._ctx) + + self.state = 1883 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,218,self._ctx) + if la_ == 1: + self.state = 1882 + self.whereClause() + + + self.state = 1886 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,219,self._ctx) + if la_ == 1: + self.state = 1885 + self.aggregationClause() + + + self.state = 1889 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,220,self._ctx) + if la_ == 1: + self.state = 1888 + self.havingClause() + + + self.state = 1892 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,221,self._ctx) + if la_ == 1: + self.state = 1891 + self.windowClause() + + + self.state = 1894 + self.queryOrganization() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class TransformClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.kind = None # Token + self.inRowFormat = None # RowFormatContext + self.recordWriter = None # StringLitContext + self.script = None # StringLitContext + self.outRowFormat = None # RowFormatContext + self.recordReader = None # StringLitContext + + def USING(self): + return self.getToken(SqlBaseParser.USING, 0) + + def stringLit(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.StringLitContext) + else: + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,i) + + + def SELECT(self): + return self.getToken(SqlBaseParser.SELECT, 0) + + def LEFT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.LEFT_PAREN) + else: + return self.getToken(SqlBaseParser.LEFT_PAREN, i) + + def expressionSeq(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionSeqContext,0) + + + def RIGHT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.RIGHT_PAREN) + else: + return self.getToken(SqlBaseParser.RIGHT_PAREN, i) + + def TRANSFORM(self): + return self.getToken(SqlBaseParser.TRANSFORM, 0) + + def MAP(self): + return self.getToken(SqlBaseParser.MAP, 0) + + def REDUCE(self): + return self.getToken(SqlBaseParser.REDUCE, 0) + + def RECORDWRITER(self): + return self.getToken(SqlBaseParser.RECORDWRITER, 0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def RECORDREADER(self): + return self.getToken(SqlBaseParser.RECORDREADER, 0) + + def rowFormat(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.RowFormatContext) + else: + return self.getTypedRuleContext(SqlBaseParser.RowFormatContext,i) + + + def setQuantifier(self): + return self.getTypedRuleContext(SqlBaseParser.SetQuantifierContext,0) + + + def identifierSeq(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierSeqContext,0) + + + def colTypeList(self): + return self.getTypedRuleContext(SqlBaseParser.ColTypeListContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_transformClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTransformClause" ): + listener.enterTransformClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTransformClause" ): + listener.exitTransformClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTransformClause" ): + return visitor.visitTransformClause(self) + else: + return visitor.visitChildren(self) + + + + + def transformClause(self): + + localctx = SqlBaseParser.TransformClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 100, self.RULE_transformClause) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1917 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [233]: + self.state = 1898 + self.match(SqlBaseParser.SELECT) + self.state = 1899 + localctx.kind = self.match(SqlBaseParser.TRANSFORM) + self.state = 1900 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 1902 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,223,self._ctx) + if la_ == 1: + self.state = 1901 + self.setQuantifier() + + + self.state = 1904 + self.expressionSeq() + self.state = 1905 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + elif token in [154]: + self.state = 1907 + localctx.kind = self.match(SqlBaseParser.MAP) + self.state = 1909 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,224,self._ctx) + if la_ == 1: + self.state = 1908 + self.setQuantifier() + + + self.state = 1911 + self.expressionSeq() + pass + elif token in [210]: + self.state = 1912 + localctx.kind = self.match(SqlBaseParser.REDUCE) + self.state = 1914 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,225,self._ctx) + if la_ == 1: + self.state = 1913 + self.setQuantifier() + + + self.state = 1916 + self.expressionSeq() + pass + else: + raise NoViableAltException(self) + + self.state = 1920 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==227: + self.state = 1919 + localctx.inRowFormat = self.rowFormat() + + + self.state = 1924 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==208: + self.state = 1922 + self.match(SqlBaseParser.RECORDWRITER) + self.state = 1923 + localctx.recordWriter = self.stringLit() + + + self.state = 1926 + self.match(SqlBaseParser.USING) + self.state = 1927 + localctx.script = self.stringLit() + self.state = 1940 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,231,self._ctx) + if la_ == 1: + self.state = 1928 + self.match(SqlBaseParser.AS) + self.state = 1938 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,230,self._ctx) + if la_ == 1: + self.state = 1929 + self.identifierSeq() + pass + + elif la_ == 2: + self.state = 1930 + self.colTypeList() + pass + + elif la_ == 3: + self.state = 1931 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 1934 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,229,self._ctx) + if la_ == 1: + self.state = 1932 + self.identifierSeq() + pass + + elif la_ == 2: + self.state = 1933 + self.colTypeList() + pass + + + self.state = 1936 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + + + + self.state = 1943 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,232,self._ctx) + if la_ == 1: + self.state = 1942 + localctx.outRowFormat = self.rowFormat() + + + self.state = 1947 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,233,self._ctx) + if la_ == 1: + self.state = 1945 + self.match(SqlBaseParser.RECORDREADER) + self.state = 1946 + localctx.recordReader = self.stringLit() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SelectClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._hint = None # HintContext + self.hints = list() # of HintContexts + + def SELECT(self): + return self.getToken(SqlBaseParser.SELECT, 0) + + def namedExpressionSeq(self): + return self.getTypedRuleContext(SqlBaseParser.NamedExpressionSeqContext,0) + + + def setQuantifier(self): + return self.getTypedRuleContext(SqlBaseParser.SetQuantifierContext,0) + + + def hint(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.HintContext) + else: + return self.getTypedRuleContext(SqlBaseParser.HintContext,i) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_selectClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSelectClause" ): + listener.enterSelectClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSelectClause" ): + listener.exitSelectClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSelectClause" ): + return visitor.visitSelectClause(self) + else: + return visitor.visitChildren(self) + + + + + def selectClause(self): + + localctx = SqlBaseParser.SelectClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 102, self.RULE_selectClause) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1949 + self.match(SqlBaseParser.SELECT) + self.state = 1953 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==328: + self.state = 1950 + localctx._hint = self.hint() + localctx.hints.append(localctx._hint) + self.state = 1955 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 1957 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,235,self._ctx) + if la_ == 1: + self.state = 1956 + self.setQuantifier() + + + self.state = 1959 + self.namedExpressionSeq() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SetClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + + def assignmentList(self): + return self.getTypedRuleContext(SqlBaseParser.AssignmentListContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_setClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSetClause" ): + listener.enterSetClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSetClause" ): + listener.exitSetClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSetClause" ): + return visitor.visitSetClause(self) + else: + return visitor.visitChildren(self) + + + + + def setClause(self): + + localctx = SqlBaseParser.SetClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 104, self.RULE_setClause) + try: + self.enterOuterAlt(localctx, 1) + self.state = 1961 + self.match(SqlBaseParser.SET) + self.state = 1962 + self.assignmentList() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MatchedClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.matchedCond = None # BooleanExpressionContext + + def WHEN(self): + return self.getToken(SqlBaseParser.WHEN, 0) + + def MATCHED(self): + return self.getToken(SqlBaseParser.MATCHED, 0) + + def THEN(self): + return self.getToken(SqlBaseParser.THEN, 0) + + def matchedAction(self): + return self.getTypedRuleContext(SqlBaseParser.MatchedActionContext,0) + + + def AND(self): + return self.getToken(SqlBaseParser.AND, 0) + + def booleanExpression(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanExpressionContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_matchedClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMatchedClause" ): + listener.enterMatchedClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMatchedClause" ): + listener.exitMatchedClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMatchedClause" ): + return visitor.visitMatchedClause(self) + else: + return visitor.visitChildren(self) + + + + + def matchedClause(self): + + localctx = SqlBaseParser.MatchedClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 106, self.RULE_matchedClause) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1964 + self.match(SqlBaseParser.WHEN) + self.state = 1965 + self.match(SqlBaseParser.MATCHED) + self.state = 1968 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==14: + self.state = 1966 + self.match(SqlBaseParser.AND) + self.state = 1967 + localctx.matchedCond = self.booleanExpression(0) + + + self.state = 1970 + self.match(SqlBaseParser.THEN) + self.state = 1971 + self.matchedAction() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NotMatchedClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.notMatchedCond = None # BooleanExpressionContext + + def WHEN(self): + return self.getToken(SqlBaseParser.WHEN, 0) + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + + def MATCHED(self): + return self.getToken(SqlBaseParser.MATCHED, 0) + + def THEN(self): + return self.getToken(SqlBaseParser.THEN, 0) + + def notMatchedAction(self): + return self.getTypedRuleContext(SqlBaseParser.NotMatchedActionContext,0) + + + def BY(self): + return self.getToken(SqlBaseParser.BY, 0) + + def TARGET(self): + return self.getToken(SqlBaseParser.TARGET, 0) + + def AND(self): + return self.getToken(SqlBaseParser.AND, 0) + + def booleanExpression(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanExpressionContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_notMatchedClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNotMatchedClause" ): + listener.enterNotMatchedClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNotMatchedClause" ): + listener.exitNotMatchedClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNotMatchedClause" ): + return visitor.visitNotMatchedClause(self) + else: + return visitor.visitChildren(self) + + + + + def notMatchedClause(self): + + localctx = SqlBaseParser.NotMatchedClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 108, self.RULE_notMatchedClause) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1973 + self.match(SqlBaseParser.WHEN) + self.state = 1974 + self.match(SqlBaseParser.NOT) + self.state = 1975 + self.match(SqlBaseParser.MATCHED) + self.state = 1978 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==28: + self.state = 1976 + self.match(SqlBaseParser.BY) + self.state = 1977 + self.match(SqlBaseParser.TARGET) + + + self.state = 1982 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==14: + self.state = 1980 + self.match(SqlBaseParser.AND) + self.state = 1981 + localctx.notMatchedCond = self.booleanExpression(0) + + + self.state = 1984 + self.match(SqlBaseParser.THEN) + self.state = 1985 + self.notMatchedAction() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NotMatchedBySourceClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.notMatchedBySourceCond = None # BooleanExpressionContext + + def WHEN(self): + return self.getToken(SqlBaseParser.WHEN, 0) + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + + def MATCHED(self): + return self.getToken(SqlBaseParser.MATCHED, 0) + + def BY(self): + return self.getToken(SqlBaseParser.BY, 0) + + def SOURCE(self): + return self.getToken(SqlBaseParser.SOURCE, 0) + + def THEN(self): + return self.getToken(SqlBaseParser.THEN, 0) + + def notMatchedBySourceAction(self): + return self.getTypedRuleContext(SqlBaseParser.NotMatchedBySourceActionContext,0) + + + def AND(self): + return self.getToken(SqlBaseParser.AND, 0) + + def booleanExpression(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanExpressionContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_notMatchedBySourceClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNotMatchedBySourceClause" ): + listener.enterNotMatchedBySourceClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNotMatchedBySourceClause" ): + listener.exitNotMatchedBySourceClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNotMatchedBySourceClause" ): + return visitor.visitNotMatchedBySourceClause(self) + else: + return visitor.visitChildren(self) + + + + + def notMatchedBySourceClause(self): + + localctx = SqlBaseParser.NotMatchedBySourceClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 110, self.RULE_notMatchedBySourceClause) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 1987 + self.match(SqlBaseParser.WHEN) + self.state = 1988 + self.match(SqlBaseParser.NOT) + self.state = 1989 + self.match(SqlBaseParser.MATCHED) + self.state = 1990 + self.match(SqlBaseParser.BY) + self.state = 1991 + self.match(SqlBaseParser.SOURCE) + self.state = 1994 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==14: + self.state = 1992 + self.match(SqlBaseParser.AND) + self.state = 1993 + localctx.notMatchedBySourceCond = self.booleanExpression(0) + + + self.state = 1996 + self.match(SqlBaseParser.THEN) + self.state = 1997 + self.notMatchedBySourceAction() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MatchedActionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def DELETE(self): + return self.getToken(SqlBaseParser.DELETE, 0) + + def UPDATE(self): + return self.getToken(SqlBaseParser.UPDATE, 0) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + + def ASTERISK(self): + return self.getToken(SqlBaseParser.ASTERISK, 0) + + def assignmentList(self): + return self.getTypedRuleContext(SqlBaseParser.AssignmentListContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_matchedAction + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMatchedAction" ): + listener.enterMatchedAction(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMatchedAction" ): + listener.exitMatchedAction(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMatchedAction" ): + return visitor.visitMatchedAction(self) + else: + return visitor.visitChildren(self) + + + + + def matchedAction(self): + + localctx = SqlBaseParser.MatchedActionContext(self, self._ctx, self.state) + self.enterRule(localctx, 112, self.RULE_matchedAction) + try: + self.state = 2006 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,240,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 1999 + self.match(SqlBaseParser.DELETE) + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 2000 + self.match(SqlBaseParser.UPDATE) + self.state = 2001 + self.match(SqlBaseParser.SET) + self.state = 2002 + self.match(SqlBaseParser.ASTERISK) + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 2003 + self.match(SqlBaseParser.UPDATE) + self.state = 2004 + self.match(SqlBaseParser.SET) + self.state = 2005 + self.assignmentList() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NotMatchedActionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.columns = None # MultipartIdentifierListContext + + def INSERT(self): + return self.getToken(SqlBaseParser.INSERT, 0) + + def ASTERISK(self): + return self.getToken(SqlBaseParser.ASTERISK, 0) + + def LEFT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.LEFT_PAREN) + else: + return self.getToken(SqlBaseParser.LEFT_PAREN, i) + + def RIGHT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.RIGHT_PAREN) + else: + return self.getToken(SqlBaseParser.RIGHT_PAREN, i) + + def VALUES(self): + return self.getToken(SqlBaseParser.VALUES, 0) + + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + + def multipartIdentifierList(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierListContext,0) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_notMatchedAction + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNotMatchedAction" ): + listener.enterNotMatchedAction(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNotMatchedAction" ): + listener.exitNotMatchedAction(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNotMatchedAction" ): + return visitor.visitNotMatchedAction(self) + else: + return visitor.visitChildren(self) + + + + + def notMatchedAction(self): + + localctx = SqlBaseParser.NotMatchedActionContext(self, self._ctx, self.state) + self.enterRule(localctx, 114, self.RULE_notMatchedAction) + self._la = 0 # Token type + try: + self.state = 2026 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,242,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 2008 + self.match(SqlBaseParser.INSERT) + self.state = 2009 + self.match(SqlBaseParser.ASTERISK) + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 2010 + self.match(SqlBaseParser.INSERT) + self.state = 2011 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2012 + localctx.columns = self.multipartIdentifierList() + self.state = 2013 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 2014 + self.match(SqlBaseParser.VALUES) + self.state = 2015 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2016 + self.expression() + self.state = 2021 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2017 + self.match(SqlBaseParser.COMMA) + self.state = 2018 + self.expression() + self.state = 2023 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2024 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NotMatchedBySourceActionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def DELETE(self): + return self.getToken(SqlBaseParser.DELETE, 0) + + def UPDATE(self): + return self.getToken(SqlBaseParser.UPDATE, 0) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + + def assignmentList(self): + return self.getTypedRuleContext(SqlBaseParser.AssignmentListContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_notMatchedBySourceAction + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNotMatchedBySourceAction" ): + listener.enterNotMatchedBySourceAction(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNotMatchedBySourceAction" ): + listener.exitNotMatchedBySourceAction(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNotMatchedBySourceAction" ): + return visitor.visitNotMatchedBySourceAction(self) + else: + return visitor.visitChildren(self) + + + + + def notMatchedBySourceAction(self): + + localctx = SqlBaseParser.NotMatchedBySourceActionContext(self, self._ctx, self.state) + self.enterRule(localctx, 116, self.RULE_notMatchedBySourceAction) + try: + self.state = 2032 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [72]: + self.enterOuterAlt(localctx, 1) + self.state = 2028 + self.match(SqlBaseParser.DELETE) + pass + elif token in [290]: + self.enterOuterAlt(localctx, 2) + self.state = 2029 + self.match(SqlBaseParser.UPDATE) + self.state = 2030 + self.match(SqlBaseParser.SET) + self.state = 2031 + self.assignmentList() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class AssignmentListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def assignment(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.AssignmentContext) + else: + return self.getTypedRuleContext(SqlBaseParser.AssignmentContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_assignmentList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAssignmentList" ): + listener.enterAssignmentList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAssignmentList" ): + listener.exitAssignmentList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAssignmentList" ): + return visitor.visitAssignmentList(self) + else: + return visitor.visitChildren(self) + + + + + def assignmentList(self): + + localctx = SqlBaseParser.AssignmentListContext(self, self._ctx, self.state) + self.enterRule(localctx, 118, self.RULE_assignmentList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2034 + self.assignment() + self.state = 2039 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2035 + self.match(SqlBaseParser.COMMA) + self.state = 2036 + self.assignment() + self.state = 2041 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class AssignmentContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.key = None # MultipartIdentifierContext + self.value = None # ExpressionContext + + def EQ(self): + return self.getToken(SqlBaseParser.EQ, 0) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_assignment + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAssignment" ): + listener.enterAssignment(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAssignment" ): + listener.exitAssignment(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAssignment" ): + return visitor.visitAssignment(self) + else: + return visitor.visitChildren(self) + + + + + def assignment(self): + + localctx = SqlBaseParser.AssignmentContext(self, self._ctx, self.state) + self.enterRule(localctx, 120, self.RULE_assignment) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2042 + localctx.key = self.multipartIdentifier() + self.state = 2043 + self.match(SqlBaseParser.EQ) + self.state = 2044 + localctx.value = self.expression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class WhereClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def WHERE(self): + return self.getToken(SqlBaseParser.WHERE, 0) + + def booleanExpression(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanExpressionContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_whereClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterWhereClause" ): + listener.enterWhereClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitWhereClause" ): + listener.exitWhereClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitWhereClause" ): + return visitor.visitWhereClause(self) + else: + return visitor.visitChildren(self) + + + + + def whereClause(self): + + localctx = SqlBaseParser.WhereClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 122, self.RULE_whereClause) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2046 + self.match(SqlBaseParser.WHERE) + self.state = 2047 + self.booleanExpression(0) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class HavingClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def HAVING(self): + return self.getToken(SqlBaseParser.HAVING, 0) + + def booleanExpression(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanExpressionContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_havingClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterHavingClause" ): + listener.enterHavingClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitHavingClause" ): + listener.exitHavingClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitHavingClause" ): + return visitor.visitHavingClause(self) + else: + return visitor.visitChildren(self) + + + + + def havingClause(self): + + localctx = SqlBaseParser.HavingClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 124, self.RULE_havingClause) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2049 + self.match(SqlBaseParser.HAVING) + self.state = 2050 + self.booleanExpression(0) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class HintContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._hintStatement = None # HintStatementContext + self.hintStatements = list() # of HintStatementContexts + + def HENT_START(self): + return self.getToken(SqlBaseParser.HENT_START, 0) + + def HENT_END(self): + return self.getToken(SqlBaseParser.HENT_END, 0) + + def hintStatement(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.HintStatementContext) + else: + return self.getTypedRuleContext(SqlBaseParser.HintStatementContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_hint + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterHint" ): + listener.enterHint(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitHint" ): + listener.exitHint(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitHint" ): + return visitor.visitHint(self) + else: + return visitor.visitChildren(self) + + + + + def hint(self): + + localctx = SqlBaseParser.HintContext(self, self._ctx, self.state) + self.enterRule(localctx, 126, self.RULE_hint) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2052 + self.match(SqlBaseParser.HENT_START) + self.state = 2053 + localctx._hintStatement = self.hintStatement() + localctx.hintStatements.append(localctx._hintStatement) + self.state = 2060 + self._errHandler.sync(self) + _la = self._input.LA(1) + while (((_la) & ~0x3f) == 0 and ((1 << _la) & -240) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 4503599627370495) != 0) or ((((_la - 331)) & ~0x3f) == 0 and ((1 << (_la - 331)) & 3073) != 0): + self.state = 2055 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==4: + self.state = 2054 + self.match(SqlBaseParser.COMMA) + + + self.state = 2057 + localctx._hintStatement = self.hintStatement() + localctx.hintStatements.append(localctx._hintStatement) + self.state = 2062 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2063 + self.match(SqlBaseParser.HENT_END) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class HintStatementContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.hintName = None # IdentifierContext + self._primaryExpression = None # PrimaryExpressionContext + self.parameters = list() # of PrimaryExpressionContexts + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def primaryExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PrimaryExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PrimaryExpressionContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_hintStatement + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterHintStatement" ): + listener.enterHintStatement(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitHintStatement" ): + listener.exitHintStatement(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitHintStatement" ): + return visitor.visitHintStatement(self) + else: + return visitor.visitChildren(self) + + + + + def hintStatement(self): + + localctx = SqlBaseParser.HintStatementContext(self, self._ctx, self.state) + self.enterRule(localctx, 128, self.RULE_hintStatement) + self._la = 0 # Token type + try: + self.state = 2078 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,248,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 2065 + localctx.hintName = self.identifier() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 2066 + localctx.hintName = self.identifier() + self.state = 2067 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2068 + localctx._primaryExpression = self.primaryExpression(0) + localctx.parameters.append(localctx._primaryExpression) + self.state = 2073 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2069 + self.match(SqlBaseParser.COMMA) + self.state = 2070 + localctx._primaryExpression = self.primaryExpression(0) + localctx.parameters.append(localctx._primaryExpression) + self.state = 2075 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2076 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FromClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + + def relation(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.RelationContext) + else: + return self.getTypedRuleContext(SqlBaseParser.RelationContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def lateralView(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.LateralViewContext) + else: + return self.getTypedRuleContext(SqlBaseParser.LateralViewContext,i) + + + def pivotClause(self): + return self.getTypedRuleContext(SqlBaseParser.PivotClauseContext,0) + + + def unpivotClause(self): + return self.getTypedRuleContext(SqlBaseParser.UnpivotClauseContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_fromClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFromClause" ): + listener.enterFromClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFromClause" ): + listener.exitFromClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFromClause" ): + return visitor.visitFromClause(self) + else: + return visitor.visitChildren(self) + + + + + def fromClause(self): + + localctx = SqlBaseParser.FromClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 130, self.RULE_fromClause) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2080 + self.match(SqlBaseParser.FROM) + self.state = 2081 + self.relation() + self.state = 2086 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,249,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 2082 + self.match(SqlBaseParser.COMMA) + self.state = 2083 + self.relation() + self.state = 2088 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,249,self._ctx) + + self.state = 2092 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,250,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 2089 + self.lateralView() + self.state = 2094 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,250,self._ctx) + + self.state = 2096 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,251,self._ctx) + if la_ == 1: + self.state = 2095 + self.pivotClause() + + + self.state = 2099 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,252,self._ctx) + if la_ == 1: + self.state = 2098 + self.unpivotClause() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class TemporalClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.timestamp = None # ValueExpressionContext + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def OF(self): + return self.getToken(SqlBaseParser.OF, 0) + + def version(self): + return self.getTypedRuleContext(SqlBaseParser.VersionContext,0) + + + def SYSTEM_VERSION(self): + return self.getToken(SqlBaseParser.SYSTEM_VERSION, 0) + + def VERSION(self): + return self.getToken(SqlBaseParser.VERSION, 0) + + def FOR(self): + return self.getToken(SqlBaseParser.FOR, 0) + + def SYSTEM_TIME(self): + return self.getToken(SqlBaseParser.SYSTEM_TIME, 0) + + def TIMESTAMP(self): + return self.getToken(SqlBaseParser.TIMESTAMP, 0) + + def valueExpression(self): + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_temporalClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTemporalClause" ): + listener.enterTemporalClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTemporalClause" ): + listener.exitTemporalClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTemporalClause" ): + return visitor.visitTemporalClause(self) + else: + return visitor.visitChildren(self) + + + + + def temporalClause(self): + + localctx = SqlBaseParser.TemporalClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 132, self.RULE_temporalClause) + self._la = 0 # Token type + try: + self.state = 2115 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,255,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 2102 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==103: + self.state = 2101 + self.match(SqlBaseParser.FOR) + + + self.state = 2104 + _la = self._input.LA(1) + if not(_la==257 or _la==295): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2105 + self.match(SqlBaseParser.AS) + self.state = 2106 + self.match(SqlBaseParser.OF) + self.state = 2107 + self.version() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 2109 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==103: + self.state = 2108 + self.match(SqlBaseParser.FOR) + + + self.state = 2111 + _la = self._input.LA(1) + if not(_la==256 or _la==267): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2112 + self.match(SqlBaseParser.AS) + self.state = 2113 + self.match(SqlBaseParser.OF) + self.state = 2114 + localctx.timestamp = self.valueExpression(0) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class AggregationClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._groupByClause = None # GroupByClauseContext + self.groupingExpressionsWithGroupingAnalytics = list() # of GroupByClauseContexts + self._expression = None # ExpressionContext + self.groupingExpressions = list() # of ExpressionContexts + self.kind = None # Token + + def GROUP(self): + return self.getToken(SqlBaseParser.GROUP, 0) + + def BY(self): + return self.getToken(SqlBaseParser.BY, 0) + + def groupByClause(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.GroupByClauseContext) + else: + return self.getTypedRuleContext(SqlBaseParser.GroupByClauseContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + + def WITH(self): + return self.getToken(SqlBaseParser.WITH, 0) + + def SETS(self): + return self.getToken(SqlBaseParser.SETS, 0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def groupingSet(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.GroupingSetContext) + else: + return self.getTypedRuleContext(SqlBaseParser.GroupingSetContext,i) + + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def ROLLUP(self): + return self.getToken(SqlBaseParser.ROLLUP, 0) + + def CUBE(self): + return self.getToken(SqlBaseParser.CUBE, 0) + + def GROUPING(self): + return self.getToken(SqlBaseParser.GROUPING, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_aggregationClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAggregationClause" ): + listener.enterAggregationClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAggregationClause" ): + listener.exitAggregationClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAggregationClause" ): + return visitor.visitAggregationClause(self) + else: + return visitor.visitChildren(self) + + + + + def aggregationClause(self): + + localctx = SqlBaseParser.AggregationClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 134, self.RULE_aggregationClause) + self._la = 0 # Token type + try: + self.state = 2156 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,260,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 2117 + self.match(SqlBaseParser.GROUP) + self.state = 2118 + self.match(SqlBaseParser.BY) + self.state = 2119 + localctx._groupByClause = self.groupByClause() + localctx.groupingExpressionsWithGroupingAnalytics.append(localctx._groupByClause) + self.state = 2124 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,256,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 2120 + self.match(SqlBaseParser.COMMA) + self.state = 2121 + localctx._groupByClause = self.groupByClause() + localctx.groupingExpressionsWithGroupingAnalytics.append(localctx._groupByClause) + self.state = 2126 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,256,self._ctx) + + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 2127 + self.match(SqlBaseParser.GROUP) + self.state = 2128 + self.match(SqlBaseParser.BY) + self.state = 2129 + localctx._expression = self.expression() + localctx.groupingExpressions.append(localctx._expression) + self.state = 2134 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,257,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 2130 + self.match(SqlBaseParser.COMMA) + self.state = 2131 + localctx._expression = self.expression() + localctx.groupingExpressions.append(localctx._expression) + self.state = 2136 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,257,self._ctx) + + self.state = 2154 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,259,self._ctx) + if la_ == 1: + self.state = 2137 + self.match(SqlBaseParser.WITH) + self.state = 2138 + localctx.kind = self.match(SqlBaseParser.ROLLUP) + + elif la_ == 2: + self.state = 2139 + self.match(SqlBaseParser.WITH) + self.state = 2140 + localctx.kind = self.match(SqlBaseParser.CUBE) + + elif la_ == 3: + self.state = 2141 + localctx.kind = self.match(SqlBaseParser.GROUPING) + self.state = 2142 + self.match(SqlBaseParser.SETS) + self.state = 2143 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2144 + self.groupingSet() + self.state = 2149 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2145 + self.match(SqlBaseParser.COMMA) + self.state = 2146 + self.groupingSet() + self.state = 2151 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2152 + self.match(SqlBaseParser.RIGHT_PAREN) + + + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class GroupByClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def groupingAnalytics(self): + return self.getTypedRuleContext(SqlBaseParser.GroupingAnalyticsContext,0) + + + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_groupByClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterGroupByClause" ): + listener.enterGroupByClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitGroupByClause" ): + listener.exitGroupByClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitGroupByClause" ): + return visitor.visitGroupByClause(self) + else: + return visitor.visitChildren(self) + + + + + def groupByClause(self): + + localctx = SqlBaseParser.GroupByClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 136, self.RULE_groupByClause) + try: + self.state = 2160 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,261,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 2158 + self.groupingAnalytics() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 2159 + self.expression() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class GroupingAnalyticsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def groupingSet(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.GroupingSetContext) + else: + return self.getTypedRuleContext(SqlBaseParser.GroupingSetContext,i) + + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def ROLLUP(self): + return self.getToken(SqlBaseParser.ROLLUP, 0) + + def CUBE(self): + return self.getToken(SqlBaseParser.CUBE, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def GROUPING(self): + return self.getToken(SqlBaseParser.GROUPING, 0) + + def SETS(self): + return self.getToken(SqlBaseParser.SETS, 0) + + def groupingElement(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.GroupingElementContext) + else: + return self.getTypedRuleContext(SqlBaseParser.GroupingElementContext,i) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_groupingAnalytics + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterGroupingAnalytics" ): + listener.enterGroupingAnalytics(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitGroupingAnalytics" ): + listener.exitGroupingAnalytics(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitGroupingAnalytics" ): + return visitor.visitGroupingAnalytics(self) + else: + return visitor.visitChildren(self) + + + + + def groupingAnalytics(self): + + localctx = SqlBaseParser.GroupingAnalyticsContext(self, self._ctx, self.state) + self.enterRule(localctx, 138, self.RULE_groupingAnalytics) + self._la = 0 # Token type + try: + self.state = 2187 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [55, 226]: + self.enterOuterAlt(localctx, 1) + self.state = 2162 + _la = self._input.LA(1) + if not(_la==55 or _la==226): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2163 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2164 + self.groupingSet() + self.state = 2169 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2165 + self.match(SqlBaseParser.COMMA) + self.state = 2166 + self.groupingSet() + self.state = 2171 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2172 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + elif token in [115]: + self.enterOuterAlt(localctx, 2) + self.state = 2174 + self.match(SqlBaseParser.GROUPING) + self.state = 2175 + self.match(SqlBaseParser.SETS) + self.state = 2176 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2177 + self.groupingElement() + self.state = 2182 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2178 + self.match(SqlBaseParser.COMMA) + self.state = 2179 + self.groupingElement() + self.state = 2184 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2185 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class GroupingElementContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def groupingAnalytics(self): + return self.getTypedRuleContext(SqlBaseParser.GroupingAnalyticsContext,0) + + + def groupingSet(self): + return self.getTypedRuleContext(SqlBaseParser.GroupingSetContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_groupingElement + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterGroupingElement" ): + listener.enterGroupingElement(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitGroupingElement" ): + listener.exitGroupingElement(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitGroupingElement" ): + return visitor.visitGroupingElement(self) + else: + return visitor.visitChildren(self) + + + + + def groupingElement(self): + + localctx = SqlBaseParser.GroupingElementContext(self, self._ctx, self.state) + self.enterRule(localctx, 140, self.RULE_groupingElement) + try: + self.state = 2191 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,265,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 2189 + self.groupingAnalytics() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 2190 + self.groupingSet() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class GroupingSetContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_groupingSet + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterGroupingSet" ): + listener.enterGroupingSet(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitGroupingSet" ): + listener.exitGroupingSet(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitGroupingSet" ): + return visitor.visitGroupingSet(self) + else: + return visitor.visitChildren(self) + + + + + def groupingSet(self): + + localctx = SqlBaseParser.GroupingSetContext(self, self._ctx, self.state) + self.enterRule(localctx, 142, self.RULE_groupingSet) + self._la = 0 # Token type + try: + self.state = 2206 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,268,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 2193 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2202 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -252) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 8074954131875299327) != 0) or ((((_la - 321)) & ~0x3f) == 0 and ((1 << (_la - 321)) & 4193825) != 0): + self.state = 2194 + self.expression() + self.state = 2199 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2195 + self.match(SqlBaseParser.COMMA) + self.state = 2196 + self.expression() + self.state = 2201 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + self.state = 2204 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 2205 + self.expression() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PivotClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.aggregates = None # NamedExpressionSeqContext + self._pivotValue = None # PivotValueContext + self.pivotValues = list() # of PivotValueContexts + + def PIVOT(self): + return self.getToken(SqlBaseParser.PIVOT, 0) + + def LEFT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.LEFT_PAREN) + else: + return self.getToken(SqlBaseParser.LEFT_PAREN, i) + + def FOR(self): + return self.getToken(SqlBaseParser.FOR, 0) + + def pivotColumn(self): + return self.getTypedRuleContext(SqlBaseParser.PivotColumnContext,0) + + + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + + def RIGHT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.RIGHT_PAREN) + else: + return self.getToken(SqlBaseParser.RIGHT_PAREN, i) + + def namedExpressionSeq(self): + return self.getTypedRuleContext(SqlBaseParser.NamedExpressionSeqContext,0) + + + def pivotValue(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PivotValueContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PivotValueContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_pivotClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPivotClause" ): + listener.enterPivotClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPivotClause" ): + listener.exitPivotClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPivotClause" ): + return visitor.visitPivotClause(self) + else: + return visitor.visitChildren(self) + + + + + def pivotClause(self): + + localctx = SqlBaseParser.PivotClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 144, self.RULE_pivotClause) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2208 + self.match(SqlBaseParser.PIVOT) + self.state = 2209 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2210 + localctx.aggregates = self.namedExpressionSeq() + self.state = 2211 + self.match(SqlBaseParser.FOR) + self.state = 2212 + self.pivotColumn() + self.state = 2213 + self.match(SqlBaseParser.IN) + self.state = 2214 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2215 + localctx._pivotValue = self.pivotValue() + localctx.pivotValues.append(localctx._pivotValue) + self.state = 2220 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2216 + self.match(SqlBaseParser.COMMA) + self.state = 2217 + localctx._pivotValue = self.pivotValue() + localctx.pivotValues.append(localctx._pivotValue) + self.state = 2222 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2223 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 2224 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PivotColumnContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._identifier = None # IdentifierContext + self.identifiers = list() # of IdentifierContexts + + def identifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.IdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,i) + + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_pivotColumn + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPivotColumn" ): + listener.enterPivotColumn(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPivotColumn" ): + listener.exitPivotColumn(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPivotColumn" ): + return visitor.visitPivotColumn(self) + else: + return visitor.visitChildren(self) + + + + + def pivotColumn(self): + + localctx = SqlBaseParser.PivotColumnContext(self, self._ctx, self.state) + self.enterRule(localctx, 146, self.RULE_pivotColumn) + self._la = 0 # Token type + try: + self.state = 2238 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 331, 341, 342]: + self.enterOuterAlt(localctx, 1) + self.state = 2226 + localctx._identifier = self.identifier() + localctx.identifiers.append(localctx._identifier) + pass + elif token in [2]: + self.enterOuterAlt(localctx, 2) + self.state = 2227 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2228 + localctx._identifier = self.identifier() + localctx.identifiers.append(localctx._identifier) + self.state = 2233 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2229 + self.match(SqlBaseParser.COMMA) + self.state = 2230 + localctx._identifier = self.identifier() + localctx.identifiers.append(localctx._identifier) + self.state = 2235 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2236 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PivotValueContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_pivotValue + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPivotValue" ): + listener.enterPivotValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPivotValue" ): + listener.exitPivotValue(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPivotValue" ): + return visitor.visitPivotValue(self) + else: + return visitor.visitChildren(self) + + + + + def pivotValue(self): + + localctx = SqlBaseParser.PivotValueContext(self, self._ctx, self.state) + self.enterRule(localctx, 148, self.RULE_pivotValue) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2240 + self.expression() + self.state = 2245 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -256) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 4503599627370495) != 0) or ((((_la - 331)) & ~0x3f) == 0 and ((1 << (_la - 331)) & 3073) != 0): + self.state = 2242 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,272,self._ctx) + if la_ == 1: + self.state = 2241 + self.match(SqlBaseParser.AS) + + + self.state = 2244 + self.identifier() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnpivotClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.nullOperator = None # UnpivotNullClauseContext + self.operator = None # UnpivotOperatorContext + + def UNPIVOT(self): + return self.getToken(SqlBaseParser.UNPIVOT, 0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def unpivotOperator(self): + return self.getTypedRuleContext(SqlBaseParser.UnpivotOperatorContext,0) + + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def unpivotNullClause(self): + return self.getTypedRuleContext(SqlBaseParser.UnpivotNullClauseContext,0) + + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_unpivotClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnpivotClause" ): + listener.enterUnpivotClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnpivotClause" ): + listener.exitUnpivotClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnpivotClause" ): + return visitor.visitUnpivotClause(self) + else: + return visitor.visitChildren(self) + + + + + def unpivotClause(self): + + localctx = SqlBaseParser.UnpivotClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 150, self.RULE_unpivotClause) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2247 + self.match(SqlBaseParser.UNPIVOT) + self.state = 2249 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==89 or _la==123: + self.state = 2248 + localctx.nullOperator = self.unpivotNullClause() + + + self.state = 2251 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2252 + localctx.operator = self.unpivotOperator() + self.state = 2253 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 2258 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,276,self._ctx) + if la_ == 1: + self.state = 2255 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,275,self._ctx) + if la_ == 1: + self.state = 2254 + self.match(SqlBaseParser.AS) + + + self.state = 2257 + self.identifier() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnpivotNullClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def NULLS(self): + return self.getToken(SqlBaseParser.NULLS, 0) + + def INCLUDE(self): + return self.getToken(SqlBaseParser.INCLUDE, 0) + + def EXCLUDE(self): + return self.getToken(SqlBaseParser.EXCLUDE, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_unpivotNullClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnpivotNullClause" ): + listener.enterUnpivotNullClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnpivotNullClause" ): + listener.exitUnpivotNullClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnpivotNullClause" ): + return visitor.visitUnpivotNullClause(self) + else: + return visitor.visitChildren(self) + + + + + def unpivotNullClause(self): + + localctx = SqlBaseParser.UnpivotNullClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 152, self.RULE_unpivotNullClause) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2260 + _la = self._input.LA(1) + if not(_la==89 or _la==123): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2261 + self.match(SqlBaseParser.NULLS) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnpivotOperatorContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def unpivotSingleValueColumnClause(self): + return self.getTypedRuleContext(SqlBaseParser.UnpivotSingleValueColumnClauseContext,0) + + + def unpivotMultiValueColumnClause(self): + return self.getTypedRuleContext(SqlBaseParser.UnpivotMultiValueColumnClauseContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_unpivotOperator + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnpivotOperator" ): + listener.enterUnpivotOperator(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnpivotOperator" ): + listener.exitUnpivotOperator(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnpivotOperator" ): + return visitor.visitUnpivotOperator(self) + else: + return visitor.visitChildren(self) + + + + + def unpivotOperator(self): + + localctx = SqlBaseParser.UnpivotOperatorContext(self, self._ctx, self.state) + self.enterRule(localctx, 154, self.RULE_unpivotOperator) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2265 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 331, 341, 342]: + self.state = 2263 + self.unpivotSingleValueColumnClause() + pass + elif token in [2]: + self.state = 2264 + self.unpivotMultiValueColumnClause() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnpivotSingleValueColumnClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._unpivotColumnAndAlias = None # UnpivotColumnAndAliasContext + self.unpivotColumns = list() # of UnpivotColumnAndAliasContexts + + def unpivotValueColumn(self): + return self.getTypedRuleContext(SqlBaseParser.UnpivotValueColumnContext,0) + + + def FOR(self): + return self.getToken(SqlBaseParser.FOR, 0) + + def unpivotNameColumn(self): + return self.getTypedRuleContext(SqlBaseParser.UnpivotNameColumnContext,0) + + + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def unpivotColumnAndAlias(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.UnpivotColumnAndAliasContext) + else: + return self.getTypedRuleContext(SqlBaseParser.UnpivotColumnAndAliasContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_unpivotSingleValueColumnClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnpivotSingleValueColumnClause" ): + listener.enterUnpivotSingleValueColumnClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnpivotSingleValueColumnClause" ): + listener.exitUnpivotSingleValueColumnClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnpivotSingleValueColumnClause" ): + return visitor.visitUnpivotSingleValueColumnClause(self) + else: + return visitor.visitChildren(self) + + + + + def unpivotSingleValueColumnClause(self): + + localctx = SqlBaseParser.UnpivotSingleValueColumnClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 156, self.RULE_unpivotSingleValueColumnClause) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2267 + self.unpivotValueColumn() + self.state = 2268 + self.match(SqlBaseParser.FOR) + self.state = 2269 + self.unpivotNameColumn() + self.state = 2270 + self.match(SqlBaseParser.IN) + self.state = 2271 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2272 + localctx._unpivotColumnAndAlias = self.unpivotColumnAndAlias() + localctx.unpivotColumns.append(localctx._unpivotColumnAndAlias) + self.state = 2277 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2273 + self.match(SqlBaseParser.COMMA) + self.state = 2274 + localctx._unpivotColumnAndAlias = self.unpivotColumnAndAlias() + localctx.unpivotColumns.append(localctx._unpivotColumnAndAlias) + self.state = 2279 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2280 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnpivotMultiValueColumnClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._unpivotValueColumn = None # UnpivotValueColumnContext + self.unpivotValueColumns = list() # of UnpivotValueColumnContexts + self._unpivotColumnSet = None # UnpivotColumnSetContext + self.unpivotColumnSets = list() # of UnpivotColumnSetContexts + + def LEFT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.LEFT_PAREN) + else: + return self.getToken(SqlBaseParser.LEFT_PAREN, i) + + def RIGHT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.RIGHT_PAREN) + else: + return self.getToken(SqlBaseParser.RIGHT_PAREN, i) + + def FOR(self): + return self.getToken(SqlBaseParser.FOR, 0) + + def unpivotNameColumn(self): + return self.getTypedRuleContext(SqlBaseParser.UnpivotNameColumnContext,0) + + + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + + def unpivotValueColumn(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.UnpivotValueColumnContext) + else: + return self.getTypedRuleContext(SqlBaseParser.UnpivotValueColumnContext,i) + + + def unpivotColumnSet(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.UnpivotColumnSetContext) + else: + return self.getTypedRuleContext(SqlBaseParser.UnpivotColumnSetContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_unpivotMultiValueColumnClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnpivotMultiValueColumnClause" ): + listener.enterUnpivotMultiValueColumnClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnpivotMultiValueColumnClause" ): + listener.exitUnpivotMultiValueColumnClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnpivotMultiValueColumnClause" ): + return visitor.visitUnpivotMultiValueColumnClause(self) + else: + return visitor.visitChildren(self) + + + + + def unpivotMultiValueColumnClause(self): + + localctx = SqlBaseParser.UnpivotMultiValueColumnClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 158, self.RULE_unpivotMultiValueColumnClause) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2282 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2283 + localctx._unpivotValueColumn = self.unpivotValueColumn() + localctx.unpivotValueColumns.append(localctx._unpivotValueColumn) + self.state = 2288 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2284 + self.match(SqlBaseParser.COMMA) + self.state = 2285 + localctx._unpivotValueColumn = self.unpivotValueColumn() + localctx.unpivotValueColumns.append(localctx._unpivotValueColumn) + self.state = 2290 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2291 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 2292 + self.match(SqlBaseParser.FOR) + self.state = 2293 + self.unpivotNameColumn() + self.state = 2294 + self.match(SqlBaseParser.IN) + self.state = 2295 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2296 + localctx._unpivotColumnSet = self.unpivotColumnSet() + localctx.unpivotColumnSets.append(localctx._unpivotColumnSet) + self.state = 2301 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2297 + self.match(SqlBaseParser.COMMA) + self.state = 2298 + localctx._unpivotColumnSet = self.unpivotColumnSet() + localctx.unpivotColumnSets.append(localctx._unpivotColumnSet) + self.state = 2303 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2304 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnpivotColumnSetContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._unpivotColumn = None # UnpivotColumnContext + self.unpivotColumns = list() # of UnpivotColumnContexts + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def unpivotColumn(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.UnpivotColumnContext) + else: + return self.getTypedRuleContext(SqlBaseParser.UnpivotColumnContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def unpivotAlias(self): + return self.getTypedRuleContext(SqlBaseParser.UnpivotAliasContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_unpivotColumnSet + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnpivotColumnSet" ): + listener.enterUnpivotColumnSet(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnpivotColumnSet" ): + listener.exitUnpivotColumnSet(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnpivotColumnSet" ): + return visitor.visitUnpivotColumnSet(self) + else: + return visitor.visitChildren(self) + + + + + def unpivotColumnSet(self): + + localctx = SqlBaseParser.UnpivotColumnSetContext(self, self._ctx, self.state) + self.enterRule(localctx, 160, self.RULE_unpivotColumnSet) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2306 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2307 + localctx._unpivotColumn = self.unpivotColumn() + localctx.unpivotColumns.append(localctx._unpivotColumn) + self.state = 2312 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2308 + self.match(SqlBaseParser.COMMA) + self.state = 2309 + localctx._unpivotColumn = self.unpivotColumn() + localctx.unpivotColumns.append(localctx._unpivotColumn) + self.state = 2314 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2315 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 2317 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -256) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 4503599627370495) != 0) or ((((_la - 331)) & ~0x3f) == 0 and ((1 << (_la - 331)) & 3073) != 0): + self.state = 2316 + self.unpivotAlias() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnpivotValueColumnContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_unpivotValueColumn + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnpivotValueColumn" ): + listener.enterUnpivotValueColumn(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnpivotValueColumn" ): + listener.exitUnpivotValueColumn(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnpivotValueColumn" ): + return visitor.visitUnpivotValueColumn(self) + else: + return visitor.visitChildren(self) + + + + + def unpivotValueColumn(self): + + localctx = SqlBaseParser.UnpivotValueColumnContext(self, self._ctx, self.state) + self.enterRule(localctx, 162, self.RULE_unpivotValueColumn) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2319 + self.identifier() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnpivotNameColumnContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_unpivotNameColumn + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnpivotNameColumn" ): + listener.enterUnpivotNameColumn(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnpivotNameColumn" ): + listener.exitUnpivotNameColumn(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnpivotNameColumn" ): + return visitor.visitUnpivotNameColumn(self) + else: + return visitor.visitChildren(self) + + + + + def unpivotNameColumn(self): + + localctx = SqlBaseParser.UnpivotNameColumnContext(self, self._ctx, self.state) + self.enterRule(localctx, 164, self.RULE_unpivotNameColumn) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2321 + self.identifier() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnpivotColumnAndAliasContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def unpivotColumn(self): + return self.getTypedRuleContext(SqlBaseParser.UnpivotColumnContext,0) + + + def unpivotAlias(self): + return self.getTypedRuleContext(SqlBaseParser.UnpivotAliasContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_unpivotColumnAndAlias + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnpivotColumnAndAlias" ): + listener.enterUnpivotColumnAndAlias(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnpivotColumnAndAlias" ): + listener.exitUnpivotColumnAndAlias(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnpivotColumnAndAlias" ): + return visitor.visitUnpivotColumnAndAlias(self) + else: + return visitor.visitChildren(self) + + + + + def unpivotColumnAndAlias(self): + + localctx = SqlBaseParser.UnpivotColumnAndAliasContext(self, self._ctx, self.state) + self.enterRule(localctx, 166, self.RULE_unpivotColumnAndAlias) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2323 + self.unpivotColumn() + self.state = 2325 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -256) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 4503599627370495) != 0) or ((((_la - 331)) & ~0x3f) == 0 and ((1 << (_la - 331)) & 3073) != 0): + self.state = 2324 + self.unpivotAlias() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnpivotColumnContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_unpivotColumn + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnpivotColumn" ): + listener.enterUnpivotColumn(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnpivotColumn" ): + listener.exitUnpivotColumn(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnpivotColumn" ): + return visitor.visitUnpivotColumn(self) + else: + return visitor.visitChildren(self) + + + + + def unpivotColumn(self): + + localctx = SqlBaseParser.UnpivotColumnContext(self, self._ctx, self.state) + self.enterRule(localctx, 168, self.RULE_unpivotColumn) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2327 + self.multipartIdentifier() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnpivotAliasContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_unpivotAlias + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnpivotAlias" ): + listener.enterUnpivotAlias(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnpivotAlias" ): + listener.exitUnpivotAlias(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnpivotAlias" ): + return visitor.visitUnpivotAlias(self) + else: + return visitor.visitChildren(self) + + + + + def unpivotAlias(self): + + localctx = SqlBaseParser.UnpivotAliasContext(self, self._ctx, self.state) + self.enterRule(localctx, 170, self.RULE_unpivotAlias) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2330 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,284,self._ctx) + if la_ == 1: + self.state = 2329 + self.match(SqlBaseParser.AS) + + + self.state = 2332 + self.identifier() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class LateralViewContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.tblName = None # IdentifierContext + self._identifier = None # IdentifierContext + self.colName = list() # of IdentifierContexts + + def LATERAL(self): + return self.getToken(SqlBaseParser.LATERAL, 0) + + def VIEW(self): + return self.getToken(SqlBaseParser.VIEW, 0) + + def qualifiedName(self): + return self.getTypedRuleContext(SqlBaseParser.QualifiedNameContext,0) + + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def identifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.IdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,i) + + + def OUTER(self): + return self.getToken(SqlBaseParser.OUTER, 0) + + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_lateralView + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterLateralView" ): + listener.enterLateralView(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitLateralView" ): + listener.exitLateralView(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitLateralView" ): + return visitor.visitLateralView(self) + else: + return visitor.visitChildren(self) + + + + + def lateralView(self): + + localctx = SqlBaseParser.LateralViewContext(self, self._ctx, self.state) + self.enterRule(localctx, 172, self.RULE_lateralView) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2334 + self.match(SqlBaseParser.LATERAL) + self.state = 2335 + self.match(SqlBaseParser.VIEW) + self.state = 2337 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,285,self._ctx) + if la_ == 1: + self.state = 2336 + self.match(SqlBaseParser.OUTER) + + + self.state = 2339 + self.qualifiedName() + self.state = 2340 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2349 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -252) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 8074954131875299327) != 0) or ((((_la - 321)) & ~0x3f) == 0 and ((1 << (_la - 321)) & 4193825) != 0): + self.state = 2341 + self.expression() + self.state = 2346 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2342 + self.match(SqlBaseParser.COMMA) + self.state = 2343 + self.expression() + self.state = 2348 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + self.state = 2351 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 2352 + localctx.tblName = self.identifier() + self.state = 2364 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,290,self._ctx) + if la_ == 1: + self.state = 2354 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,288,self._ctx) + if la_ == 1: + self.state = 2353 + self.match(SqlBaseParser.AS) + + + self.state = 2356 + localctx._identifier = self.identifier() + localctx.colName.append(localctx._identifier) + self.state = 2361 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,289,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 2357 + self.match(SqlBaseParser.COMMA) + self.state = 2358 + localctx._identifier = self.identifier() + localctx.colName.append(localctx._identifier) + self.state = 2363 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,289,self._ctx) + + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SetQuantifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def DISTINCT(self): + return self.getToken(SqlBaseParser.DISTINCT, 0) + + def ALL(self): + return self.getToken(SqlBaseParser.ALL, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_setQuantifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSetQuantifier" ): + listener.enterSetQuantifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSetQuantifier" ): + listener.exitSetQuantifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSetQuantifier" ): + return visitor.visitSetQuantifier(self) + else: + return visitor.visitChildren(self) + + + + + def setQuantifier(self): + + localctx = SqlBaseParser.SetQuantifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 174, self.RULE_setQuantifier) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2366 + _la = self._input.LA(1) + if not(_la==10 or _la==79): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class RelationContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def relationPrimary(self): + return self.getTypedRuleContext(SqlBaseParser.RelationPrimaryContext,0) + + + def LATERAL(self): + return self.getToken(SqlBaseParser.LATERAL, 0) + + def relationExtension(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.RelationExtensionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.RelationExtensionContext,i) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_relation + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRelation" ): + listener.enterRelation(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRelation" ): + listener.exitRelation(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRelation" ): + return visitor.visitRelation(self) + else: + return visitor.visitChildren(self) + + + + + def relation(self): + + localctx = SqlBaseParser.RelationContext(self, self._ctx, self.state) + self.enterRule(localctx, 176, self.RULE_relation) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2369 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,291,self._ctx) + if la_ == 1: + self.state = 2368 + self.match(SqlBaseParser.LATERAL) + + + self.state = 2371 + self.relationPrimary() + self.state = 2375 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,292,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 2372 + self.relationExtension() + self.state = 2377 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,292,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class RelationExtensionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def joinRelation(self): + return self.getTypedRuleContext(SqlBaseParser.JoinRelationContext,0) + + + def pivotClause(self): + return self.getTypedRuleContext(SqlBaseParser.PivotClauseContext,0) + + + def unpivotClause(self): + return self.getTypedRuleContext(SqlBaseParser.UnpivotClauseContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_relationExtension + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRelationExtension" ): + listener.enterRelationExtension(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRelationExtension" ): + listener.exitRelationExtension(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRelationExtension" ): + return visitor.visitRelationExtension(self) + else: + return visitor.visitChildren(self) + + + + + def relationExtension(self): + + localctx = SqlBaseParser.RelationExtensionContext(self, self._ctx, self.state) + self.enterRule(localctx, 178, self.RULE_relationExtension) + try: + self.state = 2381 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [15, 54, 108, 126, 135, 141, 170, 221, 234]: + self.enterOuterAlt(localctx, 1) + self.state = 2378 + self.joinRelation() + pass + elif token in [196]: + self.enterOuterAlt(localctx, 2) + self.state = 2379 + self.pivotClause() + pass + elif token in [288]: + self.enterOuterAlt(localctx, 3) + self.state = 2380 + self.unpivotClause() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class JoinRelationContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.right = None # RelationPrimaryContext + + def JOIN(self): + return self.getToken(SqlBaseParser.JOIN, 0) + + def relationPrimary(self): + return self.getTypedRuleContext(SqlBaseParser.RelationPrimaryContext,0) + + + def joinType(self): + return self.getTypedRuleContext(SqlBaseParser.JoinTypeContext,0) + + + def LATERAL(self): + return self.getToken(SqlBaseParser.LATERAL, 0) + + def joinCriteria(self): + return self.getTypedRuleContext(SqlBaseParser.JoinCriteriaContext,0) + + + def NATURAL(self): + return self.getToken(SqlBaseParser.NATURAL, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_joinRelation + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterJoinRelation" ): + listener.enterJoinRelation(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitJoinRelation" ): + listener.exitJoinRelation(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitJoinRelation" ): + return visitor.visitJoinRelation(self) + else: + return visitor.visitChildren(self) + + + + + def joinRelation(self): + + localctx = SqlBaseParser.JoinRelationContext(self, self._ctx, self.state) + self.enterRule(localctx, 180, self.RULE_joinRelation) + try: + self.state = 2400 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [15, 54, 108, 126, 135, 141, 221, 234]: + self.enterOuterAlt(localctx, 1) + self.state = 2383 + self.joinType() + self.state = 2384 + self.match(SqlBaseParser.JOIN) + self.state = 2386 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,294,self._ctx) + if la_ == 1: + self.state = 2385 + self.match(SqlBaseParser.LATERAL) + + + self.state = 2388 + localctx.right = self.relationPrimary() + self.state = 2390 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,295,self._ctx) + if la_ == 1: + self.state = 2389 + self.joinCriteria() + + + pass + elif token in [170]: + self.enterOuterAlt(localctx, 2) + self.state = 2392 + self.match(SqlBaseParser.NATURAL) + self.state = 2393 + self.joinType() + self.state = 2394 + self.match(SqlBaseParser.JOIN) + self.state = 2396 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,296,self._ctx) + if la_ == 1: + self.state = 2395 + self.match(SqlBaseParser.LATERAL) + + + self.state = 2398 + localctx.right = self.relationPrimary() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class JoinTypeContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def INNER(self): + return self.getToken(SqlBaseParser.INNER, 0) + + def CROSS(self): + return self.getToken(SqlBaseParser.CROSS, 0) + + def LEFT(self): + return self.getToken(SqlBaseParser.LEFT, 0) + + def OUTER(self): + return self.getToken(SqlBaseParser.OUTER, 0) + + def SEMI(self): + return self.getToken(SqlBaseParser.SEMI, 0) + + def RIGHT(self): + return self.getToken(SqlBaseParser.RIGHT, 0) + + def FULL(self): + return self.getToken(SqlBaseParser.FULL, 0) + + def ANTI(self): + return self.getToken(SqlBaseParser.ANTI, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_joinType + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterJoinType" ): + listener.enterJoinType(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitJoinType" ): + listener.exitJoinType(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitJoinType" ): + return visitor.visitJoinType(self) + else: + return visitor.visitChildren(self) + + + + + def joinType(self): + + localctx = SqlBaseParser.JoinTypeContext(self, self._ctx, self.state) + self.enterRule(localctx, 182, self.RULE_joinType) + self._la = 0 # Token type + try: + self.state = 2426 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,304,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 2403 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==126: + self.state = 2402 + self.match(SqlBaseParser.INNER) + + + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 2405 + self.match(SqlBaseParser.CROSS) + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 2406 + self.match(SqlBaseParser.LEFT) + self.state = 2408 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==184: + self.state = 2407 + self.match(SqlBaseParser.OUTER) + + + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 2411 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==141: + self.state = 2410 + self.match(SqlBaseParser.LEFT) + + + self.state = 2413 + self.match(SqlBaseParser.SEMI) + pass + + elif la_ == 5: + self.enterOuterAlt(localctx, 5) + self.state = 2414 + self.match(SqlBaseParser.RIGHT) + self.state = 2416 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==184: + self.state = 2415 + self.match(SqlBaseParser.OUTER) + + + pass + + elif la_ == 6: + self.enterOuterAlt(localctx, 6) + self.state = 2418 + self.match(SqlBaseParser.FULL) + self.state = 2420 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==184: + self.state = 2419 + self.match(SqlBaseParser.OUTER) + + + pass + + elif la_ == 7: + self.enterOuterAlt(localctx, 7) + self.state = 2423 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==141: + self.state = 2422 + self.match(SqlBaseParser.LEFT) + + + self.state = 2425 + self.match(SqlBaseParser.ANTI) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class JoinCriteriaContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ON(self): + return self.getToken(SqlBaseParser.ON, 0) + + def booleanExpression(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanExpressionContext,0) + + + def USING(self): + return self.getToken(SqlBaseParser.USING, 0) + + def identifierList(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierListContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_joinCriteria + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterJoinCriteria" ): + listener.enterJoinCriteria(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitJoinCriteria" ): + listener.exitJoinCriteria(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitJoinCriteria" ): + return visitor.visitJoinCriteria(self) + else: + return visitor.visitChildren(self) + + + + + def joinCriteria(self): + + localctx = SqlBaseParser.JoinCriteriaContext(self, self._ctx, self.state) + self.enterRule(localctx, 184, self.RULE_joinCriteria) + try: + self.state = 2432 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [177]: + self.enterOuterAlt(localctx, 1) + self.state = 2428 + self.match(SqlBaseParser.ON) + self.state = 2429 + self.booleanExpression(0) + pass + elif token in [293]: + self.enterOuterAlt(localctx, 2) + self.state = 2430 + self.match(SqlBaseParser.USING) + self.state = 2431 + self.identifierList() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SampleContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.seed = None # Token + + def TABLESAMPLE(self): + return self.getToken(SqlBaseParser.TABLESAMPLE, 0) + + def LEFT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.LEFT_PAREN) + else: + return self.getToken(SqlBaseParser.LEFT_PAREN, i) + + def RIGHT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.RIGHT_PAREN) + else: + return self.getToken(SqlBaseParser.RIGHT_PAREN, i) + + def sampleMethod(self): + return self.getTypedRuleContext(SqlBaseParser.SampleMethodContext,0) + + + def REPEATABLE(self): + return self.getToken(SqlBaseParser.REPEATABLE, 0) + + def INTEGER_VALUE(self): + return self.getToken(SqlBaseParser.INTEGER_VALUE, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_sample + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSample" ): + listener.enterSample(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSample" ): + listener.exitSample(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSample" ): + return visitor.visitSample(self) + else: + return visitor.visitChildren(self) + + + + + def sample(self): + + localctx = SqlBaseParser.SampleContext(self, self._ctx, self.state) + self.enterRule(localctx, 186, self.RULE_sample) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2434 + self.match(SqlBaseParser.TABLESAMPLE) + self.state = 2435 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2437 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -252) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 8074954131875299327) != 0) or ((((_la - 321)) & ~0x3f) == 0 and ((1 << (_la - 321)) & 4193825) != 0): + self.state = 2436 + self.sampleMethod() + + + self.state = 2439 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 2444 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,307,self._ctx) + if la_ == 1: + self.state = 2440 + self.match(SqlBaseParser.REPEATABLE) + self.state = 2441 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2442 + localctx.seed = self.match(SqlBaseParser.INTEGER_VALUE) + self.state = 2443 + self.match(SqlBaseParser.RIGHT_PAREN) + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class SampleMethodContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_sampleMethod + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class SampleByRowsContext(SampleMethodContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.SampleMethodContext + super().__init__(parser) + self.copyFrom(ctx) + + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + def ROWS(self): + return self.getToken(SqlBaseParser.ROWS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSampleByRows" ): + listener.enterSampleByRows(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSampleByRows" ): + listener.exitSampleByRows(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSampleByRows" ): + return visitor.visitSampleByRows(self) + else: + return visitor.visitChildren(self) + + + class SampleByPercentileContext(SampleMethodContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.SampleMethodContext + super().__init__(parser) + self.negativeSign = None # Token + self.percentage = None # Token + self.copyFrom(ctx) + + def PERCENTLIT(self): + return self.getToken(SqlBaseParser.PERCENTLIT, 0) + def INTEGER_VALUE(self): + return self.getToken(SqlBaseParser.INTEGER_VALUE, 0) + def DECIMAL_VALUE(self): + return self.getToken(SqlBaseParser.DECIMAL_VALUE, 0) + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSampleByPercentile" ): + listener.enterSampleByPercentile(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSampleByPercentile" ): + listener.exitSampleByPercentile(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSampleByPercentile" ): + return visitor.visitSampleByPercentile(self) + else: + return visitor.visitChildren(self) + + + class SampleByBucketContext(SampleMethodContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.SampleMethodContext + super().__init__(parser) + self.sampleType = None # Token + self.numerator = None # Token + self.denominator = None # Token + self.copyFrom(ctx) + + def OUT(self): + return self.getToken(SqlBaseParser.OUT, 0) + def OF(self): + return self.getToken(SqlBaseParser.OF, 0) + def BUCKET(self): + return self.getToken(SqlBaseParser.BUCKET, 0) + def INTEGER_VALUE(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.INTEGER_VALUE) + else: + return self.getToken(SqlBaseParser.INTEGER_VALUE, i) + def ON(self): + return self.getToken(SqlBaseParser.ON, 0) + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + def qualifiedName(self): + return self.getTypedRuleContext(SqlBaseParser.QualifiedNameContext,0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSampleByBucket" ): + listener.enterSampleByBucket(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSampleByBucket" ): + listener.exitSampleByBucket(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSampleByBucket" ): + return visitor.visitSampleByBucket(self) + else: + return visitor.visitChildren(self) + + + class SampleByBytesContext(SampleMethodContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.SampleMethodContext + super().__init__(parser) + self.bytes = None # ExpressionContext + self.copyFrom(ctx) + + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSampleByBytes" ): + listener.enterSampleByBytes(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSampleByBytes" ): + listener.exitSampleByBytes(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSampleByBytes" ): + return visitor.visitSampleByBytes(self) + else: + return visitor.visitChildren(self) + + + + def sampleMethod(self): + + localctx = SqlBaseParser.SampleMethodContext(self, self._ctx, self.state) + self.enterRule(localctx, 188, self.RULE_sampleMethod) + self._la = 0 # Token type + try: + self.state = 2470 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,311,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.SampleByPercentileContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 2447 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==317: + self.state = 2446 + localctx.negativeSign = self.match(SqlBaseParser.MINUS) + + + self.state = 2449 + localctx.percentage = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==335 or _la==337): + localctx.percentage = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2450 + self.match(SqlBaseParser.PERCENTLIT) + pass + + elif la_ == 2: + localctx = SqlBaseParser.SampleByRowsContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 2451 + self.expression() + self.state = 2452 + self.match(SqlBaseParser.ROWS) + pass + + elif la_ == 3: + localctx = SqlBaseParser.SampleByBucketContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 2454 + localctx.sampleType = self.match(SqlBaseParser.BUCKET) + self.state = 2455 + localctx.numerator = self.match(SqlBaseParser.INTEGER_VALUE) + self.state = 2456 + self.match(SqlBaseParser.OUT) + self.state = 2457 + self.match(SqlBaseParser.OF) + self.state = 2458 + localctx.denominator = self.match(SqlBaseParser.INTEGER_VALUE) + self.state = 2467 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==177: + self.state = 2459 + self.match(SqlBaseParser.ON) + self.state = 2465 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,309,self._ctx) + if la_ == 1: + self.state = 2460 + self.identifier() + pass + + elif la_ == 2: + self.state = 2461 + self.qualifiedName() + self.state = 2462 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2463 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + + + + pass + + elif la_ == 4: + localctx = SqlBaseParser.SampleByBytesContext(self, localctx) + self.enterOuterAlt(localctx, 4) + self.state = 2469 + localctx.bytes = self.expression() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class IdentifierListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def identifierSeq(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierSeqContext,0) + + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_identifierList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIdentifierList" ): + listener.enterIdentifierList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIdentifierList" ): + listener.exitIdentifierList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitIdentifierList" ): + return visitor.visitIdentifierList(self) + else: + return visitor.visitChildren(self) + + + + + def identifierList(self): + + localctx = SqlBaseParser.IdentifierListContext(self, self._ctx, self.state) + self.enterRule(localctx, 190, self.RULE_identifierList) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2472 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2473 + self.identifierSeq() + self.state = 2474 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class IdentifierSeqContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._errorCapturingIdentifier = None # ErrorCapturingIdentifierContext + self.ident = list() # of ErrorCapturingIdentifierContexts + + def errorCapturingIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ErrorCapturingIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_identifierSeq + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIdentifierSeq" ): + listener.enterIdentifierSeq(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIdentifierSeq" ): + listener.exitIdentifierSeq(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitIdentifierSeq" ): + return visitor.visitIdentifierSeq(self) + else: + return visitor.visitChildren(self) + + + + + def identifierSeq(self): + + localctx = SqlBaseParser.IdentifierSeqContext(self, self._ctx, self.state) + self.enterRule(localctx, 192, self.RULE_identifierSeq) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2476 + localctx._errorCapturingIdentifier = self.errorCapturingIdentifier() + localctx.ident.append(localctx._errorCapturingIdentifier) + self.state = 2481 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,312,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 2477 + self.match(SqlBaseParser.COMMA) + self.state = 2478 + localctx._errorCapturingIdentifier = self.errorCapturingIdentifier() + localctx.ident.append(localctx._errorCapturingIdentifier) + self.state = 2483 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,312,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OrderedIdentifierListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def orderedIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.OrderedIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.OrderedIdentifierContext,i) + + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_orderedIdentifierList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOrderedIdentifierList" ): + listener.enterOrderedIdentifierList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOrderedIdentifierList" ): + listener.exitOrderedIdentifierList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOrderedIdentifierList" ): + return visitor.visitOrderedIdentifierList(self) + else: + return visitor.visitChildren(self) + + + + + def orderedIdentifierList(self): + + localctx = SqlBaseParser.OrderedIdentifierListContext(self, self._ctx, self.state) + self.enterRule(localctx, 194, self.RULE_orderedIdentifierList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2484 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2485 + self.orderedIdentifier() + self.state = 2490 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2486 + self.match(SqlBaseParser.COMMA) + self.state = 2487 + self.orderedIdentifier() + self.state = 2492 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2493 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class OrderedIdentifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.ident = None # ErrorCapturingIdentifierContext + self.ordering = None # Token + + def errorCapturingIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,0) + + + def ASC(self): + return self.getToken(SqlBaseParser.ASC, 0) + + def DESC(self): + return self.getToken(SqlBaseParser.DESC, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_orderedIdentifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOrderedIdentifier" ): + listener.enterOrderedIdentifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOrderedIdentifier" ): + listener.exitOrderedIdentifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOrderedIdentifier" ): + return visitor.visitOrderedIdentifier(self) + else: + return visitor.visitChildren(self) + + + + + def orderedIdentifier(self): + + localctx = SqlBaseParser.OrderedIdentifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 196, self.RULE_orderedIdentifier) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2495 + localctx.ident = self.errorCapturingIdentifier() + self.state = 2497 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==21 or _la==74: + self.state = 2496 + localctx.ordering = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==21 or _la==74): + localctx.ordering = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class IdentifierCommentListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def identifierComment(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.IdentifierCommentContext) + else: + return self.getTypedRuleContext(SqlBaseParser.IdentifierCommentContext,i) + + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_identifierCommentList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIdentifierCommentList" ): + listener.enterIdentifierCommentList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIdentifierCommentList" ): + listener.exitIdentifierCommentList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitIdentifierCommentList" ): + return visitor.visitIdentifierCommentList(self) + else: + return visitor.visitChildren(self) + + + + + def identifierCommentList(self): + + localctx = SqlBaseParser.IdentifierCommentListContext(self, self._ctx, self.state) + self.enterRule(localctx, 198, self.RULE_identifierCommentList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2499 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2500 + self.identifierComment() + self.state = 2505 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2501 + self.match(SqlBaseParser.COMMA) + self.state = 2502 + self.identifierComment() + self.state = 2507 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2508 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class IdentifierCommentContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def commentSpec(self): + return self.getTypedRuleContext(SqlBaseParser.CommentSpecContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_identifierComment + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIdentifierComment" ): + listener.enterIdentifierComment(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIdentifierComment" ): + listener.exitIdentifierComment(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitIdentifierComment" ): + return visitor.visitIdentifierComment(self) + else: + return visitor.visitChildren(self) + + + + + def identifierComment(self): + + localctx = SqlBaseParser.IdentifierCommentContext(self, self._ctx, self.state) + self.enterRule(localctx, 200, self.RULE_identifierComment) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2510 + self.identifier() + self.state = 2512 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==45: + self.state = 2511 + self.commentSpec() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class RelationPrimaryContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_relationPrimary + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class TableValuedFunctionContext(RelationPrimaryContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.RelationPrimaryContext + super().__init__(parser) + self.copyFrom(ctx) + + def functionTable(self): + return self.getTypedRuleContext(SqlBaseParser.FunctionTableContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTableValuedFunction" ): + listener.enterTableValuedFunction(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTableValuedFunction" ): + listener.exitTableValuedFunction(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTableValuedFunction" ): + return visitor.visitTableValuedFunction(self) + else: + return visitor.visitChildren(self) + + + class InlineTableDefault2Context(RelationPrimaryContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.RelationPrimaryContext + super().__init__(parser) + self.copyFrom(ctx) + + def inlineTable(self): + return self.getTypedRuleContext(SqlBaseParser.InlineTableContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInlineTableDefault2" ): + listener.enterInlineTableDefault2(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInlineTableDefault2" ): + listener.exitInlineTableDefault2(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitInlineTableDefault2" ): + return visitor.visitInlineTableDefault2(self) + else: + return visitor.visitChildren(self) + + + class AliasedRelationContext(RelationPrimaryContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.RelationPrimaryContext + super().__init__(parser) + self.copyFrom(ctx) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def relation(self): + return self.getTypedRuleContext(SqlBaseParser.RelationContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def tableAlias(self): + return self.getTypedRuleContext(SqlBaseParser.TableAliasContext,0) + + def sample(self): + return self.getTypedRuleContext(SqlBaseParser.SampleContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAliasedRelation" ): + listener.enterAliasedRelation(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAliasedRelation" ): + listener.exitAliasedRelation(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAliasedRelation" ): + return visitor.visitAliasedRelation(self) + else: + return visitor.visitChildren(self) + + + class AliasedQueryContext(RelationPrimaryContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.RelationPrimaryContext + super().__init__(parser) + self.copyFrom(ctx) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def tableAlias(self): + return self.getTypedRuleContext(SqlBaseParser.TableAliasContext,0) + + def sample(self): + return self.getTypedRuleContext(SqlBaseParser.SampleContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAliasedQuery" ): + listener.enterAliasedQuery(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAliasedQuery" ): + listener.exitAliasedQuery(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAliasedQuery" ): + return visitor.visitAliasedQuery(self) + else: + return visitor.visitChildren(self) + + + class TableNameContext(RelationPrimaryContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.RelationPrimaryContext + super().__init__(parser) + self.copyFrom(ctx) + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + def tableAlias(self): + return self.getTypedRuleContext(SqlBaseParser.TableAliasContext,0) + + def temporalClause(self): + return self.getTypedRuleContext(SqlBaseParser.TemporalClauseContext,0) + + def sample(self): + return self.getTypedRuleContext(SqlBaseParser.SampleContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTableName" ): + listener.enterTableName(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTableName" ): + listener.exitTableName(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTableName" ): + return visitor.visitTableName(self) + else: + return visitor.visitChildren(self) + + + + def relationPrimary(self): + + localctx = SqlBaseParser.RelationPrimaryContext(self, self._ctx, self.state) + self.enterRule(localctx, 202, self.RULE_relationPrimary) + try: + self.state = 2541 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,321,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.TableNameContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 2514 + self.multipartIdentifier() + self.state = 2516 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,317,self._ctx) + if la_ == 1: + self.state = 2515 + self.temporalClause() + + + self.state = 2519 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,318,self._ctx) + if la_ == 1: + self.state = 2518 + self.sample() + + + self.state = 2521 + self.tableAlias() + pass + + elif la_ == 2: + localctx = SqlBaseParser.AliasedQueryContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 2523 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2524 + self.query() + self.state = 2525 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 2527 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,319,self._ctx) + if la_ == 1: + self.state = 2526 + self.sample() + + + self.state = 2529 + self.tableAlias() + pass + + elif la_ == 3: + localctx = SqlBaseParser.AliasedRelationContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 2531 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2532 + self.relation() + self.state = 2533 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 2535 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,320,self._ctx) + if la_ == 1: + self.state = 2534 + self.sample() + + + self.state = 2537 + self.tableAlias() + pass + + elif la_ == 4: + localctx = SqlBaseParser.InlineTableDefault2Context(self, localctx) + self.enterOuterAlt(localctx, 4) + self.state = 2539 + self.inlineTable() + pass + + elif la_ == 5: + localctx = SqlBaseParser.TableValuedFunctionContext(self, localctx) + self.enterOuterAlt(localctx, 5) + self.state = 2540 + self.functionTable() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class InlineTableContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def VALUES(self): + return self.getToken(SqlBaseParser.VALUES, 0) + + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + + def tableAlias(self): + return self.getTypedRuleContext(SqlBaseParser.TableAliasContext,0) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_inlineTable + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInlineTable" ): + listener.enterInlineTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInlineTable" ): + listener.exitInlineTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitInlineTable" ): + return visitor.visitInlineTable(self) + else: + return visitor.visitChildren(self) + + + + + def inlineTable(self): + + localctx = SqlBaseParser.InlineTableContext(self, self._ctx, self.state) + self.enterRule(localctx, 204, self.RULE_inlineTable) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2543 + self.match(SqlBaseParser.VALUES) + self.state = 2544 + self.expression() + self.state = 2549 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,322,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 2545 + self.match(SqlBaseParser.COMMA) + self.state = 2546 + self.expression() + self.state = 2551 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,322,self._ctx) + + self.state = 2552 + self.tableAlias() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FunctionTableContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.funcName = None # FunctionNameContext + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def tableAlias(self): + return self.getTypedRuleContext(SqlBaseParser.TableAliasContext,0) + + + def functionName(self): + return self.getTypedRuleContext(SqlBaseParser.FunctionNameContext,0) + + + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_functionTable + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFunctionTable" ): + listener.enterFunctionTable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFunctionTable" ): + listener.exitFunctionTable(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFunctionTable" ): + return visitor.visitFunctionTable(self) + else: + return visitor.visitChildren(self) + + + + + def functionTable(self): + + localctx = SqlBaseParser.FunctionTableContext(self, self._ctx, self.state) + self.enterRule(localctx, 206, self.RULE_functionTable) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2554 + localctx.funcName = self.functionName() + self.state = 2555 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2564 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -252) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 8074954131875299327) != 0) or ((((_la - 321)) & ~0x3f) == 0 and ((1 << (_la - 321)) & 4193825) != 0): + self.state = 2556 + self.expression() + self.state = 2561 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2557 + self.match(SqlBaseParser.COMMA) + self.state = 2558 + self.expression() + self.state = 2563 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + self.state = 2566 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 2567 + self.tableAlias() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class TableAliasContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def strictIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.StrictIdentifierContext,0) + + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def identifierList(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierListContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_tableAlias + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTableAlias" ): + listener.enterTableAlias(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTableAlias" ): + listener.exitTableAlias(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTableAlias" ): + return visitor.visitTableAlias(self) + else: + return visitor.visitChildren(self) + + + + + def tableAlias(self): + + localctx = SqlBaseParser.TableAliasContext(self, self._ctx, self.state) + self.enterRule(localctx, 208, self.RULE_tableAlias) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2576 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,327,self._ctx) + if la_ == 1: + self.state = 2570 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,325,self._ctx) + if la_ == 1: + self.state = 2569 + self.match(SqlBaseParser.AS) + + + self.state = 2572 + self.strictIdentifier() + self.state = 2574 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,326,self._ctx) + if la_ == 1: + self.state = 2573 + self.identifierList() + + + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class RowFormatContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_rowFormat + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class RowFormatSerdeContext(RowFormatContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.RowFormatContext + super().__init__(parser) + self.name = None # StringLitContext + self.props = None # PropertyListContext + self.copyFrom(ctx) + + def ROW(self): + return self.getToken(SqlBaseParser.ROW, 0) + def FORMAT(self): + return self.getToken(SqlBaseParser.FORMAT, 0) + def SERDE(self): + return self.getToken(SqlBaseParser.SERDE, 0) + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + def WITH(self): + return self.getToken(SqlBaseParser.WITH, 0) + def SERDEPROPERTIES(self): + return self.getToken(SqlBaseParser.SERDEPROPERTIES, 0) + def propertyList(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRowFormatSerde" ): + listener.enterRowFormatSerde(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRowFormatSerde" ): + listener.exitRowFormatSerde(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRowFormatSerde" ): + return visitor.visitRowFormatSerde(self) + else: + return visitor.visitChildren(self) + + + class RowFormatDelimitedContext(RowFormatContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.RowFormatContext + super().__init__(parser) + self.fieldsTerminatedBy = None # StringLitContext + self.escapedBy = None # StringLitContext + self.collectionItemsTerminatedBy = None # StringLitContext + self.keysTerminatedBy = None # StringLitContext + self.linesSeparatedBy = None # StringLitContext + self.nullDefinedAs = None # StringLitContext + self.copyFrom(ctx) + + def ROW(self): + return self.getToken(SqlBaseParser.ROW, 0) + def FORMAT(self): + return self.getToken(SqlBaseParser.FORMAT, 0) + def DELIMITED(self): + return self.getToken(SqlBaseParser.DELIMITED, 0) + def FIELDS(self): + return self.getToken(SqlBaseParser.FIELDS, 0) + def TERMINATED(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.TERMINATED) + else: + return self.getToken(SqlBaseParser.TERMINATED, i) + def BY(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.BY) + else: + return self.getToken(SqlBaseParser.BY, i) + def COLLECTION(self): + return self.getToken(SqlBaseParser.COLLECTION, 0) + def ITEMS(self): + return self.getToken(SqlBaseParser.ITEMS, 0) + def MAP(self): + return self.getToken(SqlBaseParser.MAP, 0) + def KEYS(self): + return self.getToken(SqlBaseParser.KEYS, 0) + def LINES(self): + return self.getToken(SqlBaseParser.LINES, 0) + def NULL(self): + return self.getToken(SqlBaseParser.NULL, 0) + def DEFINED(self): + return self.getToken(SqlBaseParser.DEFINED, 0) + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + def stringLit(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.StringLitContext) + else: + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,i) + + def ESCAPED(self): + return self.getToken(SqlBaseParser.ESCAPED, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRowFormatDelimited" ): + listener.enterRowFormatDelimited(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRowFormatDelimited" ): + listener.exitRowFormatDelimited(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRowFormatDelimited" ): + return visitor.visitRowFormatDelimited(self) + else: + return visitor.visitChildren(self) + + + + def rowFormat(self): + + localctx = SqlBaseParser.RowFormatContext(self, self._ctx, self.state) + self.enterRule(localctx, 210, self.RULE_rowFormat) + try: + self.state = 2627 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,335,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.RowFormatSerdeContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 2578 + self.match(SqlBaseParser.ROW) + self.state = 2579 + self.match(SqlBaseParser.FORMAT) + self.state = 2580 + self.match(SqlBaseParser.SERDE) + self.state = 2581 + localctx.name = self.stringLit() + self.state = 2585 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,328,self._ctx) + if la_ == 1: + self.state = 2582 + self.match(SqlBaseParser.WITH) + self.state = 2583 + self.match(SqlBaseParser.SERDEPROPERTIES) + self.state = 2584 + localctx.props = self.propertyList() + + + pass + + elif la_ == 2: + localctx = SqlBaseParser.RowFormatDelimitedContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 2587 + self.match(SqlBaseParser.ROW) + self.state = 2588 + self.match(SqlBaseParser.FORMAT) + self.state = 2589 + self.match(SqlBaseParser.DELIMITED) + self.state = 2599 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,330,self._ctx) + if la_ == 1: + self.state = 2590 + self.match(SqlBaseParser.FIELDS) + self.state = 2591 + self.match(SqlBaseParser.TERMINATED) + self.state = 2592 + self.match(SqlBaseParser.BY) + self.state = 2593 + localctx.fieldsTerminatedBy = self.stringLit() + self.state = 2597 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,329,self._ctx) + if la_ == 1: + self.state = 2594 + self.match(SqlBaseParser.ESCAPED) + self.state = 2595 + self.match(SqlBaseParser.BY) + self.state = 2596 + localctx.escapedBy = self.stringLit() + + + + + self.state = 2606 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,331,self._ctx) + if la_ == 1: + self.state = 2601 + self.match(SqlBaseParser.COLLECTION) + self.state = 2602 + self.match(SqlBaseParser.ITEMS) + self.state = 2603 + self.match(SqlBaseParser.TERMINATED) + self.state = 2604 + self.match(SqlBaseParser.BY) + self.state = 2605 + localctx.collectionItemsTerminatedBy = self.stringLit() + + + self.state = 2613 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,332,self._ctx) + if la_ == 1: + self.state = 2608 + self.match(SqlBaseParser.MAP) + self.state = 2609 + self.match(SqlBaseParser.KEYS) + self.state = 2610 + self.match(SqlBaseParser.TERMINATED) + self.state = 2611 + self.match(SqlBaseParser.BY) + self.state = 2612 + localctx.keysTerminatedBy = self.stringLit() + + + self.state = 2619 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,333,self._ctx) + if la_ == 1: + self.state = 2615 + self.match(SqlBaseParser.LINES) + self.state = 2616 + self.match(SqlBaseParser.TERMINATED) + self.state = 2617 + self.match(SqlBaseParser.BY) + self.state = 2618 + localctx.linesSeparatedBy = self.stringLit() + + + self.state = 2625 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,334,self._ctx) + if la_ == 1: + self.state = 2621 + self.match(SqlBaseParser.NULL) + self.state = 2622 + self.match(SqlBaseParser.DEFINED) + self.state = 2623 + self.match(SqlBaseParser.AS) + self.state = 2624 + localctx.nullDefinedAs = self.stringLit() + + + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MultipartIdentifierListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def multipartIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.MultipartIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_multipartIdentifierList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMultipartIdentifierList" ): + listener.enterMultipartIdentifierList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMultipartIdentifierList" ): + listener.exitMultipartIdentifierList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMultipartIdentifierList" ): + return visitor.visitMultipartIdentifierList(self) + else: + return visitor.visitChildren(self) + + + + + def multipartIdentifierList(self): + + localctx = SqlBaseParser.MultipartIdentifierListContext(self, self._ctx, self.state) + self.enterRule(localctx, 212, self.RULE_multipartIdentifierList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2629 + self.multipartIdentifier() + self.state = 2634 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2630 + self.match(SqlBaseParser.COMMA) + self.state = 2631 + self.multipartIdentifier() + self.state = 2636 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MultipartIdentifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._errorCapturingIdentifier = None # ErrorCapturingIdentifierContext + self.parts = list() # of ErrorCapturingIdentifierContexts + + def errorCapturingIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ErrorCapturingIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,i) + + + def DOT(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.DOT) + else: + return self.getToken(SqlBaseParser.DOT, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_multipartIdentifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMultipartIdentifier" ): + listener.enterMultipartIdentifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMultipartIdentifier" ): + listener.exitMultipartIdentifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMultipartIdentifier" ): + return visitor.visitMultipartIdentifier(self) + else: + return visitor.visitChildren(self) + + + + + def multipartIdentifier(self): + + localctx = SqlBaseParser.MultipartIdentifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 214, self.RULE_multipartIdentifier) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2637 + localctx._errorCapturingIdentifier = self.errorCapturingIdentifier() + localctx.parts.append(localctx._errorCapturingIdentifier) + self.state = 2642 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,337,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 2638 + self.match(SqlBaseParser.DOT) + self.state = 2639 + localctx._errorCapturingIdentifier = self.errorCapturingIdentifier() + localctx.parts.append(localctx._errorCapturingIdentifier) + self.state = 2644 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,337,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MultipartIdentifierPropertyListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def multipartIdentifierProperty(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.MultipartIdentifierPropertyContext) + else: + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierPropertyContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_multipartIdentifierPropertyList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMultipartIdentifierPropertyList" ): + listener.enterMultipartIdentifierPropertyList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMultipartIdentifierPropertyList" ): + listener.exitMultipartIdentifierPropertyList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMultipartIdentifierPropertyList" ): + return visitor.visitMultipartIdentifierPropertyList(self) + else: + return visitor.visitChildren(self) + + + + + def multipartIdentifierPropertyList(self): + + localctx = SqlBaseParser.MultipartIdentifierPropertyListContext(self, self._ctx, self.state) + self.enterRule(localctx, 216, self.RULE_multipartIdentifierPropertyList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2645 + self.multipartIdentifierProperty() + self.state = 2650 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2646 + self.match(SqlBaseParser.COMMA) + self.state = 2647 + self.multipartIdentifierProperty() + self.state = 2652 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MultipartIdentifierPropertyContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.options = None # PropertyListContext + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def OPTIONS(self): + return self.getToken(SqlBaseParser.OPTIONS, 0) + + def propertyList(self): + return self.getTypedRuleContext(SqlBaseParser.PropertyListContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_multipartIdentifierProperty + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMultipartIdentifierProperty" ): + listener.enterMultipartIdentifierProperty(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMultipartIdentifierProperty" ): + listener.exitMultipartIdentifierProperty(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMultipartIdentifierProperty" ): + return visitor.visitMultipartIdentifierProperty(self) + else: + return visitor.visitChildren(self) + + + + + def multipartIdentifierProperty(self): + + localctx = SqlBaseParser.MultipartIdentifierPropertyContext(self, self._ctx, self.state) + self.enterRule(localctx, 218, self.RULE_multipartIdentifierProperty) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2653 + self.multipartIdentifier() + self.state = 2656 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==180: + self.state = 2654 + self.match(SqlBaseParser.OPTIONS) + self.state = 2655 + localctx.options = self.propertyList() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class TableIdentifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.db = None # ErrorCapturingIdentifierContext + self.table = None # ErrorCapturingIdentifierContext + + def errorCapturingIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ErrorCapturingIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,i) + + + def DOT(self): + return self.getToken(SqlBaseParser.DOT, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_tableIdentifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTableIdentifier" ): + listener.enterTableIdentifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTableIdentifier" ): + listener.exitTableIdentifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTableIdentifier" ): + return visitor.visitTableIdentifier(self) + else: + return visitor.visitChildren(self) + + + + + def tableIdentifier(self): + + localctx = SqlBaseParser.TableIdentifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 220, self.RULE_tableIdentifier) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2661 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,340,self._ctx) + if la_ == 1: + self.state = 2658 + localctx.db = self.errorCapturingIdentifier() + self.state = 2659 + self.match(SqlBaseParser.DOT) + + + self.state = 2663 + localctx.table = self.errorCapturingIdentifier() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FunctionIdentifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.db = None # ErrorCapturingIdentifierContext + self.function = None # ErrorCapturingIdentifierContext + + def errorCapturingIdentifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ErrorCapturingIdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,i) + + + def DOT(self): + return self.getToken(SqlBaseParser.DOT, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_functionIdentifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFunctionIdentifier" ): + listener.enterFunctionIdentifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFunctionIdentifier" ): + listener.exitFunctionIdentifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFunctionIdentifier" ): + return visitor.visitFunctionIdentifier(self) + else: + return visitor.visitChildren(self) + + + + + def functionIdentifier(self): + + localctx = SqlBaseParser.FunctionIdentifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 222, self.RULE_functionIdentifier) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2668 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,341,self._ctx) + if la_ == 1: + self.state = 2665 + localctx.db = self.errorCapturingIdentifier() + self.state = 2666 + self.match(SqlBaseParser.DOT) + + + self.state = 2670 + localctx.function = self.errorCapturingIdentifier() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NamedExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.name = None # ErrorCapturingIdentifierContext + + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + + def identifierList(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierListContext,0) + + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def errorCapturingIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_namedExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNamedExpression" ): + listener.enterNamedExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNamedExpression" ): + listener.exitNamedExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNamedExpression" ): + return visitor.visitNamedExpression(self) + else: + return visitor.visitChildren(self) + + + + + def namedExpression(self): + + localctx = SqlBaseParser.NamedExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 224, self.RULE_namedExpression) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2672 + self.expression() + self.state = 2680 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,344,self._ctx) + if la_ == 1: + self.state = 2674 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,342,self._ctx) + if la_ == 1: + self.state = 2673 + self.match(SqlBaseParser.AS) + + + self.state = 2678 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 331, 341, 342]: + self.state = 2676 + localctx.name = self.errorCapturingIdentifier() + pass + elif token in [2]: + self.state = 2677 + self.identifierList() + pass + else: + raise NoViableAltException(self) + + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NamedExpressionSeqContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def namedExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.NamedExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.NamedExpressionContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_namedExpressionSeq + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNamedExpressionSeq" ): + listener.enterNamedExpressionSeq(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNamedExpressionSeq" ): + listener.exitNamedExpressionSeq(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNamedExpressionSeq" ): + return visitor.visitNamedExpressionSeq(self) + else: + return visitor.visitChildren(self) + + + + + def namedExpressionSeq(self): + + localctx = SqlBaseParser.NamedExpressionSeqContext(self, self._ctx, self.state) + self.enterRule(localctx, 226, self.RULE_namedExpressionSeq) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2682 + self.namedExpression() + self.state = 2687 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,345,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 2683 + self.match(SqlBaseParser.COMMA) + self.state = 2684 + self.namedExpression() + self.state = 2689 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,345,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PartitionFieldListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._partitionField = None # PartitionFieldContext + self.fields = list() # of PartitionFieldContexts + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def partitionField(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.PartitionFieldContext) + else: + return self.getTypedRuleContext(SqlBaseParser.PartitionFieldContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_partitionFieldList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPartitionFieldList" ): + listener.enterPartitionFieldList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPartitionFieldList" ): + listener.exitPartitionFieldList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPartitionFieldList" ): + return visitor.visitPartitionFieldList(self) + else: + return visitor.visitChildren(self) + + + + + def partitionFieldList(self): + + localctx = SqlBaseParser.PartitionFieldListContext(self, self._ctx, self.state) + self.enterRule(localctx, 228, self.RULE_partitionFieldList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2690 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2691 + localctx._partitionField = self.partitionField() + localctx.fields.append(localctx._partitionField) + self.state = 2696 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2692 + self.match(SqlBaseParser.COMMA) + self.state = 2693 + localctx._partitionField = self.partitionField() + localctx.fields.append(localctx._partitionField) + self.state = 2698 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2699 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PartitionFieldContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_partitionField + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class PartitionColumnContext(PartitionFieldContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PartitionFieldContext + super().__init__(parser) + self.copyFrom(ctx) + + def colType(self): + return self.getTypedRuleContext(SqlBaseParser.ColTypeContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPartitionColumn" ): + listener.enterPartitionColumn(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPartitionColumn" ): + listener.exitPartitionColumn(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPartitionColumn" ): + return visitor.visitPartitionColumn(self) + else: + return visitor.visitChildren(self) + + + class PartitionTransformContext(PartitionFieldContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PartitionFieldContext + super().__init__(parser) + self.copyFrom(ctx) + + def transform(self): + return self.getTypedRuleContext(SqlBaseParser.TransformContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPartitionTransform" ): + listener.enterPartitionTransform(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPartitionTransform" ): + listener.exitPartitionTransform(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPartitionTransform" ): + return visitor.visitPartitionTransform(self) + else: + return visitor.visitChildren(self) + + + + def partitionField(self): + + localctx = SqlBaseParser.PartitionFieldContext(self, self._ctx, self.state) + self.enterRule(localctx, 230, self.RULE_partitionField) + try: + self.state = 2703 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,347,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.PartitionTransformContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 2701 + self.transform() + pass + + elif la_ == 2: + localctx = SqlBaseParser.PartitionColumnContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 2702 + self.colType() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class TransformContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_transform + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class IdentityTransformContext(TransformContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.TransformContext + super().__init__(parser) + self.copyFrom(ctx) + + def qualifiedName(self): + return self.getTypedRuleContext(SqlBaseParser.QualifiedNameContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIdentityTransform" ): + listener.enterIdentityTransform(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIdentityTransform" ): + listener.exitIdentityTransform(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitIdentityTransform" ): + return visitor.visitIdentityTransform(self) + else: + return visitor.visitChildren(self) + + + class ApplyTransformContext(TransformContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.TransformContext + super().__init__(parser) + self.transformName = None # IdentifierContext + self._transformArgument = None # TransformArgumentContext + self.argument = list() # of TransformArgumentContexts + self.copyFrom(ctx) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + def transformArgument(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.TransformArgumentContext) + else: + return self.getTypedRuleContext(SqlBaseParser.TransformArgumentContext,i) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterApplyTransform" ): + listener.enterApplyTransform(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitApplyTransform" ): + listener.exitApplyTransform(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitApplyTransform" ): + return visitor.visitApplyTransform(self) + else: + return visitor.visitChildren(self) + + + + def transform(self): + + localctx = SqlBaseParser.TransformContext(self, self._ctx, self.state) + self.enterRule(localctx, 232, self.RULE_transform) + self._la = 0 # Token type + try: + self.state = 2718 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,349,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.IdentityTransformContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 2705 + self.qualifiedName() + pass + + elif la_ == 2: + localctx = SqlBaseParser.ApplyTransformContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 2706 + localctx.transformName = self.identifier() + self.state = 2707 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2708 + localctx._transformArgument = self.transformArgument() + localctx.argument.append(localctx._transformArgument) + self.state = 2713 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2709 + self.match(SqlBaseParser.COMMA) + self.state = 2710 + localctx._transformArgument = self.transformArgument() + localctx.argument.append(localctx._transformArgument) + self.state = 2715 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2716 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class TransformArgumentContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def qualifiedName(self): + return self.getTypedRuleContext(SqlBaseParser.QualifiedNameContext,0) + + + def constant(self): + return self.getTypedRuleContext(SqlBaseParser.ConstantContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_transformArgument + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTransformArgument" ): + listener.enterTransformArgument(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTransformArgument" ): + listener.exitTransformArgument(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTransformArgument" ): + return visitor.visitTransformArgument(self) + else: + return visitor.visitChildren(self) + + + + + def transformArgument(self): + + localctx = SqlBaseParser.TransformArgumentContext(self, self._ctx, self.state) + self.enterRule(localctx, 234, self.RULE_transformArgument) + try: + self.state = 2722 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,350,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 2720 + self.qualifiedName() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 2721 + self.constant() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def booleanExpression(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanExpressionContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_expression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterExpression" ): + listener.enterExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitExpression" ): + listener.exitExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitExpression" ): + return visitor.visitExpression(self) + else: + return visitor.visitChildren(self) + + + + + def expression(self): + + localctx = SqlBaseParser.ExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 236, self.RULE_expression) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2724 + self.booleanExpression(0) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ExpressionSeqContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_expressionSeq + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterExpressionSeq" ): + listener.enterExpressionSeq(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitExpressionSeq" ): + listener.exitExpressionSeq(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitExpressionSeq" ): + return visitor.visitExpressionSeq(self) + else: + return visitor.visitChildren(self) + + + + + def expressionSeq(self): + + localctx = SqlBaseParser.ExpressionSeqContext(self, self._ctx, self.state) + self.enterRule(localctx, 238, self.RULE_expressionSeq) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2726 + self.expression() + self.state = 2731 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2727 + self.match(SqlBaseParser.COMMA) + self.state = 2728 + self.expression() + self.state = 2733 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class BooleanExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_booleanExpression + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + class LogicalNotContext(BooleanExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.BooleanExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + def booleanExpression(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanExpressionContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterLogicalNot" ): + listener.enterLogicalNot(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitLogicalNot" ): + listener.exitLogicalNot(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitLogicalNot" ): + return visitor.visitLogicalNot(self) + else: + return visitor.visitChildren(self) + + + class PredicatedContext(BooleanExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.BooleanExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def valueExpression(self): + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,0) + + def predicate(self): + return self.getTypedRuleContext(SqlBaseParser.PredicateContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPredicated" ): + listener.enterPredicated(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPredicated" ): + listener.exitPredicated(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPredicated" ): + return visitor.visitPredicated(self) + else: + return visitor.visitChildren(self) + + + class ExistsContext(BooleanExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.BooleanExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterExists" ): + listener.enterExists(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitExists" ): + listener.exitExists(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitExists" ): + return visitor.visitExists(self) + else: + return visitor.visitChildren(self) + + + class LogicalBinaryContext(BooleanExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.BooleanExpressionContext + super().__init__(parser) + self.left = None # BooleanExpressionContext + self.operator = None # Token + self.right = None # BooleanExpressionContext + self.copyFrom(ctx) + + def booleanExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.BooleanExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.BooleanExpressionContext,i) + + def AND(self): + return self.getToken(SqlBaseParser.AND, 0) + def OR(self): + return self.getToken(SqlBaseParser.OR, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterLogicalBinary" ): + listener.enterLogicalBinary(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitLogicalBinary" ): + listener.exitLogicalBinary(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitLogicalBinary" ): + return visitor.visitLogicalBinary(self) + else: + return visitor.visitChildren(self) + + + + def booleanExpression(self, _p:int=0): + _parentctx = self._ctx + _parentState = self.state + localctx = SqlBaseParser.BooleanExpressionContext(self, self._ctx, _parentState) + _prevctx = localctx + _startState = 240 + self.enterRecursionRule(localctx, 240, self.RULE_booleanExpression, _p) + try: + self.enterOuterAlt(localctx, 1) + self.state = 2746 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,353,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.LogicalNotContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + + self.state = 2735 + self.match(SqlBaseParser.NOT) + self.state = 2736 + self.booleanExpression(5) + pass + + elif la_ == 2: + localctx = SqlBaseParser.ExistsContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2737 + self.match(SqlBaseParser.EXISTS) + self.state = 2738 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2739 + self.query() + self.state = 2740 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 3: + localctx = SqlBaseParser.PredicatedContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2742 + self.valueExpression(0) + self.state = 2744 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,352,self._ctx) + if la_ == 1: + self.state = 2743 + self.predicate() + + + pass + + + self._ctx.stop = self._input.LT(-1) + self.state = 2756 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,355,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + if self._parseListeners is not None: + self.triggerExitRuleEvent() + _prevctx = localctx + self.state = 2754 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,354,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.LogicalBinaryContext(self, SqlBaseParser.BooleanExpressionContext(self, _parentctx, _parentState)) + localctx.left = _prevctx + self.pushNewRecursionContext(localctx, _startState, self.RULE_booleanExpression) + self.state = 2748 + if not self.precpred(self._ctx, 2): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 2)") + self.state = 2749 + localctx.operator = self.match(SqlBaseParser.AND) + self.state = 2750 + localctx.right = self.booleanExpression(3) + pass + + elif la_ == 2: + localctx = SqlBaseParser.LogicalBinaryContext(self, SqlBaseParser.BooleanExpressionContext(self, _parentctx, _parentState)) + localctx.left = _prevctx + self.pushNewRecursionContext(localctx, _startState, self.RULE_booleanExpression) + self.state = 2751 + if not self.precpred(self._ctx, 1): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 1)") + self.state = 2752 + localctx.operator = self.match(SqlBaseParser.OR) + self.state = 2753 + localctx.right = self.booleanExpression(2) + pass + + + self.state = 2758 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,355,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.unrollRecursionContexts(_parentctx) + return localctx + + + class PredicateContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.kind = None # Token + self.lower = None # ValueExpressionContext + self.upper = None # ValueExpressionContext + self.pattern = None # ValueExpressionContext + self.quantifier = None # Token + self.escapeChar = None # StringLitContext + self.right = None # ValueExpressionContext + + def AND(self): + return self.getToken(SqlBaseParser.AND, 0) + + def BETWEEN(self): + return self.getToken(SqlBaseParser.BETWEEN, 0) + + def valueExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ValueExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,i) + + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + + def RLIKE(self): + return self.getToken(SqlBaseParser.RLIKE, 0) + + def LIKE(self): + return self.getToken(SqlBaseParser.LIKE, 0) + + def ILIKE(self): + return self.getToken(SqlBaseParser.ILIKE, 0) + + def ANY(self): + return self.getToken(SqlBaseParser.ANY, 0) + + def SOME(self): + return self.getToken(SqlBaseParser.SOME, 0) + + def ALL(self): + return self.getToken(SqlBaseParser.ALL, 0) + + def ESCAPE(self): + return self.getToken(SqlBaseParser.ESCAPE, 0) + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def IS(self): + return self.getToken(SqlBaseParser.IS, 0) + + def NULL(self): + return self.getToken(SqlBaseParser.NULL, 0) + + def TRUE(self): + return self.getToken(SqlBaseParser.TRUE, 0) + + def FALSE(self): + return self.getToken(SqlBaseParser.FALSE, 0) + + def UNKNOWN(self): + return self.getToken(SqlBaseParser.UNKNOWN, 0) + + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + + def DISTINCT(self): + return self.getToken(SqlBaseParser.DISTINCT, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_predicate + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPredicate" ): + listener.enterPredicate(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPredicate" ): + listener.exitPredicate(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPredicate" ): + return visitor.visitPredicate(self) + else: + return visitor.visitChildren(self) + + + + + def predicate(self): + + localctx = SqlBaseParser.PredicateContext(self, self._ctx, self.state) + self.enterRule(localctx, 242, self.RULE_predicate) + self._la = 0 # Token type + try: + self.state = 2841 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,369,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 2760 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==172: + self.state = 2759 + self.match(SqlBaseParser.NOT) + + + self.state = 2762 + localctx.kind = self.match(SqlBaseParser.BETWEEN) + self.state = 2763 + localctx.lower = self.valueExpression(0) + self.state = 2764 + self.match(SqlBaseParser.AND) + self.state = 2765 + localctx.upper = self.valueExpression(0) + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 2768 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==172: + self.state = 2767 + self.match(SqlBaseParser.NOT) + + + self.state = 2770 + localctx.kind = self.match(SqlBaseParser.IN) + self.state = 2771 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2772 + self.expression() + self.state = 2777 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2773 + self.match(SqlBaseParser.COMMA) + self.state = 2774 + self.expression() + self.state = 2779 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2780 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 2783 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==172: + self.state = 2782 + self.match(SqlBaseParser.NOT) + + + self.state = 2785 + localctx.kind = self.match(SqlBaseParser.IN) + self.state = 2786 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2787 + self.query() + self.state = 2788 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 2791 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==172: + self.state = 2790 + self.match(SqlBaseParser.NOT) + + + self.state = 2793 + localctx.kind = self.match(SqlBaseParser.RLIKE) + self.state = 2794 + localctx.pattern = self.valueExpression(0) + pass + + elif la_ == 5: + self.enterOuterAlt(localctx, 5) + self.state = 2796 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==172: + self.state = 2795 + self.match(SqlBaseParser.NOT) + + + self.state = 2798 + localctx.kind = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==142 or _la==143): + localctx.kind = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2799 + localctx.quantifier = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==10 or _la==16 or _la==244): + localctx.quantifier = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2813 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,363,self._ctx) + if la_ == 1: + self.state = 2800 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2801 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 2: + self.state = 2802 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2803 + self.expression() + self.state = 2808 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2804 + self.match(SqlBaseParser.COMMA) + self.state = 2805 + self.expression() + self.state = 2810 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 2811 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + + pass + + elif la_ == 6: + self.enterOuterAlt(localctx, 6) + self.state = 2816 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==172: + self.state = 2815 + self.match(SqlBaseParser.NOT) + + + self.state = 2818 + localctx.kind = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==142 or _la==143): + localctx.kind = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2819 + localctx.pattern = self.valueExpression(0) + self.state = 2822 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,365,self._ctx) + if la_ == 1: + self.state = 2820 + self.match(SqlBaseParser.ESCAPE) + self.state = 2821 + localctx.escapeChar = self.stringLit() + + + pass + + elif la_ == 7: + self.enterOuterAlt(localctx, 7) + self.state = 2824 + self.match(SqlBaseParser.IS) + self.state = 2826 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==172: + self.state = 2825 + self.match(SqlBaseParser.NOT) + + + self.state = 2828 + localctx.kind = self.match(SqlBaseParser.NULL) + pass + + elif la_ == 8: + self.enterOuterAlt(localctx, 8) + self.state = 2829 + self.match(SqlBaseParser.IS) + self.state = 2831 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==172: + self.state = 2830 + self.match(SqlBaseParser.NOT) + + + self.state = 2833 + localctx.kind = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==96 or _la==277 or _la==286): + localctx.kind = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + pass + + elif la_ == 9: + self.enterOuterAlt(localctx, 9) + self.state = 2834 + self.match(SqlBaseParser.IS) + self.state = 2836 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==172: + self.state = 2835 + self.match(SqlBaseParser.NOT) + + + self.state = 2838 + localctx.kind = self.match(SqlBaseParser.DISTINCT) + self.state = 2839 + self.match(SqlBaseParser.FROM) + self.state = 2840 + localctx.right = self.valueExpression(0) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ValueExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_valueExpression + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + class ValueExpressionDefaultContext(ValueExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ValueExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def primaryExpression(self): + return self.getTypedRuleContext(SqlBaseParser.PrimaryExpressionContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterValueExpressionDefault" ): + listener.enterValueExpressionDefault(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitValueExpressionDefault" ): + listener.exitValueExpressionDefault(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitValueExpressionDefault" ): + return visitor.visitValueExpressionDefault(self) + else: + return visitor.visitChildren(self) + + + class ComparisonContext(ValueExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ValueExpressionContext + super().__init__(parser) + self.left = None # ValueExpressionContext + self.right = None # ValueExpressionContext + self.copyFrom(ctx) + + def comparisonOperator(self): + return self.getTypedRuleContext(SqlBaseParser.ComparisonOperatorContext,0) + + def valueExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ValueExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterComparison" ): + listener.enterComparison(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitComparison" ): + listener.exitComparison(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitComparison" ): + return visitor.visitComparison(self) + else: + return visitor.visitChildren(self) + + + class ArithmeticBinaryContext(ValueExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ValueExpressionContext + super().__init__(parser) + self.left = None # ValueExpressionContext + self.operator = None # Token + self.right = None # ValueExpressionContext + self.copyFrom(ctx) + + def valueExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ValueExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,i) + + def ASTERISK(self): + return self.getToken(SqlBaseParser.ASTERISK, 0) + def SLASH(self): + return self.getToken(SqlBaseParser.SLASH, 0) + def PERCENT(self): + return self.getToken(SqlBaseParser.PERCENT, 0) + def DIV(self): + return self.getToken(SqlBaseParser.DIV, 0) + def PLUS(self): + return self.getToken(SqlBaseParser.PLUS, 0) + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + def CONCAT_PIPE(self): + return self.getToken(SqlBaseParser.CONCAT_PIPE, 0) + def AMPERSAND(self): + return self.getToken(SqlBaseParser.AMPERSAND, 0) + def HAT(self): + return self.getToken(SqlBaseParser.HAT, 0) + def PIPE(self): + return self.getToken(SqlBaseParser.PIPE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterArithmeticBinary" ): + listener.enterArithmeticBinary(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitArithmeticBinary" ): + listener.exitArithmeticBinary(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitArithmeticBinary" ): + return visitor.visitArithmeticBinary(self) + else: + return visitor.visitChildren(self) + + + class ArithmeticUnaryContext(ValueExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ValueExpressionContext + super().__init__(parser) + self.operator = None # Token + self.copyFrom(ctx) + + def valueExpression(self): + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,0) + + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + def PLUS(self): + return self.getToken(SqlBaseParser.PLUS, 0) + def TILDE(self): + return self.getToken(SqlBaseParser.TILDE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterArithmeticUnary" ): + listener.enterArithmeticUnary(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitArithmeticUnary" ): + listener.exitArithmeticUnary(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitArithmeticUnary" ): + return visitor.visitArithmeticUnary(self) + else: + return visitor.visitChildren(self) + + + + def valueExpression(self, _p:int=0): + _parentctx = self._ctx + _parentState = self.state + localctx = SqlBaseParser.ValueExpressionContext(self, self._ctx, _parentState) + _prevctx = localctx + _startState = 244 + self.enterRecursionRule(localctx, 244, self.RULE_valueExpression, _p) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2847 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,370,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.ValueExpressionDefaultContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + + self.state = 2844 + self.primaryExpression(0) + pass + + elif la_ == 2: + localctx = SqlBaseParser.ArithmeticUnaryContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2845 + localctx.operator = self._input.LT(1) + _la = self._input.LA(1) + if not(((((_la - 316)) & ~0x3f) == 0 and ((1 << (_la - 316)) & 35) != 0)): + localctx.operator = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2846 + self.valueExpression(7) + pass + + + self._ctx.stop = self._input.LT(-1) + self.state = 2870 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,372,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + if self._parseListeners is not None: + self.triggerExitRuleEvent() + _prevctx = localctx + self.state = 2868 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,371,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.ArithmeticBinaryContext(self, SqlBaseParser.ValueExpressionContext(self, _parentctx, _parentState)) + localctx.left = _prevctx + self.pushNewRecursionContext(localctx, _startState, self.RULE_valueExpression) + self.state = 2849 + if not self.precpred(self._ctx, 6): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 6)") + self.state = 2850 + localctx.operator = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==81 or ((((_la - 318)) & ~0x3f) == 0 and ((1 << (_la - 318)) & 7) != 0)): + localctx.operator = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2851 + localctx.right = self.valueExpression(7) + pass + + elif la_ == 2: + localctx = SqlBaseParser.ArithmeticBinaryContext(self, SqlBaseParser.ValueExpressionContext(self, _parentctx, _parentState)) + localctx.left = _prevctx + self.pushNewRecursionContext(localctx, _startState, self.RULE_valueExpression) + self.state = 2852 + if not self.precpred(self._ctx, 5): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 5)") + self.state = 2853 + localctx.operator = self._input.LT(1) + _la = self._input.LA(1) + if not(((((_la - 316)) & ~0x3f) == 0 and ((1 << (_la - 316)) & 259) != 0)): + localctx.operator = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2854 + localctx.right = self.valueExpression(6) + pass + + elif la_ == 3: + localctx = SqlBaseParser.ArithmeticBinaryContext(self, SqlBaseParser.ValueExpressionContext(self, _parentctx, _parentState)) + localctx.left = _prevctx + self.pushNewRecursionContext(localctx, _startState, self.RULE_valueExpression) + self.state = 2855 + if not self.precpred(self._ctx, 4): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 4)") + self.state = 2856 + localctx.operator = self.match(SqlBaseParser.AMPERSAND) + self.state = 2857 + localctx.right = self.valueExpression(5) + pass + + elif la_ == 4: + localctx = SqlBaseParser.ArithmeticBinaryContext(self, SqlBaseParser.ValueExpressionContext(self, _parentctx, _parentState)) + localctx.left = _prevctx + self.pushNewRecursionContext(localctx, _startState, self.RULE_valueExpression) + self.state = 2858 + if not self.precpred(self._ctx, 3): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 3)") + self.state = 2859 + localctx.operator = self.match(SqlBaseParser.HAT) + self.state = 2860 + localctx.right = self.valueExpression(4) + pass + + elif la_ == 5: + localctx = SqlBaseParser.ArithmeticBinaryContext(self, SqlBaseParser.ValueExpressionContext(self, _parentctx, _parentState)) + localctx.left = _prevctx + self.pushNewRecursionContext(localctx, _startState, self.RULE_valueExpression) + self.state = 2861 + if not self.precpred(self._ctx, 2): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 2)") + self.state = 2862 + localctx.operator = self.match(SqlBaseParser.PIPE) + self.state = 2863 + localctx.right = self.valueExpression(3) + pass + + elif la_ == 6: + localctx = SqlBaseParser.ComparisonContext(self, SqlBaseParser.ValueExpressionContext(self, _parentctx, _parentState)) + localctx.left = _prevctx + self.pushNewRecursionContext(localctx, _startState, self.RULE_valueExpression) + self.state = 2864 + if not self.precpred(self._ctx, 1): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 1)") + self.state = 2865 + self.comparisonOperator() + self.state = 2866 + localctx.right = self.valueExpression(2) + pass + + + self.state = 2872 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,372,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.unrollRecursionContexts(_parentctx) + return localctx + + + class DatetimeUnitContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def YEAR(self): + return self.getToken(SqlBaseParser.YEAR, 0) + + def QUARTER(self): + return self.getToken(SqlBaseParser.QUARTER, 0) + + def MONTH(self): + return self.getToken(SqlBaseParser.MONTH, 0) + + def WEEK(self): + return self.getToken(SqlBaseParser.WEEK, 0) + + def DAY(self): + return self.getToken(SqlBaseParser.DAY, 0) + + def DAYOFYEAR(self): + return self.getToken(SqlBaseParser.DAYOFYEAR, 0) + + def HOUR(self): + return self.getToken(SqlBaseParser.HOUR, 0) + + def MINUTE(self): + return self.getToken(SqlBaseParser.MINUTE, 0) + + def SECOND(self): + return self.getToken(SqlBaseParser.SECOND, 0) + + def MILLISECOND(self): + return self.getToken(SqlBaseParser.MILLISECOND, 0) + + def MICROSECOND(self): + return self.getToken(SqlBaseParser.MICROSECOND, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_datetimeUnit + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDatetimeUnit" ): + listener.enterDatetimeUnit(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDatetimeUnit" ): + listener.exitDatetimeUnit(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDatetimeUnit" ): + return visitor.visitDatetimeUnit(self) + else: + return visitor.visitChildren(self) + + + + + def datetimeUnit(self): + + localctx = SqlBaseParser.DatetimeUnitContext(self, self._ctx, self.state) + self.enterRule(localctx, 246, self.RULE_datetimeUnit) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 2873 + _la = self._input.LA(1) + if not(_la==61 or _la==63 or ((((_la - 117)) & ~0x3f) == 0 and ((1 << (_la - 117)) & 93458488360961) != 0) or _la==204 or _la==229 or _la==298 or _la==305): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PrimaryExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_primaryExpression + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + class StructContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self._namedExpression = None # NamedExpressionContext + self.argument = list() # of NamedExpressionContexts + self.copyFrom(ctx) + + def STRUCT(self): + return self.getToken(SqlBaseParser.STRUCT, 0) + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def namedExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.NamedExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.NamedExpressionContext,i) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterStruct" ): + listener.enterStruct(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitStruct" ): + listener.exitStruct(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitStruct" ): + return visitor.visitStruct(self) + else: + return visitor.visitChildren(self) + + + class DereferenceContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.base = None # PrimaryExpressionContext + self.fieldName = None # IdentifierContext + self.copyFrom(ctx) + + def DOT(self): + return self.getToken(SqlBaseParser.DOT, 0) + def primaryExpression(self): + return self.getTypedRuleContext(SqlBaseParser.PrimaryExpressionContext,0) + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDereference" ): + listener.enterDereference(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDereference" ): + listener.exitDereference(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDereference" ): + return visitor.visitDereference(self) + else: + return visitor.visitChildren(self) + + + class TimestampaddContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.name = None # Token + self.unit = None # DatetimeUnitContext + self.unitsAmount = None # ValueExpressionContext + self.timestamp = None # ValueExpressionContext + self.copyFrom(ctx) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def datetimeUnit(self): + return self.getTypedRuleContext(SqlBaseParser.DatetimeUnitContext,0) + + def valueExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ValueExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,i) + + def TIMESTAMPADD(self): + return self.getToken(SqlBaseParser.TIMESTAMPADD, 0) + def DATEADD(self): + return self.getToken(SqlBaseParser.DATEADD, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTimestampadd" ): + listener.enterTimestampadd(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTimestampadd" ): + listener.exitTimestampadd(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTimestampadd" ): + return visitor.visitTimestampadd(self) + else: + return visitor.visitChildren(self) + + + class SubstringContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.str_ = None # ValueExpressionContext + self.pos = None # ValueExpressionContext + self.len_ = None # ValueExpressionContext + self.copyFrom(ctx) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def SUBSTR(self): + return self.getToken(SqlBaseParser.SUBSTR, 0) + def SUBSTRING(self): + return self.getToken(SqlBaseParser.SUBSTRING, 0) + def valueExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ValueExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,i) + + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + def FOR(self): + return self.getToken(SqlBaseParser.FOR, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSubstring" ): + listener.enterSubstring(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSubstring" ): + listener.exitSubstring(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSubstring" ): + return visitor.visitSubstring(self) + else: + return visitor.visitChildren(self) + + + class CastContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.name = None # Token + self.copyFrom(ctx) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + def dataType(self): + return self.getTypedRuleContext(SqlBaseParser.DataTypeContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def CAST(self): + return self.getToken(SqlBaseParser.CAST, 0) + def TRY_CAST(self): + return self.getToken(SqlBaseParser.TRY_CAST, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCast" ): + listener.enterCast(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCast" ): + listener.exitCast(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCast" ): + return visitor.visitCast(self) + else: + return visitor.visitChildren(self) + + + class LambdaContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def identifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.IdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,i) + + def ARROW(self): + return self.getToken(SqlBaseParser.ARROW, 0) + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterLambda" ): + listener.enterLambda(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitLambda" ): + listener.exitLambda(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitLambda" ): + return visitor.visitLambda(self) + else: + return visitor.visitChildren(self) + + + class ParenthesizedExpressionContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterParenthesizedExpression" ): + listener.enterParenthesizedExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitParenthesizedExpression" ): + listener.exitParenthesizedExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitParenthesizedExpression" ): + return visitor.visitParenthesizedExpression(self) + else: + return visitor.visitChildren(self) + + + class Any_valueContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def ANY_VALUE(self): + return self.getToken(SqlBaseParser.ANY_VALUE, 0) + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def IGNORE(self): + return self.getToken(SqlBaseParser.IGNORE, 0) + def NULLS(self): + return self.getToken(SqlBaseParser.NULLS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAny_value" ): + listener.enterAny_value(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAny_value" ): + listener.exitAny_value(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAny_value" ): + return visitor.visitAny_value(self) + else: + return visitor.visitChildren(self) + + + class TrimContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.trimOption = None # Token + self.trimStr = None # ValueExpressionContext + self.srcStr = None # ValueExpressionContext + self.copyFrom(ctx) + + def TRIM(self): + return self.getToken(SqlBaseParser.TRIM, 0) + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def valueExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ValueExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,i) + + def BOTH(self): + return self.getToken(SqlBaseParser.BOTH, 0) + def LEADING(self): + return self.getToken(SqlBaseParser.LEADING, 0) + def TRAILING(self): + return self.getToken(SqlBaseParser.TRAILING, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTrim" ): + listener.enterTrim(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTrim" ): + listener.exitTrim(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTrim" ): + return visitor.visitTrim(self) + else: + return visitor.visitChildren(self) + + + class SimpleCaseContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.value = None # ExpressionContext + self.elseExpression = None # ExpressionContext + self.copyFrom(ctx) + + def CASE(self): + return self.getToken(SqlBaseParser.CASE, 0) + def END(self): + return self.getToken(SqlBaseParser.END, 0) + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + def whenClause(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.WhenClauseContext) + else: + return self.getTypedRuleContext(SqlBaseParser.WhenClauseContext,i) + + def ELSE(self): + return self.getToken(SqlBaseParser.ELSE, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSimpleCase" ): + listener.enterSimpleCase(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSimpleCase" ): + listener.exitSimpleCase(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSimpleCase" ): + return visitor.visitSimpleCase(self) + else: + return visitor.visitChildren(self) + + + class CurrentLikeContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.name = None # Token + self.copyFrom(ctx) + + def CURRENT_DATE(self): + return self.getToken(SqlBaseParser.CURRENT_DATE, 0) + def CURRENT_TIMESTAMP(self): + return self.getToken(SqlBaseParser.CURRENT_TIMESTAMP, 0) + def CURRENT_USER(self): + return self.getToken(SqlBaseParser.CURRENT_USER, 0) + def USER(self): + return self.getToken(SqlBaseParser.USER, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCurrentLike" ): + listener.enterCurrentLike(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCurrentLike" ): + listener.exitCurrentLike(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCurrentLike" ): + return visitor.visitCurrentLike(self) + else: + return visitor.visitChildren(self) + + + class ColumnReferenceContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterColumnReference" ): + listener.enterColumnReference(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitColumnReference" ): + listener.exitColumnReference(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitColumnReference" ): + return visitor.visitColumnReference(self) + else: + return visitor.visitChildren(self) + + + class RowConstructorContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def namedExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.NamedExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.NamedExpressionContext,i) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRowConstructor" ): + listener.enterRowConstructor(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRowConstructor" ): + listener.exitRowConstructor(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRowConstructor" ): + return visitor.visitRowConstructor(self) + else: + return visitor.visitChildren(self) + + + class LastContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def LAST(self): + return self.getToken(SqlBaseParser.LAST, 0) + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def IGNORE(self): + return self.getToken(SqlBaseParser.IGNORE, 0) + def NULLS(self): + return self.getToken(SqlBaseParser.NULLS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterLast" ): + listener.enterLast(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitLast" ): + listener.exitLast(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitLast" ): + return visitor.visitLast(self) + else: + return visitor.visitChildren(self) + + + class StarContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def ASTERISK(self): + return self.getToken(SqlBaseParser.ASTERISK, 0) + def qualifiedName(self): + return self.getTypedRuleContext(SqlBaseParser.QualifiedNameContext,0) + + def DOT(self): + return self.getToken(SqlBaseParser.DOT, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterStar" ): + listener.enterStar(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitStar" ): + listener.exitStar(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitStar" ): + return visitor.visitStar(self) + else: + return visitor.visitChildren(self) + + + class OverlayContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.input_ = None # ValueExpressionContext + self.replace = None # ValueExpressionContext + self.position = None # ValueExpressionContext + self.length = None # ValueExpressionContext + self.copyFrom(ctx) + + def OVERLAY(self): + return self.getToken(SqlBaseParser.OVERLAY, 0) + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def PLACING(self): + return self.getToken(SqlBaseParser.PLACING, 0) + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def valueExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ValueExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,i) + + def FOR(self): + return self.getToken(SqlBaseParser.FOR, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOverlay" ): + listener.enterOverlay(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOverlay" ): + listener.exitOverlay(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitOverlay" ): + return visitor.visitOverlay(self) + else: + return visitor.visitChildren(self) + + + class SubscriptContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.value = None # PrimaryExpressionContext + self.index = None # ValueExpressionContext + self.copyFrom(ctx) + + def LEFT_BRACKET(self): + return self.getToken(SqlBaseParser.LEFT_BRACKET, 0) + def RIGHT_BRACKET(self): + return self.getToken(SqlBaseParser.RIGHT_BRACKET, 0) + def primaryExpression(self): + return self.getTypedRuleContext(SqlBaseParser.PrimaryExpressionContext,0) + + def valueExpression(self): + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSubscript" ): + listener.enterSubscript(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSubscript" ): + listener.exitSubscript(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSubscript" ): + return visitor.visitSubscript(self) + else: + return visitor.visitChildren(self) + + + class TimestampdiffContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.name = None # Token + self.unit = None # DatetimeUnitContext + self.startTimestamp = None # ValueExpressionContext + self.endTimestamp = None # ValueExpressionContext + self.copyFrom(ctx) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def datetimeUnit(self): + return self.getTypedRuleContext(SqlBaseParser.DatetimeUnitContext,0) + + def valueExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ValueExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,i) + + def TIMESTAMPDIFF(self): + return self.getToken(SqlBaseParser.TIMESTAMPDIFF, 0) + def DATEDIFF(self): + return self.getToken(SqlBaseParser.DATEDIFF, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTimestampdiff" ): + listener.enterTimestampdiff(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTimestampdiff" ): + listener.exitTimestampdiff(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTimestampdiff" ): + return visitor.visitTimestampdiff(self) + else: + return visitor.visitChildren(self) + + + class SubqueryExpressionContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def query(self): + return self.getTypedRuleContext(SqlBaseParser.QueryContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSubqueryExpression" ): + listener.enterSubqueryExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSubqueryExpression" ): + listener.exitSubqueryExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSubqueryExpression" ): + return visitor.visitSubqueryExpression(self) + else: + return visitor.visitChildren(self) + + + class ConstantDefaultContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def constant(self): + return self.getTypedRuleContext(SqlBaseParser.ConstantContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterConstantDefault" ): + listener.enterConstantDefault(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitConstantDefault" ): + listener.exitConstantDefault(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitConstantDefault" ): + return visitor.visitConstantDefault(self) + else: + return visitor.visitChildren(self) + + + class ExtractContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.field = None # IdentifierContext + self.source = None # ValueExpressionContext + self.copyFrom(ctx) + + def EXTRACT(self): + return self.getToken(SqlBaseParser.EXTRACT, 0) + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + def valueExpression(self): + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterExtract" ): + listener.enterExtract(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitExtract" ): + listener.exitExtract(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitExtract" ): + return visitor.visitExtract(self) + else: + return visitor.visitChildren(self) + + + class PercentileContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.name = None # Token + self.percentage = None # ValueExpressionContext + self.where = None # BooleanExpressionContext + self.copyFrom(ctx) + + def LEFT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.LEFT_PAREN) + else: + return self.getToken(SqlBaseParser.LEFT_PAREN, i) + def RIGHT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.RIGHT_PAREN) + else: + return self.getToken(SqlBaseParser.RIGHT_PAREN, i) + def WITHIN(self): + return self.getToken(SqlBaseParser.WITHIN, 0) + def GROUP(self): + return self.getToken(SqlBaseParser.GROUP, 0) + def ORDER(self): + return self.getToken(SqlBaseParser.ORDER, 0) + def BY(self): + return self.getToken(SqlBaseParser.BY, 0) + def sortItem(self): + return self.getTypedRuleContext(SqlBaseParser.SortItemContext,0) + + def valueExpression(self): + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,0) + + def PERCENTILE_CONT(self): + return self.getToken(SqlBaseParser.PERCENTILE_CONT, 0) + def PERCENTILE_DISC(self): + return self.getToken(SqlBaseParser.PERCENTILE_DISC, 0) + def FILTER(self): + return self.getToken(SqlBaseParser.FILTER, 0) + def WHERE(self): + return self.getToken(SqlBaseParser.WHERE, 0) + def OVER(self): + return self.getToken(SqlBaseParser.OVER, 0) + def windowSpec(self): + return self.getTypedRuleContext(SqlBaseParser.WindowSpecContext,0) + + def booleanExpression(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanExpressionContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPercentile" ): + listener.enterPercentile(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPercentile" ): + listener.exitPercentile(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPercentile" ): + return visitor.visitPercentile(self) + else: + return visitor.visitChildren(self) + + + class FunctionCallContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self._expression = None # ExpressionContext + self.argument = list() # of ExpressionContexts + self.where = None # BooleanExpressionContext + self.nullsOption = None # Token + self.copyFrom(ctx) + + def functionName(self): + return self.getTypedRuleContext(SqlBaseParser.FunctionNameContext,0) + + def LEFT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.LEFT_PAREN) + else: + return self.getToken(SqlBaseParser.LEFT_PAREN, i) + def RIGHT_PAREN(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.RIGHT_PAREN) + else: + return self.getToken(SqlBaseParser.RIGHT_PAREN, i) + def FILTER(self): + return self.getToken(SqlBaseParser.FILTER, 0) + def WHERE(self): + return self.getToken(SqlBaseParser.WHERE, 0) + def NULLS(self): + return self.getToken(SqlBaseParser.NULLS, 0) + def OVER(self): + return self.getToken(SqlBaseParser.OVER, 0) + def windowSpec(self): + return self.getTypedRuleContext(SqlBaseParser.WindowSpecContext,0) + + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + def booleanExpression(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanExpressionContext,0) + + def IGNORE(self): + return self.getToken(SqlBaseParser.IGNORE, 0) + def RESPECT(self): + return self.getToken(SqlBaseParser.RESPECT, 0) + def setQuantifier(self): + return self.getTypedRuleContext(SqlBaseParser.SetQuantifierContext,0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFunctionCall" ): + listener.enterFunctionCall(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFunctionCall" ): + listener.exitFunctionCall(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFunctionCall" ): + return visitor.visitFunctionCall(self) + else: + return visitor.visitChildren(self) + + + class SearchedCaseContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.elseExpression = None # ExpressionContext + self.copyFrom(ctx) + + def CASE(self): + return self.getToken(SqlBaseParser.CASE, 0) + def END(self): + return self.getToken(SqlBaseParser.END, 0) + def whenClause(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.WhenClauseContext) + else: + return self.getTypedRuleContext(SqlBaseParser.WhenClauseContext,i) + + def ELSE(self): + return self.getToken(SqlBaseParser.ELSE, 0) + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSearchedCase" ): + listener.enterSearchedCase(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSearchedCase" ): + listener.exitSearchedCase(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSearchedCase" ): + return visitor.visitSearchedCase(self) + else: + return visitor.visitChildren(self) + + + class PositionContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.substr = None # ValueExpressionContext + self.str_ = None # ValueExpressionContext + self.copyFrom(ctx) + + def POSITION(self): + return self.getToken(SqlBaseParser.POSITION, 0) + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def valueExpression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ValueExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ValueExpressionContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPosition" ): + listener.enterPosition(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPosition" ): + listener.exitPosition(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPosition" ): + return visitor.visitPosition(self) + else: + return visitor.visitChildren(self) + + + class FirstContext(PrimaryExpressionContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.PrimaryExpressionContext + super().__init__(parser) + self.copyFrom(ctx) + + def FIRST(self): + return self.getToken(SqlBaseParser.FIRST, 0) + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def IGNORE(self): + return self.getToken(SqlBaseParser.IGNORE, 0) + def NULLS(self): + return self.getToken(SqlBaseParser.NULLS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFirst" ): + listener.enterFirst(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFirst" ): + listener.exitFirst(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFirst" ): + return visitor.visitFirst(self) + else: + return visitor.visitChildren(self) + + + + def primaryExpression(self, _p:int=0): + _parentctx = self._ctx + _parentState = self.state + localctx = SqlBaseParser.PrimaryExpressionContext(self, self._ctx, _parentState) + _prevctx = localctx + _startState = 248 + self.enterRecursionRule(localctx, 248, self.RULE_primaryExpression, _p) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3113 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,396,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.CurrentLikeContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + + self.state = 2876 + localctx.name = self._input.LT(1) + _la = self._input.LA(1) + if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 1873497444986126336) != 0) or _la==292): + localctx.name = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + pass + + elif la_ == 2: + localctx = SqlBaseParser.TimestampaddContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2877 + localctx.name = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==67 or _la==268): + localctx.name = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2878 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2879 + localctx.unit = self.datetimeUnit() + self.state = 2880 + self.match(SqlBaseParser.COMMA) + self.state = 2881 + localctx.unitsAmount = self.valueExpression(0) + self.state = 2882 + self.match(SqlBaseParser.COMMA) + self.state = 2883 + localctx.timestamp = self.valueExpression(0) + self.state = 2884 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 3: + localctx = SqlBaseParser.TimestampdiffContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2886 + localctx.name = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==68 or _la==269): + localctx.name = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2887 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2888 + localctx.unit = self.datetimeUnit() + self.state = 2889 + self.match(SqlBaseParser.COMMA) + self.state = 2890 + localctx.startTimestamp = self.valueExpression(0) + self.state = 2891 + self.match(SqlBaseParser.COMMA) + self.state = 2892 + localctx.endTimestamp = self.valueExpression(0) + self.state = 2893 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 4: + localctx = SqlBaseParser.SearchedCaseContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2895 + self.match(SqlBaseParser.CASE) + self.state = 2897 + self._errHandler.sync(self) + _la = self._input.LA(1) + while True: + self.state = 2896 + self.whenClause() + self.state = 2899 + self._errHandler.sync(self) + _la = self._input.LA(1) + if not (_la==300): + break + + self.state = 2903 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==83: + self.state = 2901 + self.match(SqlBaseParser.ELSE) + self.state = 2902 + localctx.elseExpression = self.expression() + + + self.state = 2905 + self.match(SqlBaseParser.END) + pass + + elif la_ == 5: + localctx = SqlBaseParser.SimpleCaseContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2907 + self.match(SqlBaseParser.CASE) + self.state = 2908 + localctx.value = self.expression() + self.state = 2910 + self._errHandler.sync(self) + _la = self._input.LA(1) + while True: + self.state = 2909 + self.whenClause() + self.state = 2912 + self._errHandler.sync(self) + _la = self._input.LA(1) + if not (_la==300): + break + + self.state = 2916 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==83: + self.state = 2914 + self.match(SqlBaseParser.ELSE) + self.state = 2915 + localctx.elseExpression = self.expression() + + + self.state = 2918 + self.match(SqlBaseParser.END) + pass + + elif la_ == 6: + localctx = SqlBaseParser.CastContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2920 + localctx.name = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==32 or _la==279): + localctx.name = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 2921 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2922 + self.expression() + self.state = 2923 + self.match(SqlBaseParser.AS) + self.state = 2924 + self.dataType() + self.state = 2925 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 7: + localctx = SqlBaseParser.StructContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2927 + self.match(SqlBaseParser.STRUCT) + self.state = 2928 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2937 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -252) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 8074954131875299327) != 0) or ((((_la - 321)) & ~0x3f) == 0 and ((1 << (_la - 321)) & 4193825) != 0): + self.state = 2929 + localctx._namedExpression = self.namedExpression() + localctx.argument.append(localctx._namedExpression) + self.state = 2934 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 2930 + self.match(SqlBaseParser.COMMA) + self.state = 2931 + localctx._namedExpression = self.namedExpression() + localctx.argument.append(localctx._namedExpression) + self.state = 2936 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + self.state = 2939 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 8: + localctx = SqlBaseParser.FirstContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2940 + self.match(SqlBaseParser.FIRST) + self.state = 2941 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2942 + self.expression() + self.state = 2945 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==120: + self.state = 2943 + self.match(SqlBaseParser.IGNORE) + self.state = 2944 + self.match(SqlBaseParser.NULLS) + + + self.state = 2947 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 9: + localctx = SqlBaseParser.Any_valueContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2949 + self.match(SqlBaseParser.ANY_VALUE) + self.state = 2950 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2951 + self.expression() + self.state = 2954 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==120: + self.state = 2952 + self.match(SqlBaseParser.IGNORE) + self.state = 2953 + self.match(SqlBaseParser.NULLS) + + + self.state = 2956 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 10: + localctx = SqlBaseParser.LastContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2958 + self.match(SqlBaseParser.LAST) + self.state = 2959 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2960 + self.expression() + self.state = 2963 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==120: + self.state = 2961 + self.match(SqlBaseParser.IGNORE) + self.state = 2962 + self.match(SqlBaseParser.NULLS) + + + self.state = 2965 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 11: + localctx = SqlBaseParser.PositionContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2967 + self.match(SqlBaseParser.POSITION) + self.state = 2968 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2969 + localctx.substr = self.valueExpression(0) + self.state = 2970 + self.match(SqlBaseParser.IN) + self.state = 2971 + localctx.str_ = self.valueExpression(0) + self.state = 2972 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 12: + localctx = SqlBaseParser.ConstantDefaultContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2974 + self.constant() + pass + + elif la_ == 13: + localctx = SqlBaseParser.StarContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2975 + self.match(SqlBaseParser.ASTERISK) + pass + + elif la_ == 14: + localctx = SqlBaseParser.StarContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2976 + self.qualifiedName() + self.state = 2977 + self.match(SqlBaseParser.DOT) + self.state = 2978 + self.match(SqlBaseParser.ASTERISK) + pass + + elif la_ == 15: + localctx = SqlBaseParser.RowConstructorContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2980 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2981 + self.namedExpression() + self.state = 2984 + self._errHandler.sync(self) + _la = self._input.LA(1) + while True: + self.state = 2982 + self.match(SqlBaseParser.COMMA) + self.state = 2983 + self.namedExpression() + self.state = 2986 + self._errHandler.sync(self) + _la = self._input.LA(1) + if not (_la==4): + break + + self.state = 2988 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 16: + localctx = SqlBaseParser.SubqueryExpressionContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2990 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 2991 + self.query() + self.state = 2992 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 17: + localctx = SqlBaseParser.FunctionCallContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 2994 + self.functionName() + self.state = 2995 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3007 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -252) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 8074954131875299327) != 0) or ((((_la - 321)) & ~0x3f) == 0 and ((1 << (_la - 321)) & 4193825) != 0): + self.state = 2997 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,383,self._ctx) + if la_ == 1: + self.state = 2996 + self.setQuantifier() + + + self.state = 2999 + localctx._expression = self.expression() + localctx.argument.append(localctx._expression) + self.state = 3004 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 3000 + self.match(SqlBaseParser.COMMA) + self.state = 3001 + localctx._expression = self.expression() + localctx.argument.append(localctx._expression) + self.state = 3006 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + self.state = 3009 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 3016 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,386,self._ctx) + if la_ == 1: + self.state = 3010 + self.match(SqlBaseParser.FILTER) + self.state = 3011 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3012 + self.match(SqlBaseParser.WHERE) + self.state = 3013 + localctx.where = self.booleanExpression(0) + self.state = 3014 + self.match(SqlBaseParser.RIGHT_PAREN) + + + self.state = 3020 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,387,self._ctx) + if la_ == 1: + self.state = 3018 + localctx.nullsOption = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==120 or _la==218): + localctx.nullsOption = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 3019 + self.match(SqlBaseParser.NULLS) + + + self.state = 3024 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,388,self._ctx) + if la_ == 1: + self.state = 3022 + self.match(SqlBaseParser.OVER) + self.state = 3023 + self.windowSpec() + + + pass + + elif la_ == 18: + localctx = SqlBaseParser.LambdaContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 3026 + self.identifier() + self.state = 3027 + self.match(SqlBaseParser.ARROW) + self.state = 3028 + self.expression() + pass + + elif la_ == 19: + localctx = SqlBaseParser.LambdaContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 3030 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3031 + self.identifier() + self.state = 3034 + self._errHandler.sync(self) + _la = self._input.LA(1) + while True: + self.state = 3032 + self.match(SqlBaseParser.COMMA) + self.state = 3033 + self.identifier() + self.state = 3036 + self._errHandler.sync(self) + _la = self._input.LA(1) + if not (_la==4): + break + + self.state = 3038 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 3039 + self.match(SqlBaseParser.ARROW) + self.state = 3040 + self.expression() + pass + + elif la_ == 20: + localctx = SqlBaseParser.ColumnReferenceContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 3042 + self.identifier() + pass + + elif la_ == 21: + localctx = SqlBaseParser.ParenthesizedExpressionContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 3043 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3044 + self.expression() + self.state = 3045 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 22: + localctx = SqlBaseParser.ExtractContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 3047 + self.match(SqlBaseParser.EXTRACT) + self.state = 3048 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3049 + localctx.field = self.identifier() + self.state = 3050 + self.match(SqlBaseParser.FROM) + self.state = 3051 + localctx.source = self.valueExpression(0) + self.state = 3052 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 23: + localctx = SqlBaseParser.SubstringContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 3054 + _la = self._input.LA(1) + if not(_la==253 or _la==254): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 3055 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3056 + localctx.str_ = self.valueExpression(0) + self.state = 3057 + _la = self._input.LA(1) + if not(_la==4 or _la==107): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 3058 + localctx.pos = self.valueExpression(0) + self.state = 3061 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==4 or _la==103: + self.state = 3059 + _la = self._input.LA(1) + if not(_la==4 or _la==103): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 3060 + localctx.len_ = self.valueExpression(0) + + + self.state = 3063 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 24: + localctx = SqlBaseParser.TrimContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 3065 + self.match(SqlBaseParser.TRIM) + self.state = 3066 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3068 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,391,self._ctx) + if la_ == 1: + self.state = 3067 + localctx.trimOption = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==25 or _la==140 or _la==272): + localctx.trimOption = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + self.state = 3071 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,392,self._ctx) + if la_ == 1: + self.state = 3070 + localctx.trimStr = self.valueExpression(0) + + + self.state = 3073 + self.match(SqlBaseParser.FROM) + self.state = 3074 + localctx.srcStr = self.valueExpression(0) + self.state = 3075 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 25: + localctx = SqlBaseParser.OverlayContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 3077 + self.match(SqlBaseParser.OVERLAY) + self.state = 3078 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3079 + localctx.input_ = self.valueExpression(0) + self.state = 3080 + self.match(SqlBaseParser.PLACING) + self.state = 3081 + localctx.replace = self.valueExpression(0) + self.state = 3082 + self.match(SqlBaseParser.FROM) + self.state = 3083 + localctx.position = self.valueExpression(0) + self.state = 3086 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==103: + self.state = 3084 + self.match(SqlBaseParser.FOR) + self.state = 3085 + localctx.length = self.valueExpression(0) + + + self.state = 3088 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 26: + localctx = SqlBaseParser.PercentileContext(self, localctx) + self._ctx = localctx + _prevctx = localctx + self.state = 3090 + localctx.name = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==193 or _la==194): + localctx.name = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 3091 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3092 + localctx.percentage = self.valueExpression(0) + self.state = 3093 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 3094 + self.match(SqlBaseParser.WITHIN) + self.state = 3095 + self.match(SqlBaseParser.GROUP) + self.state = 3096 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3097 + self.match(SqlBaseParser.ORDER) + self.state = 3098 + self.match(SqlBaseParser.BY) + self.state = 3099 + self.sortItem() + self.state = 3100 + self.match(SqlBaseParser.RIGHT_PAREN) + self.state = 3107 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,394,self._ctx) + if la_ == 1: + self.state = 3101 + self.match(SqlBaseParser.FILTER) + self.state = 3102 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3103 + self.match(SqlBaseParser.WHERE) + self.state = 3104 + localctx.where = self.booleanExpression(0) + self.state = 3105 + self.match(SqlBaseParser.RIGHT_PAREN) + + + self.state = 3111 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,395,self._ctx) + if la_ == 1: + self.state = 3109 + self.match(SqlBaseParser.OVER) + self.state = 3110 + self.windowSpec() + + + pass + + + self._ctx.stop = self._input.LT(-1) + self.state = 3125 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,398,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + if self._parseListeners is not None: + self.triggerExitRuleEvent() + _prevctx = localctx + self.state = 3123 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,397,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.SubscriptContext(self, SqlBaseParser.PrimaryExpressionContext(self, _parentctx, _parentState)) + localctx.value = _prevctx + self.pushNewRecursionContext(localctx, _startState, self.RULE_primaryExpression) + self.state = 3115 + if not self.precpred(self._ctx, 9): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 9)") + self.state = 3116 + self.match(SqlBaseParser.LEFT_BRACKET) + self.state = 3117 + localctx.index = self.valueExpression(0) + self.state = 3118 + self.match(SqlBaseParser.RIGHT_BRACKET) + pass + + elif la_ == 2: + localctx = SqlBaseParser.DereferenceContext(self, SqlBaseParser.PrimaryExpressionContext(self, _parentctx, _parentState)) + localctx.base = _prevctx + self.pushNewRecursionContext(localctx, _startState, self.RULE_primaryExpression) + self.state = 3120 + if not self.precpred(self._ctx, 7): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.precpred(self._ctx, 7)") + self.state = 3121 + self.match(SqlBaseParser.DOT) + self.state = 3122 + localctx.fieldName = self.identifier() + pass + + + self.state = 3127 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,398,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.unrollRecursionContexts(_parentctx) + return localctx + + + class ConstantContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_constant + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class NullLiteralContext(ConstantContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ConstantContext + super().__init__(parser) + self.copyFrom(ctx) + + def NULL(self): + return self.getToken(SqlBaseParser.NULL, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNullLiteral" ): + listener.enterNullLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNullLiteral" ): + listener.exitNullLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNullLiteral" ): + return visitor.visitNullLiteral(self) + else: + return visitor.visitChildren(self) + + + class StringLiteralContext(ConstantContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ConstantContext + super().__init__(parser) + self.copyFrom(ctx) + + def stringLit(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.StringLitContext) + else: + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterStringLiteral" ): + listener.enterStringLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitStringLiteral" ): + listener.exitStringLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitStringLiteral" ): + return visitor.visitStringLiteral(self) + else: + return visitor.visitChildren(self) + + + class TypeConstructorContext(ConstantContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ConstantContext + super().__init__(parser) + self.copyFrom(ctx) + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTypeConstructor" ): + listener.enterTypeConstructor(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTypeConstructor" ): + listener.exitTypeConstructor(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTypeConstructor" ): + return visitor.visitTypeConstructor(self) + else: + return visitor.visitChildren(self) + + + class ParameterLiteralContext(ConstantContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ConstantContext + super().__init__(parser) + self.copyFrom(ctx) + + def COLON(self): + return self.getToken(SqlBaseParser.COLON, 0) + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterParameterLiteral" ): + listener.enterParameterLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitParameterLiteral" ): + listener.exitParameterLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitParameterLiteral" ): + return visitor.visitParameterLiteral(self) + else: + return visitor.visitChildren(self) + + + class IntervalLiteralContext(ConstantContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ConstantContext + super().__init__(parser) + self.copyFrom(ctx) + + def interval(self): + return self.getTypedRuleContext(SqlBaseParser.IntervalContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIntervalLiteral" ): + listener.enterIntervalLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIntervalLiteral" ): + listener.exitIntervalLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitIntervalLiteral" ): + return visitor.visitIntervalLiteral(self) + else: + return visitor.visitChildren(self) + + + class NumericLiteralContext(ConstantContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ConstantContext + super().__init__(parser) + self.copyFrom(ctx) + + def number(self): + return self.getTypedRuleContext(SqlBaseParser.NumberContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNumericLiteral" ): + listener.enterNumericLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNumericLiteral" ): + listener.exitNumericLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNumericLiteral" ): + return visitor.visitNumericLiteral(self) + else: + return visitor.visitChildren(self) + + + class BooleanLiteralContext(ConstantContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ConstantContext + super().__init__(parser) + self.copyFrom(ctx) + + def booleanValue(self): + return self.getTypedRuleContext(SqlBaseParser.BooleanValueContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterBooleanLiteral" ): + listener.enterBooleanLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitBooleanLiteral" ): + listener.exitBooleanLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitBooleanLiteral" ): + return visitor.visitBooleanLiteral(self) + else: + return visitor.visitChildren(self) + + + + def constant(self): + + localctx = SqlBaseParser.ConstantContext(self, self._ctx, self.state) + self.enterRule(localctx, 250, self.RULE_constant) + try: + self.state = 3142 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,400,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.NullLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 3128 + self.match(SqlBaseParser.NULL) + pass + + elif la_ == 2: + localctx = SqlBaseParser.ParameterLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 3129 + self.match(SqlBaseParser.COLON) + self.state = 3130 + self.identifier() + pass + + elif la_ == 3: + localctx = SqlBaseParser.IntervalLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 3131 + self.interval() + pass + + elif la_ == 4: + localctx = SqlBaseParser.TypeConstructorContext(self, localctx) + self.enterOuterAlt(localctx, 4) + self.state = 3132 + self.identifier() + self.state = 3133 + self.stringLit() + pass + + elif la_ == 5: + localctx = SqlBaseParser.NumericLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 5) + self.state = 3135 + self.number() + pass + + elif la_ == 6: + localctx = SqlBaseParser.BooleanLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 6) + self.state = 3136 + self.booleanValue() + pass + + elif la_ == 7: + localctx = SqlBaseParser.StringLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 7) + self.state = 3138 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 3137 + self.stringLit() + + else: + raise NoViableAltException(self) + self.state = 3140 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,399,self._ctx) + + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ComparisonOperatorContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def EQ(self): + return self.getToken(SqlBaseParser.EQ, 0) + + def NEQ(self): + return self.getToken(SqlBaseParser.NEQ, 0) + + def NEQJ(self): + return self.getToken(SqlBaseParser.NEQJ, 0) + + def LT(self): + return self.getToken(SqlBaseParser.LT, 0) + + def LTE(self): + return self.getToken(SqlBaseParser.LTE, 0) + + def GT(self): + return self.getToken(SqlBaseParser.GT, 0) + + def GTE(self): + return self.getToken(SqlBaseParser.GTE, 0) + + def NSEQ(self): + return self.getToken(SqlBaseParser.NSEQ, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_comparisonOperator + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterComparisonOperator" ): + listener.enterComparisonOperator(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitComparisonOperator" ): + listener.exitComparisonOperator(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitComparisonOperator" ): + return visitor.visitComparisonOperator(self) + else: + return visitor.visitChildren(self) + + + + + def comparisonOperator(self): + + localctx = SqlBaseParser.ComparisonOperatorContext(self, self._ctx, self.state) + self.enterRule(localctx, 252, self.RULE_comparisonOperator) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3144 + _la = self._input.LA(1) + if not(((((_la - 308)) & ~0x3f) == 0 and ((1 << (_la - 308)) & 255) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ArithmeticOperatorContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def PLUS(self): + return self.getToken(SqlBaseParser.PLUS, 0) + + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def ASTERISK(self): + return self.getToken(SqlBaseParser.ASTERISK, 0) + + def SLASH(self): + return self.getToken(SqlBaseParser.SLASH, 0) + + def PERCENT(self): + return self.getToken(SqlBaseParser.PERCENT, 0) + + def DIV(self): + return self.getToken(SqlBaseParser.DIV, 0) + + def TILDE(self): + return self.getToken(SqlBaseParser.TILDE, 0) + + def AMPERSAND(self): + return self.getToken(SqlBaseParser.AMPERSAND, 0) + + def PIPE(self): + return self.getToken(SqlBaseParser.PIPE, 0) + + def CONCAT_PIPE(self): + return self.getToken(SqlBaseParser.CONCAT_PIPE, 0) + + def HAT(self): + return self.getToken(SqlBaseParser.HAT, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_arithmeticOperator + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterArithmeticOperator" ): + listener.enterArithmeticOperator(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitArithmeticOperator" ): + listener.exitArithmeticOperator(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitArithmeticOperator" ): + return visitor.visitArithmeticOperator(self) + else: + return visitor.visitChildren(self) + + + + + def arithmeticOperator(self): + + localctx = SqlBaseParser.ArithmeticOperatorContext(self, self._ctx, self.state) + self.enterRule(localctx, 254, self.RULE_arithmeticOperator) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3146 + _la = self._input.LA(1) + if not(_la==81 or ((((_la - 316)) & ~0x3f) == 0 and ((1 << (_la - 316)) & 1023) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class PredicateOperatorContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def OR(self): + return self.getToken(SqlBaseParser.OR, 0) + + def AND(self): + return self.getToken(SqlBaseParser.AND, 0) + + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_predicateOperator + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPredicateOperator" ): + listener.enterPredicateOperator(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPredicateOperator" ): + listener.exitPredicateOperator(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPredicateOperator" ): + return visitor.visitPredicateOperator(self) + else: + return visitor.visitChildren(self) + + + + + def predicateOperator(self): + + localctx = SqlBaseParser.PredicateOperatorContext(self, self._ctx, self.state) + self.enterRule(localctx, 256, self.RULE_predicateOperator) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3148 + _la = self._input.LA(1) + if not(_la==14 or ((((_la - 122)) & ~0x3f) == 0 and ((1 << (_la - 122)) & 577586652210266113) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class BooleanValueContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def TRUE(self): + return self.getToken(SqlBaseParser.TRUE, 0) + + def FALSE(self): + return self.getToken(SqlBaseParser.FALSE, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_booleanValue + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterBooleanValue" ): + listener.enterBooleanValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitBooleanValue" ): + listener.exitBooleanValue(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitBooleanValue" ): + return visitor.visitBooleanValue(self) + else: + return visitor.visitChildren(self) + + + + + def booleanValue(self): + + localctx = SqlBaseParser.BooleanValueContext(self, self._ctx, self.state) + self.enterRule(localctx, 258, self.RULE_booleanValue) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3150 + _la = self._input.LA(1) + if not(_la==96 or _la==277): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class IntervalContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def INTERVAL(self): + return self.getToken(SqlBaseParser.INTERVAL, 0) + + def errorCapturingMultiUnitsInterval(self): + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingMultiUnitsIntervalContext,0) + + + def errorCapturingUnitToUnitInterval(self): + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingUnitToUnitIntervalContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_interval + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterInterval" ): + listener.enterInterval(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitInterval" ): + listener.exitInterval(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitInterval" ): + return visitor.visitInterval(self) + else: + return visitor.visitChildren(self) + + + + + def interval(self): + + localctx = SqlBaseParser.IntervalContext(self, self._ctx, self.state) + self.enterRule(localctx, 260, self.RULE_interval) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3152 + self.match(SqlBaseParser.INTERVAL) + self.state = 3155 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,401,self._ctx) + if la_ == 1: + self.state = 3153 + self.errorCapturingMultiUnitsInterval() + pass + + elif la_ == 2: + self.state = 3154 + self.errorCapturingUnitToUnitInterval() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ErrorCapturingMultiUnitsIntervalContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.body = None # MultiUnitsIntervalContext + + def multiUnitsInterval(self): + return self.getTypedRuleContext(SqlBaseParser.MultiUnitsIntervalContext,0) + + + def unitToUnitInterval(self): + return self.getTypedRuleContext(SqlBaseParser.UnitToUnitIntervalContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_errorCapturingMultiUnitsInterval + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterErrorCapturingMultiUnitsInterval" ): + listener.enterErrorCapturingMultiUnitsInterval(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitErrorCapturingMultiUnitsInterval" ): + listener.exitErrorCapturingMultiUnitsInterval(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitErrorCapturingMultiUnitsInterval" ): + return visitor.visitErrorCapturingMultiUnitsInterval(self) + else: + return visitor.visitChildren(self) + + + + + def errorCapturingMultiUnitsInterval(self): + + localctx = SqlBaseParser.ErrorCapturingMultiUnitsIntervalContext(self, self._ctx, self.state) + self.enterRule(localctx, 262, self.RULE_errorCapturingMultiUnitsInterval) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3157 + localctx.body = self.multiUnitsInterval() + self.state = 3159 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,402,self._ctx) + if la_ == 1: + self.state = 3158 + self.unitToUnitInterval() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class MultiUnitsIntervalContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self._unitInMultiUnits = None # UnitInMultiUnitsContext + self.unit = list() # of UnitInMultiUnitsContexts + + def intervalValue(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.IntervalValueContext) + else: + return self.getTypedRuleContext(SqlBaseParser.IntervalValueContext,i) + + + def unitInMultiUnits(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.UnitInMultiUnitsContext) + else: + return self.getTypedRuleContext(SqlBaseParser.UnitInMultiUnitsContext,i) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_multiUnitsInterval + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMultiUnitsInterval" ): + listener.enterMultiUnitsInterval(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMultiUnitsInterval" ): + listener.exitMultiUnitsInterval(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMultiUnitsInterval" ): + return visitor.visitMultiUnitsInterval(self) + else: + return visitor.visitChildren(self) + + + + + def multiUnitsInterval(self): + + localctx = SqlBaseParser.MultiUnitsIntervalContext(self, self._ctx, self.state) + self.enterRule(localctx, 264, self.RULE_multiUnitsInterval) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3164 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 3161 + self.intervalValue() + self.state = 3162 + localctx._unitInMultiUnits = self.unitInMultiUnits() + localctx.unit.append(localctx._unitInMultiUnits) + + else: + raise NoViableAltException(self) + self.state = 3166 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,403,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ErrorCapturingUnitToUnitIntervalContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.body = None # UnitToUnitIntervalContext + self.error1 = None # MultiUnitsIntervalContext + self.error2 = None # UnitToUnitIntervalContext + + def unitToUnitInterval(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.UnitToUnitIntervalContext) + else: + return self.getTypedRuleContext(SqlBaseParser.UnitToUnitIntervalContext,i) + + + def multiUnitsInterval(self): + return self.getTypedRuleContext(SqlBaseParser.MultiUnitsIntervalContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_errorCapturingUnitToUnitInterval + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterErrorCapturingUnitToUnitInterval" ): + listener.enterErrorCapturingUnitToUnitInterval(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitErrorCapturingUnitToUnitInterval" ): + listener.exitErrorCapturingUnitToUnitInterval(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitErrorCapturingUnitToUnitInterval" ): + return visitor.visitErrorCapturingUnitToUnitInterval(self) + else: + return visitor.visitChildren(self) + + + + + def errorCapturingUnitToUnitInterval(self): + + localctx = SqlBaseParser.ErrorCapturingUnitToUnitIntervalContext(self, self._ctx, self.state) + self.enterRule(localctx, 266, self.RULE_errorCapturingUnitToUnitInterval) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3168 + localctx.body = self.unitToUnitInterval() + self.state = 3171 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,404,self._ctx) + if la_ == 1: + self.state = 3169 + localctx.error1 = self.multiUnitsInterval() + + elif la_ == 2: + self.state = 3170 + localctx.error2 = self.unitToUnitInterval() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnitToUnitIntervalContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.value = None # IntervalValueContext + self.from_ = None # UnitInUnitToUnitContext + self.to = None # UnitInUnitToUnitContext + + def TO(self): + return self.getToken(SqlBaseParser.TO, 0) + + def intervalValue(self): + return self.getTypedRuleContext(SqlBaseParser.IntervalValueContext,0) + + + def unitInUnitToUnit(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.UnitInUnitToUnitContext) + else: + return self.getTypedRuleContext(SqlBaseParser.UnitInUnitToUnitContext,i) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_unitToUnitInterval + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnitToUnitInterval" ): + listener.enterUnitToUnitInterval(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnitToUnitInterval" ): + listener.exitUnitToUnitInterval(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnitToUnitInterval" ): + return visitor.visitUnitToUnitInterval(self) + else: + return visitor.visitChildren(self) + + + + + def unitToUnitInterval(self): + + localctx = SqlBaseParser.UnitToUnitIntervalContext(self, self._ctx, self.state) + self.enterRule(localctx, 268, self.RULE_unitToUnitInterval) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3173 + localctx.value = self.intervalValue() + self.state = 3174 + localctx.from_ = self.unitInUnitToUnit() + self.state = 3175 + self.match(SqlBaseParser.TO) + self.state = 3176 + localctx.to = self.unitInUnitToUnit() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class IntervalValueContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def INTEGER_VALUE(self): + return self.getToken(SqlBaseParser.INTEGER_VALUE, 0) + + def DECIMAL_VALUE(self): + return self.getToken(SqlBaseParser.DECIMAL_VALUE, 0) + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def PLUS(self): + return self.getToken(SqlBaseParser.PLUS, 0) + + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_intervalValue + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIntervalValue" ): + listener.enterIntervalValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIntervalValue" ): + listener.exitIntervalValue(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitIntervalValue" ): + return visitor.visitIntervalValue(self) + else: + return visitor.visitChildren(self) + + + + + def intervalValue(self): + + localctx = SqlBaseParser.IntervalValueContext(self, self._ctx, self.state) + self.enterRule(localctx, 270, self.RULE_intervalValue) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3179 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==316 or _la==317: + self.state = 3178 + _la = self._input.LA(1) + if not(_la==316 or _la==317): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + self.state = 3184 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [335]: + self.state = 3181 + self.match(SqlBaseParser.INTEGER_VALUE) + pass + elif token in [337]: + self.state = 3182 + self.match(SqlBaseParser.DECIMAL_VALUE) + pass + elif token in [330, 331]: + self.state = 3183 + self.stringLit() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnitInMultiUnitsContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def NANOSECOND(self): + return self.getToken(SqlBaseParser.NANOSECOND, 0) + + def NANOSECONDS(self): + return self.getToken(SqlBaseParser.NANOSECONDS, 0) + + def MICROSECOND(self): + return self.getToken(SqlBaseParser.MICROSECOND, 0) + + def MICROSECONDS(self): + return self.getToken(SqlBaseParser.MICROSECONDS, 0) + + def MILLISECOND(self): + return self.getToken(SqlBaseParser.MILLISECOND, 0) + + def MILLISECONDS(self): + return self.getToken(SqlBaseParser.MILLISECONDS, 0) + + def SECOND(self): + return self.getToken(SqlBaseParser.SECOND, 0) + + def SECONDS(self): + return self.getToken(SqlBaseParser.SECONDS, 0) + + def MINUTE(self): + return self.getToken(SqlBaseParser.MINUTE, 0) + + def MINUTES(self): + return self.getToken(SqlBaseParser.MINUTES, 0) + + def HOUR(self): + return self.getToken(SqlBaseParser.HOUR, 0) + + def HOURS(self): + return self.getToken(SqlBaseParser.HOURS, 0) + + def DAY(self): + return self.getToken(SqlBaseParser.DAY, 0) + + def DAYS(self): + return self.getToken(SqlBaseParser.DAYS, 0) + + def WEEK(self): + return self.getToken(SqlBaseParser.WEEK, 0) + + def WEEKS(self): + return self.getToken(SqlBaseParser.WEEKS, 0) + + def MONTH(self): + return self.getToken(SqlBaseParser.MONTH, 0) + + def MONTHS(self): + return self.getToken(SqlBaseParser.MONTHS, 0) + + def YEAR(self): + return self.getToken(SqlBaseParser.YEAR, 0) + + def YEARS(self): + return self.getToken(SqlBaseParser.YEARS, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_unitInMultiUnits + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnitInMultiUnits" ): + listener.enterUnitInMultiUnits(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnitInMultiUnits" ): + listener.exitUnitInMultiUnits(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnitInMultiUnits" ): + return visitor.visitUnitInMultiUnits(self) + else: + return visitor.visitChildren(self) + + + + + def unitInMultiUnits(self): + + localctx = SqlBaseParser.UnitInMultiUnitsContext(self, self._ctx, self.state) + self.enterRule(localctx, 272, self.RULE_unitInMultiUnits) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3186 + _la = self._input.LA(1) + if not(_la==61 or _la==62 or ((((_la - 117)) & ~0x3f) == 0 and ((1 << (_la - 117)) & 7035774906138627) != 0) or _la==229 or _la==230 or ((((_la - 298)) & ~0x3f) == 0 and ((1 << (_la - 298)) & 387) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class UnitInUnitToUnitContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def SECOND(self): + return self.getToken(SqlBaseParser.SECOND, 0) + + def MINUTE(self): + return self.getToken(SqlBaseParser.MINUTE, 0) + + def HOUR(self): + return self.getToken(SqlBaseParser.HOUR, 0) + + def DAY(self): + return self.getToken(SqlBaseParser.DAY, 0) + + def MONTH(self): + return self.getToken(SqlBaseParser.MONTH, 0) + + def YEAR(self): + return self.getToken(SqlBaseParser.YEAR, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_unitInUnitToUnit + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnitInUnitToUnit" ): + listener.enterUnitInUnitToUnit(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnitInUnitToUnit" ): + listener.exitUnitInUnitToUnit(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnitInUnitToUnit" ): + return visitor.visitUnitInUnitToUnit(self) + else: + return visitor.visitChildren(self) + + + + + def unitInUnitToUnit(self): + + localctx = SqlBaseParser.UnitInUnitToUnitContext(self, self._ctx, self.state) + self.enterRule(localctx, 274, self.RULE_unitInUnitToUnit) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3188 + _la = self._input.LA(1) + if not(_la==61 or ((((_la - 117)) & ~0x3f) == 0 and ((1 << (_la - 117)) & 87960930222081) != 0) or _la==229 or _la==305): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ColPositionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.position = None # Token + self.afterCol = None # ErrorCapturingIdentifierContext + + def FIRST(self): + return self.getToken(SqlBaseParser.FIRST, 0) + + def AFTER(self): + return self.getToken(SqlBaseParser.AFTER, 0) + + def errorCapturingIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_colPosition + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterColPosition" ): + listener.enterColPosition(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitColPosition" ): + listener.exitColPosition(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitColPosition" ): + return visitor.visitColPosition(self) + else: + return visitor.visitChildren(self) + + + + + def colPosition(self): + + localctx = SqlBaseParser.ColPositionContext(self, self._ctx, self.state) + self.enterRule(localctx, 276, self.RULE_colPosition) + try: + self.state = 3193 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [101]: + self.enterOuterAlt(localctx, 1) + self.state = 3190 + localctx.position = self.match(SqlBaseParser.FIRST) + pass + elif token in [9]: + self.enterOuterAlt(localctx, 2) + self.state = 3191 + localctx.position = self.match(SqlBaseParser.AFTER) + self.state = 3192 + localctx.afterCol = self.errorCapturingIdentifier() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class DataTypeContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_dataType + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class ComplexDataTypeContext(DataTypeContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.DataTypeContext + super().__init__(parser) + self.complex_ = None # Token + self.copyFrom(ctx) + + def LT(self): + return self.getToken(SqlBaseParser.LT, 0) + def dataType(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.DataTypeContext) + else: + return self.getTypedRuleContext(SqlBaseParser.DataTypeContext,i) + + def GT(self): + return self.getToken(SqlBaseParser.GT, 0) + def ARRAY(self): + return self.getToken(SqlBaseParser.ARRAY, 0) + def COMMA(self): + return self.getToken(SqlBaseParser.COMMA, 0) + def MAP(self): + return self.getToken(SqlBaseParser.MAP, 0) + def STRUCT(self): + return self.getToken(SqlBaseParser.STRUCT, 0) + def NEQ(self): + return self.getToken(SqlBaseParser.NEQ, 0) + def complexColTypeList(self): + return self.getTypedRuleContext(SqlBaseParser.ComplexColTypeListContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterComplexDataType" ): + listener.enterComplexDataType(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitComplexDataType" ): + listener.exitComplexDataType(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitComplexDataType" ): + return visitor.visitComplexDataType(self) + else: + return visitor.visitChildren(self) + + + class YearMonthIntervalDataTypeContext(DataTypeContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.DataTypeContext + super().__init__(parser) + self.from_ = None # Token + self.to = None # Token + self.copyFrom(ctx) + + def INTERVAL(self): + return self.getToken(SqlBaseParser.INTERVAL, 0) + def YEAR(self): + return self.getToken(SqlBaseParser.YEAR, 0) + def MONTH(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.MONTH) + else: + return self.getToken(SqlBaseParser.MONTH, i) + def TO(self): + return self.getToken(SqlBaseParser.TO, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterYearMonthIntervalDataType" ): + listener.enterYearMonthIntervalDataType(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitYearMonthIntervalDataType" ): + listener.exitYearMonthIntervalDataType(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitYearMonthIntervalDataType" ): + return visitor.visitYearMonthIntervalDataType(self) + else: + return visitor.visitChildren(self) + + + class DayTimeIntervalDataTypeContext(DataTypeContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.DataTypeContext + super().__init__(parser) + self.from_ = None # Token + self.to = None # Token + self.copyFrom(ctx) + + def INTERVAL(self): + return self.getToken(SqlBaseParser.INTERVAL, 0) + def DAY(self): + return self.getToken(SqlBaseParser.DAY, 0) + def HOUR(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.HOUR) + else: + return self.getToken(SqlBaseParser.HOUR, i) + def MINUTE(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.MINUTE) + else: + return self.getToken(SqlBaseParser.MINUTE, i) + def SECOND(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.SECOND) + else: + return self.getToken(SqlBaseParser.SECOND, i) + def TO(self): + return self.getToken(SqlBaseParser.TO, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDayTimeIntervalDataType" ): + listener.enterDayTimeIntervalDataType(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDayTimeIntervalDataType" ): + listener.exitDayTimeIntervalDataType(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDayTimeIntervalDataType" ): + return visitor.visitDayTimeIntervalDataType(self) + else: + return visitor.visitChildren(self) + + + class PrimitiveDataTypeContext(DataTypeContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.DataTypeContext + super().__init__(parser) + self.copyFrom(ctx) + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def INTEGER_VALUE(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.INTEGER_VALUE) + else: + return self.getToken(SqlBaseParser.INTEGER_VALUE, i) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPrimitiveDataType" ): + listener.enterPrimitiveDataType(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPrimitiveDataType" ): + listener.exitPrimitiveDataType(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitPrimitiveDataType" ): + return visitor.visitPrimitiveDataType(self) + else: + return visitor.visitChildren(self) + + + + def dataType(self): + + localctx = SqlBaseParser.DataTypeContext(self, self._ctx, self.state) + self.enterRule(localctx, 278, self.RULE_dataType) + self._la = 0 # Token type + try: + self.state = 3241 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,414,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.ComplexDataTypeContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 3195 + localctx.complex_ = self.match(SqlBaseParser.ARRAY) + self.state = 3196 + self.match(SqlBaseParser.LT) + self.state = 3197 + self.dataType() + self.state = 3198 + self.match(SqlBaseParser.GT) + pass + + elif la_ == 2: + localctx = SqlBaseParser.ComplexDataTypeContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 3200 + localctx.complex_ = self.match(SqlBaseParser.MAP) + self.state = 3201 + self.match(SqlBaseParser.LT) + self.state = 3202 + self.dataType() + self.state = 3203 + self.match(SqlBaseParser.COMMA) + self.state = 3204 + self.dataType() + self.state = 3205 + self.match(SqlBaseParser.GT) + pass + + elif la_ == 3: + localctx = SqlBaseParser.ComplexDataTypeContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 3207 + localctx.complex_ = self.match(SqlBaseParser.STRUCT) + self.state = 3214 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [312]: + self.state = 3208 + self.match(SqlBaseParser.LT) + self.state = 3210 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -256) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -1) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -1) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 4503599627370495) != 0) or ((((_la - 331)) & ~0x3f) == 0 and ((1 << (_la - 331)) & 3073) != 0): + self.state = 3209 + self.complexColTypeList() + + + self.state = 3212 + self.match(SqlBaseParser.GT) + pass + elif token in [310]: + self.state = 3213 + self.match(SqlBaseParser.NEQ) + pass + else: + raise NoViableAltException(self) + + pass + + elif la_ == 4: + localctx = SqlBaseParser.YearMonthIntervalDataTypeContext(self, localctx) + self.enterOuterAlt(localctx, 4) + self.state = 3216 + self.match(SqlBaseParser.INTERVAL) + self.state = 3217 + localctx.from_ = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==163 or _la==305): + localctx.from_ = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 3220 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,410,self._ctx) + if la_ == 1: + self.state = 3218 + self.match(SqlBaseParser.TO) + self.state = 3219 + localctx.to = self.match(SqlBaseParser.MONTH) + + + pass + + elif la_ == 5: + localctx = SqlBaseParser.DayTimeIntervalDataTypeContext(self, localctx) + self.enterOuterAlt(localctx, 5) + self.state = 3222 + self.match(SqlBaseParser.INTERVAL) + self.state = 3223 + localctx.from_ = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==61 or _la==117 or _la==161 or _la==229): + localctx.from_ = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 3226 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,411,self._ctx) + if la_ == 1: + self.state = 3224 + self.match(SqlBaseParser.TO) + self.state = 3225 + localctx.to = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==117 or _la==161 or _la==229): + localctx.to = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + + + pass + + elif la_ == 6: + localctx = SqlBaseParser.PrimitiveDataTypeContext(self, localctx) + self.enterOuterAlt(localctx, 6) + self.state = 3228 + self.identifier() + self.state = 3239 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,413,self._ctx) + if la_ == 1: + self.state = 3229 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3230 + self.match(SqlBaseParser.INTEGER_VALUE) + self.state = 3235 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 3231 + self.match(SqlBaseParser.COMMA) + self.state = 3232 + self.match(SqlBaseParser.INTEGER_VALUE) + self.state = 3237 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 3238 + self.match(SqlBaseParser.RIGHT_PAREN) + + + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class QualifiedColTypeWithPositionListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def qualifiedColTypeWithPosition(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.QualifiedColTypeWithPositionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.QualifiedColTypeWithPositionContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_qualifiedColTypeWithPositionList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterQualifiedColTypeWithPositionList" ): + listener.enterQualifiedColTypeWithPositionList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitQualifiedColTypeWithPositionList" ): + listener.exitQualifiedColTypeWithPositionList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitQualifiedColTypeWithPositionList" ): + return visitor.visitQualifiedColTypeWithPositionList(self) + else: + return visitor.visitChildren(self) + + + + + def qualifiedColTypeWithPositionList(self): + + localctx = SqlBaseParser.QualifiedColTypeWithPositionListContext(self, self._ctx, self.state) + self.enterRule(localctx, 280, self.RULE_qualifiedColTypeWithPositionList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3243 + self.qualifiedColTypeWithPosition() + self.state = 3248 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 3244 + self.match(SqlBaseParser.COMMA) + self.state = 3245 + self.qualifiedColTypeWithPosition() + self.state = 3250 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class QualifiedColTypeWithPositionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.name = None # MultipartIdentifierContext + + def dataType(self): + return self.getTypedRuleContext(SqlBaseParser.DataTypeContext,0) + + + def multipartIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.MultipartIdentifierContext,0) + + + def colDefinitionDescriptorWithPosition(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ColDefinitionDescriptorWithPositionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ColDefinitionDescriptorWithPositionContext,i) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_qualifiedColTypeWithPosition + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterQualifiedColTypeWithPosition" ): + listener.enterQualifiedColTypeWithPosition(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitQualifiedColTypeWithPosition" ): + listener.exitQualifiedColTypeWithPosition(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitQualifiedColTypeWithPosition" ): + return visitor.visitQualifiedColTypeWithPosition(self) + else: + return visitor.visitChildren(self) + + + + + def qualifiedColTypeWithPosition(self): + + localctx = SqlBaseParser.QualifiedColTypeWithPositionContext(self, self._ctx, self.state) + self.enterRule(localctx, 282, self.RULE_qualifiedColTypeWithPosition) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3251 + localctx.name = self.multipartIdentifier() + self.state = 3252 + self.dataType() + self.state = 3256 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==9 or _la==45 or _la==70 or _la==101 or _la==172: + self.state = 3253 + self.colDefinitionDescriptorWithPosition() + self.state = 3258 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ColDefinitionDescriptorWithPositionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + + def NULL(self): + return self.getToken(SqlBaseParser.NULL, 0) + + def defaultExpression(self): + return self.getTypedRuleContext(SqlBaseParser.DefaultExpressionContext,0) + + + def commentSpec(self): + return self.getTypedRuleContext(SqlBaseParser.CommentSpecContext,0) + + + def colPosition(self): + return self.getTypedRuleContext(SqlBaseParser.ColPositionContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_colDefinitionDescriptorWithPosition + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterColDefinitionDescriptorWithPosition" ): + listener.enterColDefinitionDescriptorWithPosition(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitColDefinitionDescriptorWithPosition" ): + listener.exitColDefinitionDescriptorWithPosition(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitColDefinitionDescriptorWithPosition" ): + return visitor.visitColDefinitionDescriptorWithPosition(self) + else: + return visitor.visitChildren(self) + + + + + def colDefinitionDescriptorWithPosition(self): + + localctx = SqlBaseParser.ColDefinitionDescriptorWithPositionContext(self, self._ctx, self.state) + self.enterRule(localctx, 284, self.RULE_colDefinitionDescriptorWithPosition) + try: + self.state = 3264 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [172]: + self.enterOuterAlt(localctx, 1) + self.state = 3259 + self.match(SqlBaseParser.NOT) + self.state = 3260 + self.match(SqlBaseParser.NULL) + pass + elif token in [70]: + self.enterOuterAlt(localctx, 2) + self.state = 3261 + self.defaultExpression() + pass + elif token in [45]: + self.enterOuterAlt(localctx, 3) + self.state = 3262 + self.commentSpec() + pass + elif token in [9, 101]: + self.enterOuterAlt(localctx, 4) + self.state = 3263 + self.colPosition() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class DefaultExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def DEFAULT(self): + return self.getToken(SqlBaseParser.DEFAULT, 0) + + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_defaultExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDefaultExpression" ): + listener.enterDefaultExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDefaultExpression" ): + listener.exitDefaultExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDefaultExpression" ): + return visitor.visitDefaultExpression(self) + else: + return visitor.visitChildren(self) + + + + + def defaultExpression(self): + + localctx = SqlBaseParser.DefaultExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 286, self.RULE_defaultExpression) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3266 + self.match(SqlBaseParser.DEFAULT) + self.state = 3267 + self.expression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ColTypeListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def colType(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ColTypeContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ColTypeContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_colTypeList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterColTypeList" ): + listener.enterColTypeList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitColTypeList" ): + listener.exitColTypeList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitColTypeList" ): + return visitor.visitColTypeList(self) + else: + return visitor.visitChildren(self) + + + + + def colTypeList(self): + + localctx = SqlBaseParser.ColTypeListContext(self, self._ctx, self.state) + self.enterRule(localctx, 288, self.RULE_colTypeList) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3269 + self.colType() + self.state = 3274 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,418,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 3270 + self.match(SqlBaseParser.COMMA) + self.state = 3271 + self.colType() + self.state = 3276 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,418,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ColTypeContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.colName = None # ErrorCapturingIdentifierContext + + def dataType(self): + return self.getTypedRuleContext(SqlBaseParser.DataTypeContext,0) + + + def errorCapturingIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,0) + + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + + def NULL(self): + return self.getToken(SqlBaseParser.NULL, 0) + + def commentSpec(self): + return self.getTypedRuleContext(SqlBaseParser.CommentSpecContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_colType + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterColType" ): + listener.enterColType(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitColType" ): + listener.exitColType(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitColType" ): + return visitor.visitColType(self) + else: + return visitor.visitChildren(self) + + + + + def colType(self): + + localctx = SqlBaseParser.ColTypeContext(self, self._ctx, self.state) + self.enterRule(localctx, 290, self.RULE_colType) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3277 + localctx.colName = self.errorCapturingIdentifier() + self.state = 3278 + self.dataType() + self.state = 3281 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,419,self._ctx) + if la_ == 1: + self.state = 3279 + self.match(SqlBaseParser.NOT) + self.state = 3280 + self.match(SqlBaseParser.NULL) + + + self.state = 3284 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,420,self._ctx) + if la_ == 1: + self.state = 3283 + self.commentSpec() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class CreateOrReplaceTableColTypeListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def createOrReplaceTableColType(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.CreateOrReplaceTableColTypeContext) + else: + return self.getTypedRuleContext(SqlBaseParser.CreateOrReplaceTableColTypeContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_createOrReplaceTableColTypeList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCreateOrReplaceTableColTypeList" ): + listener.enterCreateOrReplaceTableColTypeList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCreateOrReplaceTableColTypeList" ): + listener.exitCreateOrReplaceTableColTypeList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCreateOrReplaceTableColTypeList" ): + return visitor.visitCreateOrReplaceTableColTypeList(self) + else: + return visitor.visitChildren(self) + + + + + def createOrReplaceTableColTypeList(self): + + localctx = SqlBaseParser.CreateOrReplaceTableColTypeListContext(self, self._ctx, self.state) + self.enterRule(localctx, 292, self.RULE_createOrReplaceTableColTypeList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3286 + self.createOrReplaceTableColType() + self.state = 3291 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 3287 + self.match(SqlBaseParser.COMMA) + self.state = 3288 + self.createOrReplaceTableColType() + self.state = 3293 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class CreateOrReplaceTableColTypeContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.colName = None # ErrorCapturingIdentifierContext + + def dataType(self): + return self.getTypedRuleContext(SqlBaseParser.DataTypeContext,0) + + + def errorCapturingIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,0) + + + def colDefinitionOption(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ColDefinitionOptionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ColDefinitionOptionContext,i) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_createOrReplaceTableColType + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterCreateOrReplaceTableColType" ): + listener.enterCreateOrReplaceTableColType(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitCreateOrReplaceTableColType" ): + listener.exitCreateOrReplaceTableColType(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitCreateOrReplaceTableColType" ): + return visitor.visitCreateOrReplaceTableColType(self) + else: + return visitor.visitChildren(self) + + + + + def createOrReplaceTableColType(self): + + localctx = SqlBaseParser.CreateOrReplaceTableColTypeContext(self, self._ctx, self.state) + self.enterRule(localctx, 294, self.RULE_createOrReplaceTableColType) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3294 + localctx.colName = self.errorCapturingIdentifier() + self.state = 3295 + self.dataType() + self.state = 3299 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==45 or _la==70 or _la==111 or _la==172: + self.state = 3296 + self.colDefinitionOption() + self.state = 3301 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ColDefinitionOptionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + + def NULL(self): + return self.getToken(SqlBaseParser.NULL, 0) + + def defaultExpression(self): + return self.getTypedRuleContext(SqlBaseParser.DefaultExpressionContext,0) + + + def generationExpression(self): + return self.getTypedRuleContext(SqlBaseParser.GenerationExpressionContext,0) + + + def commentSpec(self): + return self.getTypedRuleContext(SqlBaseParser.CommentSpecContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_colDefinitionOption + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterColDefinitionOption" ): + listener.enterColDefinitionOption(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitColDefinitionOption" ): + listener.exitColDefinitionOption(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitColDefinitionOption" ): + return visitor.visitColDefinitionOption(self) + else: + return visitor.visitChildren(self) + + + + + def colDefinitionOption(self): + + localctx = SqlBaseParser.ColDefinitionOptionContext(self, self._ctx, self.state) + self.enterRule(localctx, 296, self.RULE_colDefinitionOption) + try: + self.state = 3307 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [172]: + self.enterOuterAlt(localctx, 1) + self.state = 3302 + self.match(SqlBaseParser.NOT) + self.state = 3303 + self.match(SqlBaseParser.NULL) + pass + elif token in [70]: + self.enterOuterAlt(localctx, 2) + self.state = 3304 + self.defaultExpression() + pass + elif token in [111]: + self.enterOuterAlt(localctx, 3) + self.state = 3305 + self.generationExpression() + pass + elif token in [45]: + self.enterOuterAlt(localctx, 4) + self.state = 3306 + self.commentSpec() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class GenerationExpressionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def GENERATED(self): + return self.getToken(SqlBaseParser.GENERATED, 0) + + def ALWAYS(self): + return self.getToken(SqlBaseParser.ALWAYS, 0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_generationExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterGenerationExpression" ): + listener.enterGenerationExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitGenerationExpression" ): + listener.exitGenerationExpression(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitGenerationExpression" ): + return visitor.visitGenerationExpression(self) + else: + return visitor.visitChildren(self) + + + + + def generationExpression(self): + + localctx = SqlBaseParser.GenerationExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 298, self.RULE_generationExpression) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3309 + self.match(SqlBaseParser.GENERATED) + self.state = 3310 + self.match(SqlBaseParser.ALWAYS) + self.state = 3311 + self.match(SqlBaseParser.AS) + self.state = 3312 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3313 + self.expression() + self.state = 3314 + self.match(SqlBaseParser.RIGHT_PAREN) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ComplexColTypeListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def complexColType(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ComplexColTypeContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ComplexColTypeContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_complexColTypeList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterComplexColTypeList" ): + listener.enterComplexColTypeList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitComplexColTypeList" ): + listener.exitComplexColTypeList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitComplexColTypeList" ): + return visitor.visitComplexColTypeList(self) + else: + return visitor.visitChildren(self) + + + + + def complexColTypeList(self): + + localctx = SqlBaseParser.ComplexColTypeListContext(self, self._ctx, self.state) + self.enterRule(localctx, 300, self.RULE_complexColTypeList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3316 + self.complexColType() + self.state = 3321 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 3317 + self.match(SqlBaseParser.COMMA) + self.state = 3318 + self.complexColType() + self.state = 3323 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ComplexColTypeContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def dataType(self): + return self.getTypedRuleContext(SqlBaseParser.DataTypeContext,0) + + + def COLON(self): + return self.getToken(SqlBaseParser.COLON, 0) + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + + def NULL(self): + return self.getToken(SqlBaseParser.NULL, 0) + + def commentSpec(self): + return self.getTypedRuleContext(SqlBaseParser.CommentSpecContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_complexColType + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterComplexColType" ): + listener.enterComplexColType(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitComplexColType" ): + listener.exitComplexColType(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitComplexColType" ): + return visitor.visitComplexColType(self) + else: + return visitor.visitChildren(self) + + + + + def complexColType(self): + + localctx = SqlBaseParser.ComplexColTypeContext(self, self._ctx, self.state) + self.enterRule(localctx, 302, self.RULE_complexColType) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3324 + self.identifier() + self.state = 3326 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==326: + self.state = 3325 + self.match(SqlBaseParser.COLON) + + + self.state = 3328 + self.dataType() + self.state = 3331 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==172: + self.state = 3329 + self.match(SqlBaseParser.NOT) + self.state = 3330 + self.match(SqlBaseParser.NULL) + + + self.state = 3334 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==45: + self.state = 3333 + self.commentSpec() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class WhenClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.condition = None # ExpressionContext + self.result = None # ExpressionContext + + def WHEN(self): + return self.getToken(SqlBaseParser.WHEN, 0) + + def THEN(self): + return self.getToken(SqlBaseParser.THEN, 0) + + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_whenClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterWhenClause" ): + listener.enterWhenClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitWhenClause" ): + listener.exitWhenClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitWhenClause" ): + return visitor.visitWhenClause(self) + else: + return visitor.visitChildren(self) + + + + + def whenClause(self): + + localctx = SqlBaseParser.WhenClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 304, self.RULE_whenClause) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3336 + self.match(SqlBaseParser.WHEN) + self.state = 3337 + localctx.condition = self.expression() + self.state = 3338 + self.match(SqlBaseParser.THEN) + self.state = 3339 + localctx.result = self.expression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class WindowClauseContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def WINDOW(self): + return self.getToken(SqlBaseParser.WINDOW, 0) + + def namedWindow(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.NamedWindowContext) + else: + return self.getTypedRuleContext(SqlBaseParser.NamedWindowContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_windowClause + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterWindowClause" ): + listener.enterWindowClause(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitWindowClause" ): + listener.exitWindowClause(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitWindowClause" ): + return visitor.visitWindowClause(self) + else: + return visitor.visitChildren(self) + + + + + def windowClause(self): + + localctx = SqlBaseParser.WindowClauseContext(self, self._ctx, self.state) + self.enterRule(localctx, 306, self.RULE_windowClause) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3341 + self.match(SqlBaseParser.WINDOW) + self.state = 3342 + self.namedWindow() + self.state = 3347 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,428,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 3343 + self.match(SqlBaseParser.COMMA) + self.state = 3344 + self.namedWindow() + self.state = 3349 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,428,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NamedWindowContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.name = None # ErrorCapturingIdentifierContext + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def windowSpec(self): + return self.getTypedRuleContext(SqlBaseParser.WindowSpecContext,0) + + + def errorCapturingIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_namedWindow + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNamedWindow" ): + listener.enterNamedWindow(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNamedWindow" ): + listener.exitNamedWindow(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNamedWindow" ): + return visitor.visitNamedWindow(self) + else: + return visitor.visitChildren(self) + + + + + def namedWindow(self): + + localctx = SqlBaseParser.NamedWindowContext(self, self._ctx, self.state) + self.enterRule(localctx, 308, self.RULE_namedWindow) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3350 + localctx.name = self.errorCapturingIdentifier() + self.state = 3351 + self.match(SqlBaseParser.AS) + self.state = 3352 + self.windowSpec() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class WindowSpecContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_windowSpec + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class WindowRefContext(WindowSpecContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.WindowSpecContext + super().__init__(parser) + self.name = None # ErrorCapturingIdentifierContext + self.copyFrom(ctx) + + def errorCapturingIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierContext,0) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterWindowRef" ): + listener.enterWindowRef(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitWindowRef" ): + listener.exitWindowRef(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitWindowRef" ): + return visitor.visitWindowRef(self) + else: + return visitor.visitChildren(self) + + + class WindowDefContext(WindowSpecContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.WindowSpecContext + super().__init__(parser) + self._expression = None # ExpressionContext + self.partition = list() # of ExpressionContexts + self.copyFrom(ctx) + + def LEFT_PAREN(self): + return self.getToken(SqlBaseParser.LEFT_PAREN, 0) + def RIGHT_PAREN(self): + return self.getToken(SqlBaseParser.RIGHT_PAREN, 0) + def CLUSTER(self): + return self.getToken(SqlBaseParser.CLUSTER, 0) + def BY(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.BY) + else: + return self.getToken(SqlBaseParser.BY, i) + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.ExpressionContext) + else: + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,i) + + def windowFrame(self): + return self.getTypedRuleContext(SqlBaseParser.WindowFrameContext,0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + def sortItem(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.SortItemContext) + else: + return self.getTypedRuleContext(SqlBaseParser.SortItemContext,i) + + def PARTITION(self): + return self.getToken(SqlBaseParser.PARTITION, 0) + def DISTRIBUTE(self): + return self.getToken(SqlBaseParser.DISTRIBUTE, 0) + def ORDER(self): + return self.getToken(SqlBaseParser.ORDER, 0) + def SORT(self): + return self.getToken(SqlBaseParser.SORT, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterWindowDef" ): + listener.enterWindowDef(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitWindowDef" ): + listener.exitWindowDef(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitWindowDef" ): + return visitor.visitWindowDef(self) + else: + return visitor.visitChildren(self) + + + + def windowSpec(self): + + localctx = SqlBaseParser.WindowSpecContext(self, self._ctx, self.state) + self.enterRule(localctx, 310, self.RULE_windowSpec) + self._la = 0 # Token type + try: + self.state = 3400 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,436,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.WindowRefContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 3354 + localctx.name = self.errorCapturingIdentifier() + pass + + elif la_ == 2: + localctx = SqlBaseParser.WindowRefContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 3355 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3356 + localctx.name = self.errorCapturingIdentifier() + self.state = 3357 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + elif la_ == 3: + localctx = SqlBaseParser.WindowDefContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 3359 + self.match(SqlBaseParser.LEFT_PAREN) + self.state = 3394 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [38]: + self.state = 3360 + self.match(SqlBaseParser.CLUSTER) + self.state = 3361 + self.match(SqlBaseParser.BY) + self.state = 3362 + localctx._expression = self.expression() + localctx.partition.append(localctx._expression) + self.state = 3367 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 3363 + self.match(SqlBaseParser.COMMA) + self.state = 3364 + localctx._expression = self.expression() + localctx.partition.append(localctx._expression) + self.state = 3369 + self._errHandler.sync(self) + _la = self._input.LA(1) + + pass + elif token in [3, 80, 182, 190, 206, 228, 245]: + self.state = 3380 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==80 or _la==190: + self.state = 3370 + _la = self._input.LA(1) + if not(_la==80 or _la==190): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 3371 + self.match(SqlBaseParser.BY) + self.state = 3372 + localctx._expression = self.expression() + localctx.partition.append(localctx._expression) + self.state = 3377 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 3373 + self.match(SqlBaseParser.COMMA) + self.state = 3374 + localctx._expression = self.expression() + localctx.partition.append(localctx._expression) + self.state = 3379 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + self.state = 3392 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==182 or _la==245: + self.state = 3382 + _la = self._input.LA(1) + if not(_la==182 or _la==245): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 3383 + self.match(SqlBaseParser.BY) + self.state = 3384 + self.sortItem() + self.state = 3389 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 3385 + self.match(SqlBaseParser.COMMA) + self.state = 3386 + self.sortItem() + self.state = 3391 + self._errHandler.sync(self) + _la = self._input.LA(1) + + + + pass + else: + raise NoViableAltException(self) + + self.state = 3397 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==206 or _la==228: + self.state = 3396 + self.windowFrame() + + + self.state = 3399 + self.match(SqlBaseParser.RIGHT_PAREN) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class WindowFrameContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.frameType = None # Token + self.start = None # FrameBoundContext + self.end = None # FrameBoundContext + + def RANGE(self): + return self.getToken(SqlBaseParser.RANGE, 0) + + def frameBound(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.FrameBoundContext) + else: + return self.getTypedRuleContext(SqlBaseParser.FrameBoundContext,i) + + + def ROWS(self): + return self.getToken(SqlBaseParser.ROWS, 0) + + def BETWEEN(self): + return self.getToken(SqlBaseParser.BETWEEN, 0) + + def AND(self): + return self.getToken(SqlBaseParser.AND, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_windowFrame + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterWindowFrame" ): + listener.enterWindowFrame(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitWindowFrame" ): + listener.exitWindowFrame(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitWindowFrame" ): + return visitor.visitWindowFrame(self) + else: + return visitor.visitChildren(self) + + + + + def windowFrame(self): + + localctx = SqlBaseParser.WindowFrameContext(self, self._ctx, self.state) + self.enterRule(localctx, 312, self.RULE_windowFrame) + try: + self.state = 3418 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,437,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 3402 + localctx.frameType = self.match(SqlBaseParser.RANGE) + self.state = 3403 + localctx.start = self.frameBound() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 3404 + localctx.frameType = self.match(SqlBaseParser.ROWS) + self.state = 3405 + localctx.start = self.frameBound() + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 3406 + localctx.frameType = self.match(SqlBaseParser.RANGE) + self.state = 3407 + self.match(SqlBaseParser.BETWEEN) + self.state = 3408 + localctx.start = self.frameBound() + self.state = 3409 + self.match(SqlBaseParser.AND) + self.state = 3410 + localctx.end = self.frameBound() + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 3412 + localctx.frameType = self.match(SqlBaseParser.ROWS) + self.state = 3413 + self.match(SqlBaseParser.BETWEEN) + self.state = 3414 + localctx.start = self.frameBound() + self.state = 3415 + self.match(SqlBaseParser.AND) + self.state = 3416 + localctx.end = self.frameBound() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FrameBoundContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.boundType = None # Token + + def UNBOUNDED(self): + return self.getToken(SqlBaseParser.UNBOUNDED, 0) + + def PRECEDING(self): + return self.getToken(SqlBaseParser.PRECEDING, 0) + + def FOLLOWING(self): + return self.getToken(SqlBaseParser.FOLLOWING, 0) + + def ROW(self): + return self.getToken(SqlBaseParser.ROW, 0) + + def CURRENT(self): + return self.getToken(SqlBaseParser.CURRENT, 0) + + def expression(self): + return self.getTypedRuleContext(SqlBaseParser.ExpressionContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_frameBound + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFrameBound" ): + listener.enterFrameBound(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFrameBound" ): + listener.exitFrameBound(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFrameBound" ): + return visitor.visitFrameBound(self) + else: + return visitor.visitChildren(self) + + + + + def frameBound(self): + + localctx = SqlBaseParser.FrameBoundContext(self, self._ctx, self.state) + self.enterRule(localctx, 314, self.RULE_frameBound) + self._la = 0 # Token type + try: + self.state = 3427 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,438,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 3420 + self.match(SqlBaseParser.UNBOUNDED) + self.state = 3421 + localctx.boundType = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==102 or _la==199): + localctx.boundType = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 3422 + localctx.boundType = self.match(SqlBaseParser.CURRENT) + self.state = 3423 + self.match(SqlBaseParser.ROW) + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 3424 + self.expression() + self.state = 3425 + localctx.boundType = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==102 or _la==199): + localctx.boundType = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class QualifiedNameListContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def qualifiedName(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.QualifiedNameContext) + else: + return self.getTypedRuleContext(SqlBaseParser.QualifiedNameContext,i) + + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.COMMA) + else: + return self.getToken(SqlBaseParser.COMMA, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_qualifiedNameList + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterQualifiedNameList" ): + listener.enterQualifiedNameList(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitQualifiedNameList" ): + listener.exitQualifiedNameList(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitQualifiedNameList" ): + return visitor.visitQualifiedNameList(self) + else: + return visitor.visitChildren(self) + + + + + def qualifiedNameList(self): + + localctx = SqlBaseParser.QualifiedNameListContext(self, self._ctx, self.state) + self.enterRule(localctx, 316, self.RULE_qualifiedNameList) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3429 + self.qualifiedName() + self.state = 3434 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==4: + self.state = 3430 + self.match(SqlBaseParser.COMMA) + self.state = 3431 + self.qualifiedName() + self.state = 3436 + self._errHandler.sync(self) + _la = self._input.LA(1) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FunctionNameContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def qualifiedName(self): + return self.getTypedRuleContext(SqlBaseParser.QualifiedNameContext,0) + + + def FILTER(self): + return self.getToken(SqlBaseParser.FILTER, 0) + + def LEFT(self): + return self.getToken(SqlBaseParser.LEFT, 0) + + def RIGHT(self): + return self.getToken(SqlBaseParser.RIGHT, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_functionName + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFunctionName" ): + listener.enterFunctionName(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFunctionName" ): + listener.exitFunctionName(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFunctionName" ): + return visitor.visitFunctionName(self) + else: + return visitor.visitChildren(self) + + + + + def functionName(self): + + localctx = SqlBaseParser.FunctionNameContext(self, self._ctx, self.state) + self.enterRule(localctx, 318, self.RULE_functionName) + try: + self.state = 3441 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,440,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 3437 + self.qualifiedName() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 3438 + self.match(SqlBaseParser.FILTER) + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 3439 + self.match(SqlBaseParser.LEFT) + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 3440 + self.match(SqlBaseParser.RIGHT) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class QualifiedNameContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def identifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.IdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,i) + + + def DOT(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.DOT) + else: + return self.getToken(SqlBaseParser.DOT, i) + + def getRuleIndex(self): + return SqlBaseParser.RULE_qualifiedName + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterQualifiedName" ): + listener.enterQualifiedName(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitQualifiedName" ): + listener.exitQualifiedName(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitQualifiedName" ): + return visitor.visitQualifiedName(self) + else: + return visitor.visitChildren(self) + + + + + def qualifiedName(self): + + localctx = SqlBaseParser.QualifiedNameContext(self, self._ctx, self.state) + self.enterRule(localctx, 320, self.RULE_qualifiedName) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3443 + self.identifier() + self.state = 3448 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,441,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 3444 + self.match(SqlBaseParser.DOT) + self.state = 3445 + self.identifier() + self.state = 3450 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,441,self._ctx) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ErrorCapturingIdentifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def identifier(self): + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,0) + + + def errorCapturingIdentifierExtra(self): + return self.getTypedRuleContext(SqlBaseParser.ErrorCapturingIdentifierExtraContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_errorCapturingIdentifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterErrorCapturingIdentifier" ): + listener.enterErrorCapturingIdentifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitErrorCapturingIdentifier" ): + listener.exitErrorCapturingIdentifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitErrorCapturingIdentifier" ): + return visitor.visitErrorCapturingIdentifier(self) + else: + return visitor.visitChildren(self) + + + + + def errorCapturingIdentifier(self): + + localctx = SqlBaseParser.ErrorCapturingIdentifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 322, self.RULE_errorCapturingIdentifier) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3451 + self.identifier() + self.state = 3452 + self.errorCapturingIdentifierExtra() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ErrorCapturingIdentifierExtraContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_errorCapturingIdentifierExtra + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class ErrorIdentContext(ErrorCapturingIdentifierExtraContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ErrorCapturingIdentifierExtraContext + super().__init__(parser) + self.copyFrom(ctx) + + def MINUS(self, i:int=None): + if i is None: + return self.getTokens(SqlBaseParser.MINUS) + else: + return self.getToken(SqlBaseParser.MINUS, i) + def identifier(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(SqlBaseParser.IdentifierContext) + else: + return self.getTypedRuleContext(SqlBaseParser.IdentifierContext,i) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterErrorIdent" ): + listener.enterErrorIdent(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitErrorIdent" ): + listener.exitErrorIdent(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitErrorIdent" ): + return visitor.visitErrorIdent(self) + else: + return visitor.visitChildren(self) + + + class RealIdentContext(ErrorCapturingIdentifierExtraContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.ErrorCapturingIdentifierExtraContext + super().__init__(parser) + self.copyFrom(ctx) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterRealIdent" ): + listener.enterRealIdent(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitRealIdent" ): + listener.exitRealIdent(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitRealIdent" ): + return visitor.visitRealIdent(self) + else: + return visitor.visitChildren(self) + + + + def errorCapturingIdentifierExtra(self): + + localctx = SqlBaseParser.ErrorCapturingIdentifierExtraContext(self, self._ctx, self.state) + self.enterRule(localctx, 324, self.RULE_errorCapturingIdentifierExtra) + try: + self.state = 3461 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,443,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.ErrorIdentContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 3456 + self._errHandler.sync(self) + _alt = 1 + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt == 1: + self.state = 3454 + self.match(SqlBaseParser.MINUS) + self.state = 3455 + self.identifier() + + else: + raise NoViableAltException(self) + self.state = 3458 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,442,self._ctx) + + pass + + elif la_ == 2: + localctx = SqlBaseParser.RealIdentContext(self, localctx) + self.enterOuterAlt(localctx, 2) + + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class IdentifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def strictIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.StrictIdentifierContext,0) + + + def strictNonReserved(self): + return self.getTypedRuleContext(SqlBaseParser.StrictNonReservedContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_identifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIdentifier" ): + listener.enterIdentifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIdentifier" ): + listener.exitIdentifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitIdentifier" ): + return visitor.visitIdentifier(self) + else: + return visitor.visitChildren(self) + + + + + def identifier(self): + + localctx = SqlBaseParser.IdentifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 326, self.RULE_identifier) + try: + self.state = 3465 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,444,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 3463 + self.strictIdentifier() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 3464 + self.strictNonReserved() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class StrictIdentifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_strictIdentifier + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class QuotedIdentifierAlternativeContext(StrictIdentifierContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StrictIdentifierContext + super().__init__(parser) + self.copyFrom(ctx) + + def quotedIdentifier(self): + return self.getTypedRuleContext(SqlBaseParser.QuotedIdentifierContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterQuotedIdentifierAlternative" ): + listener.enterQuotedIdentifierAlternative(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitQuotedIdentifierAlternative" ): + listener.exitQuotedIdentifierAlternative(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitQuotedIdentifierAlternative" ): + return visitor.visitQuotedIdentifierAlternative(self) + else: + return visitor.visitChildren(self) + + + class UnquotedIdentifierContext(StrictIdentifierContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.StrictIdentifierContext + super().__init__(parser) + self.copyFrom(ctx) + + def IDENTIFIER(self): + return self.getToken(SqlBaseParser.IDENTIFIER, 0) + def ansiNonReserved(self): + return self.getTypedRuleContext(SqlBaseParser.AnsiNonReservedContext,0) + + def nonReserved(self): + return self.getTypedRuleContext(SqlBaseParser.NonReservedContext,0) + + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterUnquotedIdentifier" ): + listener.enterUnquotedIdentifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitUnquotedIdentifier" ): + listener.exitUnquotedIdentifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitUnquotedIdentifier" ): + return visitor.visitUnquotedIdentifier(self) + else: + return visitor.visitChildren(self) + + + + def strictIdentifier(self): + + localctx = SqlBaseParser.StrictIdentifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 328, self.RULE_strictIdentifier) + try: + self.state = 3471 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,445,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.UnquotedIdentifierContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 3467 + self.match(SqlBaseParser.IDENTIFIER) + pass + + elif la_ == 2: + localctx = SqlBaseParser.QuotedIdentifierAlternativeContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 3468 + self.quotedIdentifier() + pass + + elif la_ == 3: + localctx = SqlBaseParser.UnquotedIdentifierContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 3469 + self.ansiNonReserved() + pass + + elif la_ == 4: + localctx = SqlBaseParser.UnquotedIdentifierContext(self, localctx) + self.enterOuterAlt(localctx, 4) + self.state = 3470 + self.nonReserved() + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class QuotedIdentifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def BACKQUOTED_IDENTIFIER(self): + return self.getToken(SqlBaseParser.BACKQUOTED_IDENTIFIER, 0) + + def DOUBLEQUOTED_STRING(self): + return self.getToken(SqlBaseParser.DOUBLEQUOTED_STRING, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_quotedIdentifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterQuotedIdentifier" ): + listener.enterQuotedIdentifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitQuotedIdentifier" ): + listener.exitQuotedIdentifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitQuotedIdentifier" ): + return visitor.visitQuotedIdentifier(self) + else: + return visitor.visitChildren(self) + + + + + def quotedIdentifier(self): + + localctx = SqlBaseParser.QuotedIdentifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 330, self.RULE_quotedIdentifier) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3473 + _la = self._input.LA(1) + if not(_la==331 or _la==342): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class BackQuotedIdentifierContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def BACKQUOTED_IDENTIFIER(self): + return self.getToken(SqlBaseParser.BACKQUOTED_IDENTIFIER, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_backQuotedIdentifier + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterBackQuotedIdentifier" ): + listener.enterBackQuotedIdentifier(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitBackQuotedIdentifier" ): + listener.exitBackQuotedIdentifier(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitBackQuotedIdentifier" ): + return visitor.visitBackQuotedIdentifier(self) + else: + return visitor.visitChildren(self) + + + + + def backQuotedIdentifier(self): + + localctx = SqlBaseParser.BackQuotedIdentifierContext(self, self._ctx, self.state) + self.enterRule(localctx, 332, self.RULE_backQuotedIdentifier) + try: + self.enterOuterAlt(localctx, 1) + self.state = 3475 + self.match(SqlBaseParser.BACKQUOTED_IDENTIFIER) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NumberContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + + def getRuleIndex(self): + return SqlBaseParser.RULE_number + + + def copyFrom(self, ctx:ParserRuleContext): + super().copyFrom(ctx) + + + + class DecimalLiteralContext(NumberContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.NumberContext + super().__init__(parser) + self.copyFrom(ctx) + + def DECIMAL_VALUE(self): + return self.getToken(SqlBaseParser.DECIMAL_VALUE, 0) + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDecimalLiteral" ): + listener.enterDecimalLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDecimalLiteral" ): + listener.exitDecimalLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDecimalLiteral" ): + return visitor.visitDecimalLiteral(self) + else: + return visitor.visitChildren(self) + + + class BigIntLiteralContext(NumberContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.NumberContext + super().__init__(parser) + self.copyFrom(ctx) + + def BIGINT_LITERAL(self): + return self.getToken(SqlBaseParser.BIGINT_LITERAL, 0) + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterBigIntLiteral" ): + listener.enterBigIntLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitBigIntLiteral" ): + listener.exitBigIntLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitBigIntLiteral" ): + return visitor.visitBigIntLiteral(self) + else: + return visitor.visitChildren(self) + + + class TinyIntLiteralContext(NumberContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.NumberContext + super().__init__(parser) + self.copyFrom(ctx) + + def TINYINT_LITERAL(self): + return self.getToken(SqlBaseParser.TINYINT_LITERAL, 0) + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterTinyIntLiteral" ): + listener.enterTinyIntLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitTinyIntLiteral" ): + listener.exitTinyIntLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitTinyIntLiteral" ): + return visitor.visitTinyIntLiteral(self) + else: + return visitor.visitChildren(self) + + + class LegacyDecimalLiteralContext(NumberContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.NumberContext + super().__init__(parser) + self.copyFrom(ctx) + + def EXPONENT_VALUE(self): + return self.getToken(SqlBaseParser.EXPONENT_VALUE, 0) + def DECIMAL_VALUE(self): + return self.getToken(SqlBaseParser.DECIMAL_VALUE, 0) + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterLegacyDecimalLiteral" ): + listener.enterLegacyDecimalLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitLegacyDecimalLiteral" ): + listener.exitLegacyDecimalLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitLegacyDecimalLiteral" ): + return visitor.visitLegacyDecimalLiteral(self) + else: + return visitor.visitChildren(self) + + + class BigDecimalLiteralContext(NumberContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.NumberContext + super().__init__(parser) + self.copyFrom(ctx) + + def BIGDECIMAL_LITERAL(self): + return self.getToken(SqlBaseParser.BIGDECIMAL_LITERAL, 0) + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterBigDecimalLiteral" ): + listener.enterBigDecimalLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitBigDecimalLiteral" ): + listener.exitBigDecimalLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitBigDecimalLiteral" ): + return visitor.visitBigDecimalLiteral(self) + else: + return visitor.visitChildren(self) + + + class ExponentLiteralContext(NumberContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.NumberContext + super().__init__(parser) + self.copyFrom(ctx) + + def EXPONENT_VALUE(self): + return self.getToken(SqlBaseParser.EXPONENT_VALUE, 0) + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterExponentLiteral" ): + listener.enterExponentLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitExponentLiteral" ): + listener.exitExponentLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitExponentLiteral" ): + return visitor.visitExponentLiteral(self) + else: + return visitor.visitChildren(self) + + + class DoubleLiteralContext(NumberContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.NumberContext + super().__init__(parser) + self.copyFrom(ctx) + + def DOUBLE_LITERAL(self): + return self.getToken(SqlBaseParser.DOUBLE_LITERAL, 0) + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDoubleLiteral" ): + listener.enterDoubleLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDoubleLiteral" ): + listener.exitDoubleLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDoubleLiteral" ): + return visitor.visitDoubleLiteral(self) + else: + return visitor.visitChildren(self) + + + class IntegerLiteralContext(NumberContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.NumberContext + super().__init__(parser) + self.copyFrom(ctx) + + def INTEGER_VALUE(self): + return self.getToken(SqlBaseParser.INTEGER_VALUE, 0) + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterIntegerLiteral" ): + listener.enterIntegerLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitIntegerLiteral" ): + listener.exitIntegerLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitIntegerLiteral" ): + return visitor.visitIntegerLiteral(self) + else: + return visitor.visitChildren(self) + + + class FloatLiteralContext(NumberContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.NumberContext + super().__init__(parser) + self.copyFrom(ctx) + + def FLOAT_LITERAL(self): + return self.getToken(SqlBaseParser.FLOAT_LITERAL, 0) + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterFloatLiteral" ): + listener.enterFloatLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitFloatLiteral" ): + listener.exitFloatLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitFloatLiteral" ): + return visitor.visitFloatLiteral(self) + else: + return visitor.visitChildren(self) + + + class SmallIntLiteralContext(NumberContext): + + def __init__(self, parser, ctx:ParserRuleContext): # actually a SqlBaseParser.NumberContext + super().__init__(parser) + self.copyFrom(ctx) + + def SMALLINT_LITERAL(self): + return self.getToken(SqlBaseParser.SMALLINT_LITERAL, 0) + def MINUS(self): + return self.getToken(SqlBaseParser.MINUS, 0) + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSmallIntLiteral" ): + listener.enterSmallIntLiteral(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSmallIntLiteral" ): + listener.exitSmallIntLiteral(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitSmallIntLiteral" ): + return visitor.visitSmallIntLiteral(self) + else: + return visitor.visitChildren(self) + + + + def number(self): + + localctx = SqlBaseParser.NumberContext(self, self._ctx, self.state) + self.enterRule(localctx, 334, self.RULE_number) + self._la = 0 # Token type + try: + self.state = 3517 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,456,self._ctx) + if la_ == 1: + localctx = SqlBaseParser.ExponentLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 1) + self.state = 3478 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==317: + self.state = 3477 + self.match(SqlBaseParser.MINUS) + + + self.state = 3480 + self.match(SqlBaseParser.EXPONENT_VALUE) + pass + + elif la_ == 2: + localctx = SqlBaseParser.DecimalLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 2) + self.state = 3482 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==317: + self.state = 3481 + self.match(SqlBaseParser.MINUS) + + + self.state = 3484 + self.match(SqlBaseParser.DECIMAL_VALUE) + pass + + elif la_ == 3: + localctx = SqlBaseParser.LegacyDecimalLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 3) + self.state = 3486 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==317: + self.state = 3485 + self.match(SqlBaseParser.MINUS) + + + self.state = 3488 + _la = self._input.LA(1) + if not(_la==336 or _la==337): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + pass + + elif la_ == 4: + localctx = SqlBaseParser.IntegerLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 4) + self.state = 3490 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==317: + self.state = 3489 + self.match(SqlBaseParser.MINUS) + + + self.state = 3492 + self.match(SqlBaseParser.INTEGER_VALUE) + pass + + elif la_ == 5: + localctx = SqlBaseParser.BigIntLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 5) + self.state = 3494 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==317: + self.state = 3493 + self.match(SqlBaseParser.MINUS) + + + self.state = 3496 + self.match(SqlBaseParser.BIGINT_LITERAL) + pass + + elif la_ == 6: + localctx = SqlBaseParser.SmallIntLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 6) + self.state = 3498 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==317: + self.state = 3497 + self.match(SqlBaseParser.MINUS) + + + self.state = 3500 + self.match(SqlBaseParser.SMALLINT_LITERAL) + pass + + elif la_ == 7: + localctx = SqlBaseParser.TinyIntLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 7) + self.state = 3502 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==317: + self.state = 3501 + self.match(SqlBaseParser.MINUS) + + + self.state = 3504 + self.match(SqlBaseParser.TINYINT_LITERAL) + pass + + elif la_ == 8: + localctx = SqlBaseParser.DoubleLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 8) + self.state = 3506 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==317: + self.state = 3505 + self.match(SqlBaseParser.MINUS) + + + self.state = 3508 + self.match(SqlBaseParser.DOUBLE_LITERAL) + pass + + elif la_ == 9: + localctx = SqlBaseParser.FloatLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 9) + self.state = 3510 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==317: + self.state = 3509 + self.match(SqlBaseParser.MINUS) + + + self.state = 3512 + self.match(SqlBaseParser.FLOAT_LITERAL) + pass + + elif la_ == 10: + localctx = SqlBaseParser.BigDecimalLiteralContext(self, localctx) + self.enterOuterAlt(localctx, 10) + self.state = 3514 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==317: + self.state = 3513 + self.match(SqlBaseParser.MINUS) + + + self.state = 3516 + self.match(SqlBaseParser.BIGDECIMAL_LITERAL) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class AlterColumnActionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + self.setOrDrop = None # Token + self.dropDefault = None # Token + + def TYPE(self): + return self.getToken(SqlBaseParser.TYPE, 0) + + def dataType(self): + return self.getTypedRuleContext(SqlBaseParser.DataTypeContext,0) + + + def commentSpec(self): + return self.getTypedRuleContext(SqlBaseParser.CommentSpecContext,0) + + + def colPosition(self): + return self.getTypedRuleContext(SqlBaseParser.ColPositionContext,0) + + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + + def NULL(self): + return self.getToken(SqlBaseParser.NULL, 0) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + + def DROP(self): + return self.getToken(SqlBaseParser.DROP, 0) + + def defaultExpression(self): + return self.getTypedRuleContext(SqlBaseParser.DefaultExpressionContext,0) + + + def DEFAULT(self): + return self.getToken(SqlBaseParser.DEFAULT, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_alterColumnAction + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAlterColumnAction" ): + listener.enterAlterColumnAction(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAlterColumnAction" ): + listener.exitAlterColumnAction(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAlterColumnAction" ): + return visitor.visitAlterColumnAction(self) + else: + return visitor.visitChildren(self) + + + + + def alterColumnAction(self): + + localctx = SqlBaseParser.AlterColumnActionContext(self, self._ctx, self.state) + self.enterRule(localctx, 336, self.RULE_alterColumnAction) + self._la = 0 # Token type + try: + self.state = 3530 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,457,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 3519 + self.match(SqlBaseParser.TYPE) + self.state = 3520 + self.dataType() + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 3521 + self.commentSpec() + pass + + elif la_ == 3: + self.enterOuterAlt(localctx, 3) + self.state = 3522 + self.colPosition() + pass + + elif la_ == 4: + self.enterOuterAlt(localctx, 4) + self.state = 3523 + localctx.setOrDrop = self._input.LT(1) + _la = self._input.LA(1) + if not(_la==82 or _la==239): + localctx.setOrDrop = self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 3524 + self.match(SqlBaseParser.NOT) + self.state = 3525 + self.match(SqlBaseParser.NULL) + pass + + elif la_ == 5: + self.enterOuterAlt(localctx, 5) + self.state = 3526 + self.match(SqlBaseParser.SET) + self.state = 3527 + self.defaultExpression() + pass + + elif la_ == 6: + self.enterOuterAlt(localctx, 6) + self.state = 3528 + localctx.dropDefault = self.match(SqlBaseParser.DROP) + self.state = 3529 + self.match(SqlBaseParser.DEFAULT) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class StringLitContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def STRING(self): + return self.getToken(SqlBaseParser.STRING, 0) + + def DOUBLEQUOTED_STRING(self): + return self.getToken(SqlBaseParser.DOUBLEQUOTED_STRING, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_stringLit + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterStringLit" ): + listener.enterStringLit(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitStringLit" ): + listener.exitStringLit(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitStringLit" ): + return visitor.visitStringLit(self) + else: + return visitor.visitChildren(self) + + + + + def stringLit(self): + + localctx = SqlBaseParser.StringLitContext(self, self._ctx, self.state) + self.enterRule(localctx, 338, self.RULE_stringLit) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3532 + _la = self._input.LA(1) + if not(_la==330 or _la==331): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class CommentContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def NULL(self): + return self.getToken(SqlBaseParser.NULL, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_comment + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterComment" ): + listener.enterComment(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitComment" ): + listener.exitComment(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitComment" ): + return visitor.visitComment(self) + else: + return visitor.visitChildren(self) + + + + + def comment(self): + + localctx = SqlBaseParser.CommentContext(self, self._ctx, self.state) + self.enterRule(localctx, 340, self.RULE_comment) + try: + self.state = 3536 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [330, 331]: + self.enterOuterAlt(localctx, 1) + self.state = 3534 + self.stringLit() + pass + elif token in [173]: + self.enterOuterAlt(localctx, 2) + self.state = 3535 + self.match(SqlBaseParser.NULL) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class VersionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def INTEGER_VALUE(self): + return self.getToken(SqlBaseParser.INTEGER_VALUE, 0) + + def stringLit(self): + return self.getTypedRuleContext(SqlBaseParser.StringLitContext,0) + + + def getRuleIndex(self): + return SqlBaseParser.RULE_version + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterVersion" ): + listener.enterVersion(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitVersion" ): + listener.exitVersion(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitVersion" ): + return visitor.visitVersion(self) + else: + return visitor.visitChildren(self) + + + + + def version(self): + + localctx = SqlBaseParser.VersionContext(self, self._ctx, self.state) + self.enterRule(localctx, 342, self.RULE_version) + try: + self.state = 3540 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [335]: + self.enterOuterAlt(localctx, 1) + self.state = 3538 + self.match(SqlBaseParser.INTEGER_VALUE) + pass + elif token in [330, 331]: + self.enterOuterAlt(localctx, 2) + self.state = 3539 + self.stringLit() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class AnsiNonReservedContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ADD(self): + return self.getToken(SqlBaseParser.ADD, 0) + + def AFTER(self): + return self.getToken(SqlBaseParser.AFTER, 0) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + + def ALWAYS(self): + return self.getToken(SqlBaseParser.ALWAYS, 0) + + def ANALYZE(self): + return self.getToken(SqlBaseParser.ANALYZE, 0) + + def ANTI(self): + return self.getToken(SqlBaseParser.ANTI, 0) + + def ANY_VALUE(self): + return self.getToken(SqlBaseParser.ANY_VALUE, 0) + + def ARCHIVE(self): + return self.getToken(SqlBaseParser.ARCHIVE, 0) + + def ARRAY(self): + return self.getToken(SqlBaseParser.ARRAY, 0) + + def ASC(self): + return self.getToken(SqlBaseParser.ASC, 0) + + def AT(self): + return self.getToken(SqlBaseParser.AT, 0) + + def BETWEEN(self): + return self.getToken(SqlBaseParser.BETWEEN, 0) + + def BUCKET(self): + return self.getToken(SqlBaseParser.BUCKET, 0) + + def BUCKETS(self): + return self.getToken(SqlBaseParser.BUCKETS, 0) + + def BY(self): + return self.getToken(SqlBaseParser.BY, 0) + + def CACHE(self): + return self.getToken(SqlBaseParser.CACHE, 0) + + def CASCADE(self): + return self.getToken(SqlBaseParser.CASCADE, 0) + + def CATALOG(self): + return self.getToken(SqlBaseParser.CATALOG, 0) + + def CATALOGS(self): + return self.getToken(SqlBaseParser.CATALOGS, 0) + + def CHANGE(self): + return self.getToken(SqlBaseParser.CHANGE, 0) + + def CLEAR(self): + return self.getToken(SqlBaseParser.CLEAR, 0) + + def CLUSTER(self): + return self.getToken(SqlBaseParser.CLUSTER, 0) + + def CLUSTERED(self): + return self.getToken(SqlBaseParser.CLUSTERED, 0) + + def CODEGEN(self): + return self.getToken(SqlBaseParser.CODEGEN, 0) + + def COLLECTION(self): + return self.getToken(SqlBaseParser.COLLECTION, 0) + + def COLUMNS(self): + return self.getToken(SqlBaseParser.COLUMNS, 0) + + def COMMENT(self): + return self.getToken(SqlBaseParser.COMMENT, 0) + + def COMMIT(self): + return self.getToken(SqlBaseParser.COMMIT, 0) + + def COMPACT(self): + return self.getToken(SqlBaseParser.COMPACT, 0) + + def COMPACTIONS(self): + return self.getToken(SqlBaseParser.COMPACTIONS, 0) + + def COMPUTE(self): + return self.getToken(SqlBaseParser.COMPUTE, 0) + + def CONCATENATE(self): + return self.getToken(SqlBaseParser.CONCATENATE, 0) + + def COST(self): + return self.getToken(SqlBaseParser.COST, 0) + + def CUBE(self): + return self.getToken(SqlBaseParser.CUBE, 0) + + def CURRENT(self): + return self.getToken(SqlBaseParser.CURRENT, 0) + + def DATA(self): + return self.getToken(SqlBaseParser.DATA, 0) + + def DATABASE(self): + return self.getToken(SqlBaseParser.DATABASE, 0) + + def DATABASES(self): + return self.getToken(SqlBaseParser.DATABASES, 0) + + def DATEADD(self): + return self.getToken(SqlBaseParser.DATEADD, 0) + + def DATEDIFF(self): + return self.getToken(SqlBaseParser.DATEDIFF, 0) + + def DAY(self): + return self.getToken(SqlBaseParser.DAY, 0) + + def DAYS(self): + return self.getToken(SqlBaseParser.DAYS, 0) + + def DAYOFYEAR(self): + return self.getToken(SqlBaseParser.DAYOFYEAR, 0) + + def DBPROPERTIES(self): + return self.getToken(SqlBaseParser.DBPROPERTIES, 0) + + def DEFAULT(self): + return self.getToken(SqlBaseParser.DEFAULT, 0) + + def DEFINED(self): + return self.getToken(SqlBaseParser.DEFINED, 0) + + def DELETE(self): + return self.getToken(SqlBaseParser.DELETE, 0) + + def DELIMITED(self): + return self.getToken(SqlBaseParser.DELIMITED, 0) + + def DESC(self): + return self.getToken(SqlBaseParser.DESC, 0) + + def DESCRIBE(self): + return self.getToken(SqlBaseParser.DESCRIBE, 0) + + def DFS(self): + return self.getToken(SqlBaseParser.DFS, 0) + + def DIRECTORIES(self): + return self.getToken(SqlBaseParser.DIRECTORIES, 0) + + def DIRECTORY(self): + return self.getToken(SqlBaseParser.DIRECTORY, 0) + + def DISTRIBUTE(self): + return self.getToken(SqlBaseParser.DISTRIBUTE, 0) + + def DIV(self): + return self.getToken(SqlBaseParser.DIV, 0) + + def DROP(self): + return self.getToken(SqlBaseParser.DROP, 0) + + def ESCAPED(self): + return self.getToken(SqlBaseParser.ESCAPED, 0) + + def EXCHANGE(self): + return self.getToken(SqlBaseParser.EXCHANGE, 0) + + def EXCLUDE(self): + return self.getToken(SqlBaseParser.EXCLUDE, 0) + + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + + def EXPLAIN(self): + return self.getToken(SqlBaseParser.EXPLAIN, 0) + + def EXPORT(self): + return self.getToken(SqlBaseParser.EXPORT, 0) + + def EXTENDED(self): + return self.getToken(SqlBaseParser.EXTENDED, 0) + + def EXTERNAL(self): + return self.getToken(SqlBaseParser.EXTERNAL, 0) + + def EXTRACT(self): + return self.getToken(SqlBaseParser.EXTRACT, 0) + + def FIELDS(self): + return self.getToken(SqlBaseParser.FIELDS, 0) + + def FILEFORMAT(self): + return self.getToken(SqlBaseParser.FILEFORMAT, 0) + + def FIRST(self): + return self.getToken(SqlBaseParser.FIRST, 0) + + def FOLLOWING(self): + return self.getToken(SqlBaseParser.FOLLOWING, 0) + + def FORMAT(self): + return self.getToken(SqlBaseParser.FORMAT, 0) + + def FORMATTED(self): + return self.getToken(SqlBaseParser.FORMATTED, 0) + + def FUNCTION(self): + return self.getToken(SqlBaseParser.FUNCTION, 0) + + def FUNCTIONS(self): + return self.getToken(SqlBaseParser.FUNCTIONS, 0) + + def GENERATED(self): + return self.getToken(SqlBaseParser.GENERATED, 0) + + def GLOBAL(self): + return self.getToken(SqlBaseParser.GLOBAL, 0) + + def GROUPING(self): + return self.getToken(SqlBaseParser.GROUPING, 0) + + def HOUR(self): + return self.getToken(SqlBaseParser.HOUR, 0) + + def HOURS(self): + return self.getToken(SqlBaseParser.HOURS, 0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + + def IGNORE(self): + return self.getToken(SqlBaseParser.IGNORE, 0) + + def IMPORT(self): + return self.getToken(SqlBaseParser.IMPORT, 0) + + def INCLUDE(self): + return self.getToken(SqlBaseParser.INCLUDE, 0) + + def INDEX(self): + return self.getToken(SqlBaseParser.INDEX, 0) + + def INDEXES(self): + return self.getToken(SqlBaseParser.INDEXES, 0) + + def INPATH(self): + return self.getToken(SqlBaseParser.INPATH, 0) + + def INPUTFORMAT(self): + return self.getToken(SqlBaseParser.INPUTFORMAT, 0) + + def INSERT(self): + return self.getToken(SqlBaseParser.INSERT, 0) + + def INTERVAL(self): + return self.getToken(SqlBaseParser.INTERVAL, 0) + + def ITEMS(self): + return self.getToken(SqlBaseParser.ITEMS, 0) + + def KEYS(self): + return self.getToken(SqlBaseParser.KEYS, 0) + + def LAST(self): + return self.getToken(SqlBaseParser.LAST, 0) + + def LAZY(self): + return self.getToken(SqlBaseParser.LAZY, 0) + + def LIKE(self): + return self.getToken(SqlBaseParser.LIKE, 0) + + def ILIKE(self): + return self.getToken(SqlBaseParser.ILIKE, 0) + + def LIMIT(self): + return self.getToken(SqlBaseParser.LIMIT, 0) + + def LINES(self): + return self.getToken(SqlBaseParser.LINES, 0) + + def LIST(self): + return self.getToken(SqlBaseParser.LIST, 0) + + def LOAD(self): + return self.getToken(SqlBaseParser.LOAD, 0) + + def LOCAL(self): + return self.getToken(SqlBaseParser.LOCAL, 0) + + def LOCATION(self): + return self.getToken(SqlBaseParser.LOCATION, 0) + + def LOCK(self): + return self.getToken(SqlBaseParser.LOCK, 0) + + def LOCKS(self): + return self.getToken(SqlBaseParser.LOCKS, 0) + + def LOGICAL(self): + return self.getToken(SqlBaseParser.LOGICAL, 0) + + def MACRO(self): + return self.getToken(SqlBaseParser.MACRO, 0) + + def MAP(self): + return self.getToken(SqlBaseParser.MAP, 0) + + def MATCHED(self): + return self.getToken(SqlBaseParser.MATCHED, 0) + + def MERGE(self): + return self.getToken(SqlBaseParser.MERGE, 0) + + def MICROSECOND(self): + return self.getToken(SqlBaseParser.MICROSECOND, 0) + + def MICROSECONDS(self): + return self.getToken(SqlBaseParser.MICROSECONDS, 0) + + def MILLISECOND(self): + return self.getToken(SqlBaseParser.MILLISECOND, 0) + + def MILLISECONDS(self): + return self.getToken(SqlBaseParser.MILLISECONDS, 0) + + def MINUTE(self): + return self.getToken(SqlBaseParser.MINUTE, 0) + + def MINUTES(self): + return self.getToken(SqlBaseParser.MINUTES, 0) + + def MONTH(self): + return self.getToken(SqlBaseParser.MONTH, 0) + + def MONTHS(self): + return self.getToken(SqlBaseParser.MONTHS, 0) + + def MSCK(self): + return self.getToken(SqlBaseParser.MSCK, 0) + + def NAMESPACE(self): + return self.getToken(SqlBaseParser.NAMESPACE, 0) + + def NAMESPACES(self): + return self.getToken(SqlBaseParser.NAMESPACES, 0) + + def NANOSECOND(self): + return self.getToken(SqlBaseParser.NANOSECOND, 0) + + def NANOSECONDS(self): + return self.getToken(SqlBaseParser.NANOSECONDS, 0) + + def NO(self): + return self.getToken(SqlBaseParser.NO, 0) + + def NULLS(self): + return self.getToken(SqlBaseParser.NULLS, 0) + + def OF(self): + return self.getToken(SqlBaseParser.OF, 0) + + def OPTION(self): + return self.getToken(SqlBaseParser.OPTION, 0) + + def OPTIONS(self): + return self.getToken(SqlBaseParser.OPTIONS, 0) + + def OUT(self): + return self.getToken(SqlBaseParser.OUT, 0) + + def OUTPUTFORMAT(self): + return self.getToken(SqlBaseParser.OUTPUTFORMAT, 0) + + def OVER(self): + return self.getToken(SqlBaseParser.OVER, 0) + + def OVERLAY(self): + return self.getToken(SqlBaseParser.OVERLAY, 0) + + def OVERWRITE(self): + return self.getToken(SqlBaseParser.OVERWRITE, 0) + + def PARTITION(self): + return self.getToken(SqlBaseParser.PARTITION, 0) + + def PARTITIONED(self): + return self.getToken(SqlBaseParser.PARTITIONED, 0) + + def PARTITIONS(self): + return self.getToken(SqlBaseParser.PARTITIONS, 0) + + def PERCENTLIT(self): + return self.getToken(SqlBaseParser.PERCENTLIT, 0) + + def PIVOT(self): + return self.getToken(SqlBaseParser.PIVOT, 0) + + def PLACING(self): + return self.getToken(SqlBaseParser.PLACING, 0) + + def POSITION(self): + return self.getToken(SqlBaseParser.POSITION, 0) + + def PRECEDING(self): + return self.getToken(SqlBaseParser.PRECEDING, 0) + + def PRINCIPALS(self): + return self.getToken(SqlBaseParser.PRINCIPALS, 0) + + def PROPERTIES(self): + return self.getToken(SqlBaseParser.PROPERTIES, 0) + + def PURGE(self): + return self.getToken(SqlBaseParser.PURGE, 0) + + def QUARTER(self): + return self.getToken(SqlBaseParser.QUARTER, 0) + + def QUERY(self): + return self.getToken(SqlBaseParser.QUERY, 0) + + def RANGE(self): + return self.getToken(SqlBaseParser.RANGE, 0) + + def RECORDREADER(self): + return self.getToken(SqlBaseParser.RECORDREADER, 0) + + def RECORDWRITER(self): + return self.getToken(SqlBaseParser.RECORDWRITER, 0) + + def RECOVER(self): + return self.getToken(SqlBaseParser.RECOVER, 0) + + def REDUCE(self): + return self.getToken(SqlBaseParser.REDUCE, 0) + + def REFRESH(self): + return self.getToken(SqlBaseParser.REFRESH, 0) + + def RENAME(self): + return self.getToken(SqlBaseParser.RENAME, 0) + + def REPAIR(self): + return self.getToken(SqlBaseParser.REPAIR, 0) + + def REPEATABLE(self): + return self.getToken(SqlBaseParser.REPEATABLE, 0) + + def REPLACE(self): + return self.getToken(SqlBaseParser.REPLACE, 0) + + def RESET(self): + return self.getToken(SqlBaseParser.RESET, 0) + + def RESPECT(self): + return self.getToken(SqlBaseParser.RESPECT, 0) + + def RESTRICT(self): + return self.getToken(SqlBaseParser.RESTRICT, 0) + + def REVOKE(self): + return self.getToken(SqlBaseParser.REVOKE, 0) + + def RLIKE(self): + return self.getToken(SqlBaseParser.RLIKE, 0) + + def ROLE(self): + return self.getToken(SqlBaseParser.ROLE, 0) + + def ROLES(self): + return self.getToken(SqlBaseParser.ROLES, 0) + + def ROLLBACK(self): + return self.getToken(SqlBaseParser.ROLLBACK, 0) + + def ROLLUP(self): + return self.getToken(SqlBaseParser.ROLLUP, 0) + + def ROW(self): + return self.getToken(SqlBaseParser.ROW, 0) + + def ROWS(self): + return self.getToken(SqlBaseParser.ROWS, 0) + + def SCHEMA(self): + return self.getToken(SqlBaseParser.SCHEMA, 0) + + def SCHEMAS(self): + return self.getToken(SqlBaseParser.SCHEMAS, 0) + + def SECOND(self): + return self.getToken(SqlBaseParser.SECOND, 0) + + def SECONDS(self): + return self.getToken(SqlBaseParser.SECONDS, 0) + + def SEMI(self): + return self.getToken(SqlBaseParser.SEMI, 0) + + def SEPARATED(self): + return self.getToken(SqlBaseParser.SEPARATED, 0) + + def SERDE(self): + return self.getToken(SqlBaseParser.SERDE, 0) + + def SERDEPROPERTIES(self): + return self.getToken(SqlBaseParser.SERDEPROPERTIES, 0) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + + def SETMINUS(self): + return self.getToken(SqlBaseParser.SETMINUS, 0) + + def SETS(self): + return self.getToken(SqlBaseParser.SETS, 0) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + + def SKEWED(self): + return self.getToken(SqlBaseParser.SKEWED, 0) + + def SORT(self): + return self.getToken(SqlBaseParser.SORT, 0) + + def SORTED(self): + return self.getToken(SqlBaseParser.SORTED, 0) + + def SOURCE(self): + return self.getToken(SqlBaseParser.SOURCE, 0) + + def START(self): + return self.getToken(SqlBaseParser.START, 0) + + def STATISTICS(self): + return self.getToken(SqlBaseParser.STATISTICS, 0) + + def STORED(self): + return self.getToken(SqlBaseParser.STORED, 0) + + def STRATIFY(self): + return self.getToken(SqlBaseParser.STRATIFY, 0) + + def STRUCT(self): + return self.getToken(SqlBaseParser.STRUCT, 0) + + def SUBSTR(self): + return self.getToken(SqlBaseParser.SUBSTR, 0) + + def SUBSTRING(self): + return self.getToken(SqlBaseParser.SUBSTRING, 0) + + def SYNC(self): + return self.getToken(SqlBaseParser.SYNC, 0) + + def SYSTEM_TIME(self): + return self.getToken(SqlBaseParser.SYSTEM_TIME, 0) + + def SYSTEM_VERSION(self): + return self.getToken(SqlBaseParser.SYSTEM_VERSION, 0) + + def TABLES(self): + return self.getToken(SqlBaseParser.TABLES, 0) + + def TABLESAMPLE(self): + return self.getToken(SqlBaseParser.TABLESAMPLE, 0) + + def TARGET(self): + return self.getToken(SqlBaseParser.TARGET, 0) + + def TBLPROPERTIES(self): + return self.getToken(SqlBaseParser.TBLPROPERTIES, 0) + + def TEMPORARY(self): + return self.getToken(SqlBaseParser.TEMPORARY, 0) + + def TERMINATED(self): + return self.getToken(SqlBaseParser.TERMINATED, 0) + + def TIMESTAMP(self): + return self.getToken(SqlBaseParser.TIMESTAMP, 0) + + def TIMESTAMPADD(self): + return self.getToken(SqlBaseParser.TIMESTAMPADD, 0) + + def TIMESTAMPDIFF(self): + return self.getToken(SqlBaseParser.TIMESTAMPDIFF, 0) + + def TOUCH(self): + return self.getToken(SqlBaseParser.TOUCH, 0) + + def TRANSACTION(self): + return self.getToken(SqlBaseParser.TRANSACTION, 0) + + def TRANSACTIONS(self): + return self.getToken(SqlBaseParser.TRANSACTIONS, 0) + + def TRANSFORM(self): + return self.getToken(SqlBaseParser.TRANSFORM, 0) + + def TRIM(self): + return self.getToken(SqlBaseParser.TRIM, 0) + + def TRUE(self): + return self.getToken(SqlBaseParser.TRUE, 0) + + def TRUNCATE(self): + return self.getToken(SqlBaseParser.TRUNCATE, 0) + + def TRY_CAST(self): + return self.getToken(SqlBaseParser.TRY_CAST, 0) + + def TYPE(self): + return self.getToken(SqlBaseParser.TYPE, 0) + + def UNARCHIVE(self): + return self.getToken(SqlBaseParser.UNARCHIVE, 0) + + def UNBOUNDED(self): + return self.getToken(SqlBaseParser.UNBOUNDED, 0) + + def UNCACHE(self): + return self.getToken(SqlBaseParser.UNCACHE, 0) + + def UNLOCK(self): + return self.getToken(SqlBaseParser.UNLOCK, 0) + + def UNPIVOT(self): + return self.getToken(SqlBaseParser.UNPIVOT, 0) + + def UNSET(self): + return self.getToken(SqlBaseParser.UNSET, 0) + + def UPDATE(self): + return self.getToken(SqlBaseParser.UPDATE, 0) + + def USE(self): + return self.getToken(SqlBaseParser.USE, 0) + + def VALUES(self): + return self.getToken(SqlBaseParser.VALUES, 0) + + def VERSION(self): + return self.getToken(SqlBaseParser.VERSION, 0) + + def VIEW(self): + return self.getToken(SqlBaseParser.VIEW, 0) + + def VIEWS(self): + return self.getToken(SqlBaseParser.VIEWS, 0) + + def WEEK(self): + return self.getToken(SqlBaseParser.WEEK, 0) + + def WEEKS(self): + return self.getToken(SqlBaseParser.WEEKS, 0) + + def WINDOW(self): + return self.getToken(SqlBaseParser.WINDOW, 0) + + def YEAR(self): + return self.getToken(SqlBaseParser.YEAR, 0) + + def YEARS(self): + return self.getToken(SqlBaseParser.YEARS, 0) + + def ZONE(self): + return self.getToken(SqlBaseParser.ZONE, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_ansiNonReserved + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAnsiNonReserved" ): + listener.enterAnsiNonReserved(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAnsiNonReserved" ): + listener.exitAnsiNonReserved(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitAnsiNonReserved" ): + return visitor.visitAnsiNonReserved(self) + else: + return visitor.visitChildren(self) + + + + + def ansiNonReserved(self): + + localctx = SqlBaseParser.AnsiNonReservedContext(self, self._ctx, self.state) + self.enterRule(localctx, 344, self.RULE_ansiNonReserved) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3542 + _la = self._input.LA(1) + if not((((_la) & ~0x3f) == 0 and ((1 << _la) & -2191012289037026560) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -4906136928869974017) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -677567443547206837) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -4576167932199175) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 4028402566609403) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class StrictNonReservedContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ANTI(self): + return self.getToken(SqlBaseParser.ANTI, 0) + + def CROSS(self): + return self.getToken(SqlBaseParser.CROSS, 0) + + def EXCEPT(self): + return self.getToken(SqlBaseParser.EXCEPT, 0) + + def FULL(self): + return self.getToken(SqlBaseParser.FULL, 0) + + def INNER(self): + return self.getToken(SqlBaseParser.INNER, 0) + + def INTERSECT(self): + return self.getToken(SqlBaseParser.INTERSECT, 0) + + def JOIN(self): + return self.getToken(SqlBaseParser.JOIN, 0) + + def LATERAL(self): + return self.getToken(SqlBaseParser.LATERAL, 0) + + def LEFT(self): + return self.getToken(SqlBaseParser.LEFT, 0) + + def NATURAL(self): + return self.getToken(SqlBaseParser.NATURAL, 0) + + def ON(self): + return self.getToken(SqlBaseParser.ON, 0) + + def RIGHT(self): + return self.getToken(SqlBaseParser.RIGHT, 0) + + def SEMI(self): + return self.getToken(SqlBaseParser.SEMI, 0) + + def SETMINUS(self): + return self.getToken(SqlBaseParser.SETMINUS, 0) + + def UNION(self): + return self.getToken(SqlBaseParser.UNION, 0) + + def USING(self): + return self.getToken(SqlBaseParser.USING, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_strictNonReserved + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterStrictNonReserved" ): + listener.enterStrictNonReserved(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitStrictNonReserved" ): + listener.exitStrictNonReserved(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitStrictNonReserved" ): + return visitor.visitStrictNonReserved(self) + else: + return visitor.visitChildren(self) + + + + + def strictNonReserved(self): + + localctx = SqlBaseParser.StrictNonReservedContext(self, self._ctx, self.state) + self.enterRule(localctx, 346, self.RULE_strictNonReserved) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3544 + _la = self._input.LA(1) + if not(_la==15 or _la==54 or ((((_la - 87)) & ~0x3f) == 0 and ((1 << (_la - 87)) & 20557019150811137) != 0) or ((((_la - 170)) & ~0x3f) == 0 and ((1 << (_la - 170)) & 2251799813685377) != 0) or ((((_la - 234)) & ~0x3f) == 0 and ((1 << (_la - 234)) & 577586652210266177) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class NonReservedContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ADD(self): + return self.getToken(SqlBaseParser.ADD, 0) + + def AFTER(self): + return self.getToken(SqlBaseParser.AFTER, 0) + + def ALL(self): + return self.getToken(SqlBaseParser.ALL, 0) + + def ALTER(self): + return self.getToken(SqlBaseParser.ALTER, 0) + + def ALWAYS(self): + return self.getToken(SqlBaseParser.ALWAYS, 0) + + def ANALYZE(self): + return self.getToken(SqlBaseParser.ANALYZE, 0) + + def AND(self): + return self.getToken(SqlBaseParser.AND, 0) + + def ANY(self): + return self.getToken(SqlBaseParser.ANY, 0) + + def ANY_VALUE(self): + return self.getToken(SqlBaseParser.ANY_VALUE, 0) + + def ARCHIVE(self): + return self.getToken(SqlBaseParser.ARCHIVE, 0) + + def ARRAY(self): + return self.getToken(SqlBaseParser.ARRAY, 0) + + def AS(self): + return self.getToken(SqlBaseParser.AS, 0) + + def ASC(self): + return self.getToken(SqlBaseParser.ASC, 0) + + def AT(self): + return self.getToken(SqlBaseParser.AT, 0) + + def AUTHORIZATION(self): + return self.getToken(SqlBaseParser.AUTHORIZATION, 0) + + def BETWEEN(self): + return self.getToken(SqlBaseParser.BETWEEN, 0) + + def BOTH(self): + return self.getToken(SqlBaseParser.BOTH, 0) + + def BUCKET(self): + return self.getToken(SqlBaseParser.BUCKET, 0) + + def BUCKETS(self): + return self.getToken(SqlBaseParser.BUCKETS, 0) + + def BY(self): + return self.getToken(SqlBaseParser.BY, 0) + + def CACHE(self): + return self.getToken(SqlBaseParser.CACHE, 0) + + def CASCADE(self): + return self.getToken(SqlBaseParser.CASCADE, 0) + + def CASE(self): + return self.getToken(SqlBaseParser.CASE, 0) + + def CAST(self): + return self.getToken(SqlBaseParser.CAST, 0) + + def CATALOG(self): + return self.getToken(SqlBaseParser.CATALOG, 0) + + def CATALOGS(self): + return self.getToken(SqlBaseParser.CATALOGS, 0) + + def CHANGE(self): + return self.getToken(SqlBaseParser.CHANGE, 0) + + def CHECK(self): + return self.getToken(SqlBaseParser.CHECK, 0) + + def CLEAR(self): + return self.getToken(SqlBaseParser.CLEAR, 0) + + def CLUSTER(self): + return self.getToken(SqlBaseParser.CLUSTER, 0) + + def CLUSTERED(self): + return self.getToken(SqlBaseParser.CLUSTERED, 0) + + def CODEGEN(self): + return self.getToken(SqlBaseParser.CODEGEN, 0) + + def COLLATE(self): + return self.getToken(SqlBaseParser.COLLATE, 0) + + def COLLECTION(self): + return self.getToken(SqlBaseParser.COLLECTION, 0) + + def COLUMN(self): + return self.getToken(SqlBaseParser.COLUMN, 0) + + def COLUMNS(self): + return self.getToken(SqlBaseParser.COLUMNS, 0) + + def COMMENT(self): + return self.getToken(SqlBaseParser.COMMENT, 0) + + def COMMIT(self): + return self.getToken(SqlBaseParser.COMMIT, 0) + + def COMPACT(self): + return self.getToken(SqlBaseParser.COMPACT, 0) + + def COMPACTIONS(self): + return self.getToken(SqlBaseParser.COMPACTIONS, 0) + + def COMPUTE(self): + return self.getToken(SqlBaseParser.COMPUTE, 0) + + def CONCATENATE(self): + return self.getToken(SqlBaseParser.CONCATENATE, 0) + + def CONSTRAINT(self): + return self.getToken(SqlBaseParser.CONSTRAINT, 0) + + def COST(self): + return self.getToken(SqlBaseParser.COST, 0) + + def CREATE(self): + return self.getToken(SqlBaseParser.CREATE, 0) + + def CUBE(self): + return self.getToken(SqlBaseParser.CUBE, 0) + + def CURRENT(self): + return self.getToken(SqlBaseParser.CURRENT, 0) + + def CURRENT_DATE(self): + return self.getToken(SqlBaseParser.CURRENT_DATE, 0) + + def CURRENT_TIME(self): + return self.getToken(SqlBaseParser.CURRENT_TIME, 0) + + def CURRENT_TIMESTAMP(self): + return self.getToken(SqlBaseParser.CURRENT_TIMESTAMP, 0) + + def CURRENT_USER(self): + return self.getToken(SqlBaseParser.CURRENT_USER, 0) + + def DATA(self): + return self.getToken(SqlBaseParser.DATA, 0) + + def DATABASE(self): + return self.getToken(SqlBaseParser.DATABASE, 0) + + def DATABASES(self): + return self.getToken(SqlBaseParser.DATABASES, 0) + + def DATEADD(self): + return self.getToken(SqlBaseParser.DATEADD, 0) + + def DATEDIFF(self): + return self.getToken(SqlBaseParser.DATEDIFF, 0) + + def DAY(self): + return self.getToken(SqlBaseParser.DAY, 0) + + def DAYS(self): + return self.getToken(SqlBaseParser.DAYS, 0) + + def DAYOFYEAR(self): + return self.getToken(SqlBaseParser.DAYOFYEAR, 0) + + def DBPROPERTIES(self): + return self.getToken(SqlBaseParser.DBPROPERTIES, 0) + + def DEFAULT(self): + return self.getToken(SqlBaseParser.DEFAULT, 0) + + def DEFINED(self): + return self.getToken(SqlBaseParser.DEFINED, 0) + + def DELETE(self): + return self.getToken(SqlBaseParser.DELETE, 0) + + def DELIMITED(self): + return self.getToken(SqlBaseParser.DELIMITED, 0) + + def DESC(self): + return self.getToken(SqlBaseParser.DESC, 0) + + def DESCRIBE(self): + return self.getToken(SqlBaseParser.DESCRIBE, 0) + + def DFS(self): + return self.getToken(SqlBaseParser.DFS, 0) + + def DIRECTORIES(self): + return self.getToken(SqlBaseParser.DIRECTORIES, 0) + + def DIRECTORY(self): + return self.getToken(SqlBaseParser.DIRECTORY, 0) + + def DISTINCT(self): + return self.getToken(SqlBaseParser.DISTINCT, 0) + + def DISTRIBUTE(self): + return self.getToken(SqlBaseParser.DISTRIBUTE, 0) + + def DIV(self): + return self.getToken(SqlBaseParser.DIV, 0) + + def DROP(self): + return self.getToken(SqlBaseParser.DROP, 0) + + def ELSE(self): + return self.getToken(SqlBaseParser.ELSE, 0) + + def END(self): + return self.getToken(SqlBaseParser.END, 0) + + def ESCAPE(self): + return self.getToken(SqlBaseParser.ESCAPE, 0) + + def ESCAPED(self): + return self.getToken(SqlBaseParser.ESCAPED, 0) + + def EXCHANGE(self): + return self.getToken(SqlBaseParser.EXCHANGE, 0) + + def EXCLUDE(self): + return self.getToken(SqlBaseParser.EXCLUDE, 0) + + def EXISTS(self): + return self.getToken(SqlBaseParser.EXISTS, 0) + + def EXPLAIN(self): + return self.getToken(SqlBaseParser.EXPLAIN, 0) + + def EXPORT(self): + return self.getToken(SqlBaseParser.EXPORT, 0) + + def EXTENDED(self): + return self.getToken(SqlBaseParser.EXTENDED, 0) + + def EXTERNAL(self): + return self.getToken(SqlBaseParser.EXTERNAL, 0) + + def EXTRACT(self): + return self.getToken(SqlBaseParser.EXTRACT, 0) + + def FALSE(self): + return self.getToken(SqlBaseParser.FALSE, 0) + + def FETCH(self): + return self.getToken(SqlBaseParser.FETCH, 0) + + def FILTER(self): + return self.getToken(SqlBaseParser.FILTER, 0) + + def FIELDS(self): + return self.getToken(SqlBaseParser.FIELDS, 0) + + def FILEFORMAT(self): + return self.getToken(SqlBaseParser.FILEFORMAT, 0) + + def FIRST(self): + return self.getToken(SqlBaseParser.FIRST, 0) + + def FOLLOWING(self): + return self.getToken(SqlBaseParser.FOLLOWING, 0) + + def FOR(self): + return self.getToken(SqlBaseParser.FOR, 0) + + def FOREIGN(self): + return self.getToken(SqlBaseParser.FOREIGN, 0) + + def FORMAT(self): + return self.getToken(SqlBaseParser.FORMAT, 0) + + def FORMATTED(self): + return self.getToken(SqlBaseParser.FORMATTED, 0) + + def FROM(self): + return self.getToken(SqlBaseParser.FROM, 0) + + def FUNCTION(self): + return self.getToken(SqlBaseParser.FUNCTION, 0) + + def FUNCTIONS(self): + return self.getToken(SqlBaseParser.FUNCTIONS, 0) + + def GENERATED(self): + return self.getToken(SqlBaseParser.GENERATED, 0) + + def GLOBAL(self): + return self.getToken(SqlBaseParser.GLOBAL, 0) + + def GRANT(self): + return self.getToken(SqlBaseParser.GRANT, 0) + + def GROUP(self): + return self.getToken(SqlBaseParser.GROUP, 0) + + def GROUPING(self): + return self.getToken(SqlBaseParser.GROUPING, 0) + + def HAVING(self): + return self.getToken(SqlBaseParser.HAVING, 0) + + def HOUR(self): + return self.getToken(SqlBaseParser.HOUR, 0) + + def HOURS(self): + return self.getToken(SqlBaseParser.HOURS, 0) + + def IF(self): + return self.getToken(SqlBaseParser.IF, 0) + + def IGNORE(self): + return self.getToken(SqlBaseParser.IGNORE, 0) + + def IMPORT(self): + return self.getToken(SqlBaseParser.IMPORT, 0) + + def IN(self): + return self.getToken(SqlBaseParser.IN, 0) + + def INCLUDE(self): + return self.getToken(SqlBaseParser.INCLUDE, 0) + + def INDEX(self): + return self.getToken(SqlBaseParser.INDEX, 0) + + def INDEXES(self): + return self.getToken(SqlBaseParser.INDEXES, 0) + + def INPATH(self): + return self.getToken(SqlBaseParser.INPATH, 0) + + def INPUTFORMAT(self): + return self.getToken(SqlBaseParser.INPUTFORMAT, 0) + + def INSERT(self): + return self.getToken(SqlBaseParser.INSERT, 0) + + def INTERVAL(self): + return self.getToken(SqlBaseParser.INTERVAL, 0) + + def INTO(self): + return self.getToken(SqlBaseParser.INTO, 0) + + def IS(self): + return self.getToken(SqlBaseParser.IS, 0) + + def ITEMS(self): + return self.getToken(SqlBaseParser.ITEMS, 0) + + def KEYS(self): + return self.getToken(SqlBaseParser.KEYS, 0) + + def LAST(self): + return self.getToken(SqlBaseParser.LAST, 0) + + def LAZY(self): + return self.getToken(SqlBaseParser.LAZY, 0) + + def LEADING(self): + return self.getToken(SqlBaseParser.LEADING, 0) + + def LIKE(self): + return self.getToken(SqlBaseParser.LIKE, 0) + + def ILIKE(self): + return self.getToken(SqlBaseParser.ILIKE, 0) + + def LIMIT(self): + return self.getToken(SqlBaseParser.LIMIT, 0) + + def LINES(self): + return self.getToken(SqlBaseParser.LINES, 0) + + def LIST(self): + return self.getToken(SqlBaseParser.LIST, 0) + + def LOAD(self): + return self.getToken(SqlBaseParser.LOAD, 0) + + def LOCAL(self): + return self.getToken(SqlBaseParser.LOCAL, 0) + + def LOCATION(self): + return self.getToken(SqlBaseParser.LOCATION, 0) + + def LOCK(self): + return self.getToken(SqlBaseParser.LOCK, 0) + + def LOCKS(self): + return self.getToken(SqlBaseParser.LOCKS, 0) + + def LOGICAL(self): + return self.getToken(SqlBaseParser.LOGICAL, 0) + + def MACRO(self): + return self.getToken(SqlBaseParser.MACRO, 0) + + def MAP(self): + return self.getToken(SqlBaseParser.MAP, 0) + + def MATCHED(self): + return self.getToken(SqlBaseParser.MATCHED, 0) + + def MERGE(self): + return self.getToken(SqlBaseParser.MERGE, 0) + + def MICROSECOND(self): + return self.getToken(SqlBaseParser.MICROSECOND, 0) + + def MICROSECONDS(self): + return self.getToken(SqlBaseParser.MICROSECONDS, 0) + + def MILLISECOND(self): + return self.getToken(SqlBaseParser.MILLISECOND, 0) + + def MILLISECONDS(self): + return self.getToken(SqlBaseParser.MILLISECONDS, 0) + + def MINUTE(self): + return self.getToken(SqlBaseParser.MINUTE, 0) + + def MINUTES(self): + return self.getToken(SqlBaseParser.MINUTES, 0) + + def MONTH(self): + return self.getToken(SqlBaseParser.MONTH, 0) + + def MONTHS(self): + return self.getToken(SqlBaseParser.MONTHS, 0) + + def MSCK(self): + return self.getToken(SqlBaseParser.MSCK, 0) + + def NAMESPACE(self): + return self.getToken(SqlBaseParser.NAMESPACE, 0) + + def NAMESPACES(self): + return self.getToken(SqlBaseParser.NAMESPACES, 0) + + def NANOSECOND(self): + return self.getToken(SqlBaseParser.NANOSECOND, 0) + + def NANOSECONDS(self): + return self.getToken(SqlBaseParser.NANOSECONDS, 0) + + def NO(self): + return self.getToken(SqlBaseParser.NO, 0) + + def NOT(self): + return self.getToken(SqlBaseParser.NOT, 0) + + def NULL(self): + return self.getToken(SqlBaseParser.NULL, 0) + + def NULLS(self): + return self.getToken(SqlBaseParser.NULLS, 0) + + def OF(self): + return self.getToken(SqlBaseParser.OF, 0) + + def OFFSET(self): + return self.getToken(SqlBaseParser.OFFSET, 0) + + def ONLY(self): + return self.getToken(SqlBaseParser.ONLY, 0) + + def OPTION(self): + return self.getToken(SqlBaseParser.OPTION, 0) + + def OPTIONS(self): + return self.getToken(SqlBaseParser.OPTIONS, 0) + + def OR(self): + return self.getToken(SqlBaseParser.OR, 0) + + def ORDER(self): + return self.getToken(SqlBaseParser.ORDER, 0) + + def OUT(self): + return self.getToken(SqlBaseParser.OUT, 0) + + def OUTER(self): + return self.getToken(SqlBaseParser.OUTER, 0) + + def OUTPUTFORMAT(self): + return self.getToken(SqlBaseParser.OUTPUTFORMAT, 0) + + def OVER(self): + return self.getToken(SqlBaseParser.OVER, 0) + + def OVERLAPS(self): + return self.getToken(SqlBaseParser.OVERLAPS, 0) + + def OVERLAY(self): + return self.getToken(SqlBaseParser.OVERLAY, 0) + + def OVERWRITE(self): + return self.getToken(SqlBaseParser.OVERWRITE, 0) + + def PARTITION(self): + return self.getToken(SqlBaseParser.PARTITION, 0) + + def PARTITIONED(self): + return self.getToken(SqlBaseParser.PARTITIONED, 0) + + def PARTITIONS(self): + return self.getToken(SqlBaseParser.PARTITIONS, 0) + + def PERCENTILE_CONT(self): + return self.getToken(SqlBaseParser.PERCENTILE_CONT, 0) + + def PERCENTILE_DISC(self): + return self.getToken(SqlBaseParser.PERCENTILE_DISC, 0) + + def PERCENTLIT(self): + return self.getToken(SqlBaseParser.PERCENTLIT, 0) + + def PIVOT(self): + return self.getToken(SqlBaseParser.PIVOT, 0) + + def PLACING(self): + return self.getToken(SqlBaseParser.PLACING, 0) + + def POSITION(self): + return self.getToken(SqlBaseParser.POSITION, 0) + + def PRECEDING(self): + return self.getToken(SqlBaseParser.PRECEDING, 0) + + def PRIMARY(self): + return self.getToken(SqlBaseParser.PRIMARY, 0) + + def PRINCIPALS(self): + return self.getToken(SqlBaseParser.PRINCIPALS, 0) + + def PROPERTIES(self): + return self.getToken(SqlBaseParser.PROPERTIES, 0) + + def PURGE(self): + return self.getToken(SqlBaseParser.PURGE, 0) + + def QUARTER(self): + return self.getToken(SqlBaseParser.QUARTER, 0) + + def QUERY(self): + return self.getToken(SqlBaseParser.QUERY, 0) + + def RANGE(self): + return self.getToken(SqlBaseParser.RANGE, 0) + + def RECORDREADER(self): + return self.getToken(SqlBaseParser.RECORDREADER, 0) + + def RECORDWRITER(self): + return self.getToken(SqlBaseParser.RECORDWRITER, 0) + + def RECOVER(self): + return self.getToken(SqlBaseParser.RECOVER, 0) + + def REDUCE(self): + return self.getToken(SqlBaseParser.REDUCE, 0) + + def REFERENCES(self): + return self.getToken(SqlBaseParser.REFERENCES, 0) + + def REFRESH(self): + return self.getToken(SqlBaseParser.REFRESH, 0) + + def RENAME(self): + return self.getToken(SqlBaseParser.RENAME, 0) + + def REPAIR(self): + return self.getToken(SqlBaseParser.REPAIR, 0) + + def REPEATABLE(self): + return self.getToken(SqlBaseParser.REPEATABLE, 0) + + def REPLACE(self): + return self.getToken(SqlBaseParser.REPLACE, 0) + + def RESET(self): + return self.getToken(SqlBaseParser.RESET, 0) + + def RESPECT(self): + return self.getToken(SqlBaseParser.RESPECT, 0) + + def RESTRICT(self): + return self.getToken(SqlBaseParser.RESTRICT, 0) + + def REVOKE(self): + return self.getToken(SqlBaseParser.REVOKE, 0) + + def RLIKE(self): + return self.getToken(SqlBaseParser.RLIKE, 0) + + def ROLE(self): + return self.getToken(SqlBaseParser.ROLE, 0) + + def ROLES(self): + return self.getToken(SqlBaseParser.ROLES, 0) + + def ROLLBACK(self): + return self.getToken(SqlBaseParser.ROLLBACK, 0) + + def ROLLUP(self): + return self.getToken(SqlBaseParser.ROLLUP, 0) + + def ROW(self): + return self.getToken(SqlBaseParser.ROW, 0) + + def ROWS(self): + return self.getToken(SqlBaseParser.ROWS, 0) + + def SCHEMA(self): + return self.getToken(SqlBaseParser.SCHEMA, 0) + + def SCHEMAS(self): + return self.getToken(SqlBaseParser.SCHEMAS, 0) + + def SECOND(self): + return self.getToken(SqlBaseParser.SECOND, 0) + + def SECONDS(self): + return self.getToken(SqlBaseParser.SECONDS, 0) + + def SELECT(self): + return self.getToken(SqlBaseParser.SELECT, 0) + + def SEPARATED(self): + return self.getToken(SqlBaseParser.SEPARATED, 0) + + def SERDE(self): + return self.getToken(SqlBaseParser.SERDE, 0) + + def SERDEPROPERTIES(self): + return self.getToken(SqlBaseParser.SERDEPROPERTIES, 0) + + def SESSION_USER(self): + return self.getToken(SqlBaseParser.SESSION_USER, 0) + + def SET(self): + return self.getToken(SqlBaseParser.SET, 0) + + def SETS(self): + return self.getToken(SqlBaseParser.SETS, 0) + + def SHOW(self): + return self.getToken(SqlBaseParser.SHOW, 0) + + def SKEWED(self): + return self.getToken(SqlBaseParser.SKEWED, 0) + + def SOME(self): + return self.getToken(SqlBaseParser.SOME, 0) + + def SORT(self): + return self.getToken(SqlBaseParser.SORT, 0) + + def SORTED(self): + return self.getToken(SqlBaseParser.SORTED, 0) + + def SOURCE(self): + return self.getToken(SqlBaseParser.SOURCE, 0) + + def START(self): + return self.getToken(SqlBaseParser.START, 0) + + def STATISTICS(self): + return self.getToken(SqlBaseParser.STATISTICS, 0) + + def STORED(self): + return self.getToken(SqlBaseParser.STORED, 0) + + def STRATIFY(self): + return self.getToken(SqlBaseParser.STRATIFY, 0) + + def STRUCT(self): + return self.getToken(SqlBaseParser.STRUCT, 0) + + def SUBSTR(self): + return self.getToken(SqlBaseParser.SUBSTR, 0) + + def SUBSTRING(self): + return self.getToken(SqlBaseParser.SUBSTRING, 0) + + def SYNC(self): + return self.getToken(SqlBaseParser.SYNC, 0) + + def SYSTEM_TIME(self): + return self.getToken(SqlBaseParser.SYSTEM_TIME, 0) + + def SYSTEM_VERSION(self): + return self.getToken(SqlBaseParser.SYSTEM_VERSION, 0) + + def TABLE(self): + return self.getToken(SqlBaseParser.TABLE, 0) + + def TABLES(self): + return self.getToken(SqlBaseParser.TABLES, 0) + + def TABLESAMPLE(self): + return self.getToken(SqlBaseParser.TABLESAMPLE, 0) + + def TARGET(self): + return self.getToken(SqlBaseParser.TARGET, 0) + + def TBLPROPERTIES(self): + return self.getToken(SqlBaseParser.TBLPROPERTIES, 0) + + def TEMPORARY(self): + return self.getToken(SqlBaseParser.TEMPORARY, 0) + + def TERMINATED(self): + return self.getToken(SqlBaseParser.TERMINATED, 0) + + def THEN(self): + return self.getToken(SqlBaseParser.THEN, 0) + + def TIME(self): + return self.getToken(SqlBaseParser.TIME, 0) + + def TIMESTAMP(self): + return self.getToken(SqlBaseParser.TIMESTAMP, 0) + + def TIMESTAMPADD(self): + return self.getToken(SqlBaseParser.TIMESTAMPADD, 0) + + def TIMESTAMPDIFF(self): + return self.getToken(SqlBaseParser.TIMESTAMPDIFF, 0) + + def TO(self): + return self.getToken(SqlBaseParser.TO, 0) + + def TOUCH(self): + return self.getToken(SqlBaseParser.TOUCH, 0) + + def TRAILING(self): + return self.getToken(SqlBaseParser.TRAILING, 0) + + def TRANSACTION(self): + return self.getToken(SqlBaseParser.TRANSACTION, 0) + + def TRANSACTIONS(self): + return self.getToken(SqlBaseParser.TRANSACTIONS, 0) + + def TRANSFORM(self): + return self.getToken(SqlBaseParser.TRANSFORM, 0) + + def TRIM(self): + return self.getToken(SqlBaseParser.TRIM, 0) + + def TRUE(self): + return self.getToken(SqlBaseParser.TRUE, 0) + + def TRUNCATE(self): + return self.getToken(SqlBaseParser.TRUNCATE, 0) + + def TRY_CAST(self): + return self.getToken(SqlBaseParser.TRY_CAST, 0) + + def TYPE(self): + return self.getToken(SqlBaseParser.TYPE, 0) + + def UNARCHIVE(self): + return self.getToken(SqlBaseParser.UNARCHIVE, 0) + + def UNBOUNDED(self): + return self.getToken(SqlBaseParser.UNBOUNDED, 0) + + def UNCACHE(self): + return self.getToken(SqlBaseParser.UNCACHE, 0) + + def UNIQUE(self): + return self.getToken(SqlBaseParser.UNIQUE, 0) + + def UNKNOWN(self): + return self.getToken(SqlBaseParser.UNKNOWN, 0) + + def UNLOCK(self): + return self.getToken(SqlBaseParser.UNLOCK, 0) + + def UNPIVOT(self): + return self.getToken(SqlBaseParser.UNPIVOT, 0) + + def UNSET(self): + return self.getToken(SqlBaseParser.UNSET, 0) + + def UPDATE(self): + return self.getToken(SqlBaseParser.UPDATE, 0) + + def USE(self): + return self.getToken(SqlBaseParser.USE, 0) + + def USER(self): + return self.getToken(SqlBaseParser.USER, 0) + + def VALUES(self): + return self.getToken(SqlBaseParser.VALUES, 0) + + def VERSION(self): + return self.getToken(SqlBaseParser.VERSION, 0) + + def VIEW(self): + return self.getToken(SqlBaseParser.VIEW, 0) + + def VIEWS(self): + return self.getToken(SqlBaseParser.VIEWS, 0) + + def WEEK(self): + return self.getToken(SqlBaseParser.WEEK, 0) + + def WEEKS(self): + return self.getToken(SqlBaseParser.WEEKS, 0) + + def WHEN(self): + return self.getToken(SqlBaseParser.WHEN, 0) + + def WHERE(self): + return self.getToken(SqlBaseParser.WHERE, 0) + + def WINDOW(self): + return self.getToken(SqlBaseParser.WINDOW, 0) + + def WITH(self): + return self.getToken(SqlBaseParser.WITH, 0) + + def WITHIN(self): + return self.getToken(SqlBaseParser.WITHIN, 0) + + def YEAR(self): + return self.getToken(SqlBaseParser.YEAR, 0) + + def YEARS(self): + return self.getToken(SqlBaseParser.YEARS, 0) + + def ZONE(self): + return self.getToken(SqlBaseParser.ZONE, 0) + + def getRuleIndex(self): + return SqlBaseParser.RULE_nonReserved + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNonReserved" ): + listener.enterNonReserved(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNonReserved" ): + listener.exitNonReserved(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitNonReserved" ): + return visitor.visitNonReserved(self) + else: + return visitor.visitChildren(self) + + + + + def nonReserved(self): + + localctx = SqlBaseParser.NonReservedContext(self, self._ctx, self.state) + self.enterRule(localctx, 348, self.RULE_nonReserved) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 3546 + _la = self._input.LA(1) + if not((((_la) & ~0x3f) == 0 and ((1 << _la) & -18014398509515008) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -4611703610621820929) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -567347999941765) != 0) or ((((_la - 192)) & ~0x3f) == 0 and ((1 << (_la - 192)) & -285873560092673) != 0) or ((((_la - 256)) & ~0x3f) == 0 and ((1 << (_la - 256)) & 4503461919981567) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + + def sempred(self, localctx:RuleContext, ruleIndex:int, predIndex:int): + if self._predicates == None: + self._predicates = dict() + self._predicates[29] = self.queryTerm_sempred + self._predicates[120] = self.booleanExpression_sempred + self._predicates[122] = self.valueExpression_sempred + self._predicates[124] = self.primaryExpression_sempred + pred = self._predicates.get(ruleIndex, None) + if pred is None: + raise Exception("No predicate with index:" + str(ruleIndex)) + else: + return pred(localctx, predIndex) + + def queryTerm_sempred(self, localctx:QueryTermContext, predIndex:int): + if predIndex == 0: + return self.precpred(self._ctx, 1) + + + def booleanExpression_sempred(self, localctx:BooleanExpressionContext, predIndex:int): + if predIndex == 1: + return self.precpred(self._ctx, 2) + + + if predIndex == 2: + return self.precpred(self._ctx, 1) + + + def valueExpression_sempred(self, localctx:ValueExpressionContext, predIndex:int): + if predIndex == 3: + return self.precpred(self._ctx, 6) + + + if predIndex == 4: + return self.precpred(self._ctx, 5) + + + if predIndex == 5: + return self.precpred(self._ctx, 4) + + + if predIndex == 6: + return self.precpred(self._ctx, 3) + + + if predIndex == 7: + return self.precpred(self._ctx, 2) + + + if predIndex == 8: + return self.precpred(self._ctx, 1) + + + def primaryExpression_sempred(self, localctx:PrimaryExpressionContext, predIndex:int): + if predIndex == 9: + return self.precpred(self._ctx, 9) + + + if predIndex == 10: + return self.precpred(self._ctx, 7) + + + + + diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParser.tokens b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParser.tokens new file mode 100644 index 000000000..c76d66e32 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParser.tokens @@ -0,0 +1,669 @@ +SEMICOLON=1 +LEFT_PAREN=2 +RIGHT_PAREN=3 +COMMA=4 +DOT=5 +LEFT_BRACKET=6 +RIGHT_BRACKET=7 +ADD=8 +AFTER=9 +ALL=10 +ALTER=11 +ALWAYS=12 +ANALYZE=13 +AND=14 +ANTI=15 +ANY=16 +ANY_VALUE=17 +ARCHIVE=18 +ARRAY=19 +AS=20 +ASC=21 +AT=22 +AUTHORIZATION=23 +BETWEEN=24 +BOTH=25 +BUCKET=26 +BUCKETS=27 +BY=28 +CACHE=29 +CASCADE=30 +CASE=31 +CAST=32 +CATALOG=33 +CATALOGS=34 +CHANGE=35 +CHECK=36 +CLEAR=37 +CLUSTER=38 +CLUSTERED=39 +CODEGEN=40 +COLLATE=41 +COLLECTION=42 +COLUMN=43 +COLUMNS=44 +COMMENT=45 +COMMIT=46 +COMPACT=47 +COMPACTIONS=48 +COMPUTE=49 +CONCATENATE=50 +CONSTRAINT=51 +COST=52 +CREATE=53 +CROSS=54 +CUBE=55 +CURRENT=56 +CURRENT_DATE=57 +CURRENT_TIME=58 +CURRENT_TIMESTAMP=59 +CURRENT_USER=60 +DAY=61 +DAYS=62 +DAYOFYEAR=63 +DATA=64 +DATABASE=65 +DATABASES=66 +DATEADD=67 +DATEDIFF=68 +DBPROPERTIES=69 +DEFAULT=70 +DEFINED=71 +DELETE=72 +DELIMITED=73 +DESC=74 +DESCRIBE=75 +DFS=76 +DIRECTORIES=77 +DIRECTORY=78 +DISTINCT=79 +DISTRIBUTE=80 +DIV=81 +DROP=82 +ELSE=83 +END=84 +ESCAPE=85 +ESCAPED=86 +EXCEPT=87 +EXCHANGE=88 +EXCLUDE=89 +EXISTS=90 +EXPLAIN=91 +EXPORT=92 +EXTENDED=93 +EXTERNAL=94 +EXTRACT=95 +FALSE=96 +FETCH=97 +FIELDS=98 +FILTER=99 +FILEFORMAT=100 +FIRST=101 +FOLLOWING=102 +FOR=103 +FOREIGN=104 +FORMAT=105 +FORMATTED=106 +FROM=107 +FULL=108 +FUNCTION=109 +FUNCTIONS=110 +GENERATED=111 +GLOBAL=112 +GRANT=113 +GROUP=114 +GROUPING=115 +HAVING=116 +HOUR=117 +HOURS=118 +IF=119 +IGNORE=120 +IMPORT=121 +IN=122 +INCLUDE=123 +INDEX=124 +INDEXES=125 +INNER=126 +INPATH=127 +INPUTFORMAT=128 +INSERT=129 +INTERSECT=130 +INTERVAL=131 +INTO=132 +IS=133 +ITEMS=134 +JOIN=135 +KEYS=136 +LAST=137 +LATERAL=138 +LAZY=139 +LEADING=140 +LEFT=141 +LIKE=142 +ILIKE=143 +LIMIT=144 +LINES=145 +LIST=146 +LOAD=147 +LOCAL=148 +LOCATION=149 +LOCK=150 +LOCKS=151 +LOGICAL=152 +MACRO=153 +MAP=154 +MATCHED=155 +MERGE=156 +MICROSECOND=157 +MICROSECONDS=158 +MILLISECOND=159 +MILLISECONDS=160 +MINUTE=161 +MINUTES=162 +MONTH=163 +MONTHS=164 +MSCK=165 +NAMESPACE=166 +NAMESPACES=167 +NANOSECOND=168 +NANOSECONDS=169 +NATURAL=170 +NO=171 +NOT=172 +NULL=173 +NULLS=174 +OF=175 +OFFSET=176 +ON=177 +ONLY=178 +OPTION=179 +OPTIONS=180 +OR=181 +ORDER=182 +OUT=183 +OUTER=184 +OUTPUTFORMAT=185 +OVER=186 +OVERLAPS=187 +OVERLAY=188 +OVERWRITE=189 +PARTITION=190 +PARTITIONED=191 +PARTITIONS=192 +PERCENTILE_CONT=193 +PERCENTILE_DISC=194 +PERCENTLIT=195 +PIVOT=196 +PLACING=197 +POSITION=198 +PRECEDING=199 +PRIMARY=200 +PRINCIPALS=201 +PROPERTIES=202 +PURGE=203 +QUARTER=204 +QUERY=205 +RANGE=206 +RECORDREADER=207 +RECORDWRITER=208 +RECOVER=209 +REDUCE=210 +REFERENCES=211 +REFRESH=212 +RENAME=213 +REPAIR=214 +REPEATABLE=215 +REPLACE=216 +RESET=217 +RESPECT=218 +RESTRICT=219 +REVOKE=220 +RIGHT=221 +RLIKE=222 +ROLE=223 +ROLES=224 +ROLLBACK=225 +ROLLUP=226 +ROW=227 +ROWS=228 +SECOND=229 +SECONDS=230 +SCHEMA=231 +SCHEMAS=232 +SELECT=233 +SEMI=234 +SEPARATED=235 +SERDE=236 +SERDEPROPERTIES=237 +SESSION_USER=238 +SET=239 +SETMINUS=240 +SETS=241 +SHOW=242 +SKEWED=243 +SOME=244 +SORT=245 +SORTED=246 +SOURCE=247 +START=248 +STATISTICS=249 +STORED=250 +STRATIFY=251 +STRUCT=252 +SUBSTR=253 +SUBSTRING=254 +SYNC=255 +SYSTEM_TIME=256 +SYSTEM_VERSION=257 +TABLE=258 +TABLES=259 +TABLESAMPLE=260 +TARGET=261 +TBLPROPERTIES=262 +TEMPORARY=263 +TERMINATED=264 +THEN=265 +TIME=266 +TIMESTAMP=267 +TIMESTAMPADD=268 +TIMESTAMPDIFF=269 +TO=270 +TOUCH=271 +TRAILING=272 +TRANSACTION=273 +TRANSACTIONS=274 +TRANSFORM=275 +TRIM=276 +TRUE=277 +TRUNCATE=278 +TRY_CAST=279 +TYPE=280 +UNARCHIVE=281 +UNBOUNDED=282 +UNCACHE=283 +UNION=284 +UNIQUE=285 +UNKNOWN=286 +UNLOCK=287 +UNPIVOT=288 +UNSET=289 +UPDATE=290 +USE=291 +USER=292 +USING=293 +VALUES=294 +VERSION=295 +VIEW=296 +VIEWS=297 +WEEK=298 +WEEKS=299 +WHEN=300 +WHERE=301 +WINDOW=302 +WITH=303 +WITHIN=304 +YEAR=305 +YEARS=306 +ZONE=307 +EQ=308 +NSEQ=309 +NEQ=310 +NEQJ=311 +LT=312 +LTE=313 +GT=314 +GTE=315 +PLUS=316 +MINUS=317 +ASTERISK=318 +SLASH=319 +PERCENT=320 +TILDE=321 +AMPERSAND=322 +PIPE=323 +CONCAT_PIPE=324 +HAT=325 +COLON=326 +ARROW=327 +HENT_START=328 +HENT_END=329 +STRING=330 +DOUBLEQUOTED_STRING=331 +BIGINT_LITERAL=332 +SMALLINT_LITERAL=333 +TINYINT_LITERAL=334 +INTEGER_VALUE=335 +EXPONENT_VALUE=336 +DECIMAL_VALUE=337 +FLOAT_LITERAL=338 +DOUBLE_LITERAL=339 +BIGDECIMAL_LITERAL=340 +IDENTIFIER=341 +BACKQUOTED_IDENTIFIER=342 +SIMPLE_COMMENT=343 +BRACKETED_COMMENT=344 +WS=345 +UNRECOGNIZED=346 +';'=1 +'('=2 +')'=3 +','=4 +'.'=5 +'['=6 +']'=7 +'ADD'=8 +'AFTER'=9 +'ALL'=10 +'ALTER'=11 +'ALWAYS'=12 +'ANALYZE'=13 +'AND'=14 +'ANTI'=15 +'ANY'=16 +'ANY_VALUE'=17 +'ARCHIVE'=18 +'ARRAY'=19 +'AS'=20 +'ASC'=21 +'AT'=22 +'AUTHORIZATION'=23 +'BETWEEN'=24 +'BOTH'=25 +'BUCKET'=26 +'BUCKETS'=27 +'BY'=28 +'CACHE'=29 +'CASCADE'=30 +'CASE'=31 +'CAST'=32 +'CATALOG'=33 +'CATALOGS'=34 +'CHANGE'=35 +'CHECK'=36 +'CLEAR'=37 +'CLUSTER'=38 +'CLUSTERED'=39 +'CODEGEN'=40 +'COLLATE'=41 +'COLLECTION'=42 +'COLUMN'=43 +'COLUMNS'=44 +'COMMENT'=45 +'COMMIT'=46 +'COMPACT'=47 +'COMPACTIONS'=48 +'COMPUTE'=49 +'CONCATENATE'=50 +'CONSTRAINT'=51 +'COST'=52 +'CREATE'=53 +'CROSS'=54 +'CUBE'=55 +'CURRENT'=56 +'CURRENT_DATE'=57 +'CURRENT_TIME'=58 +'CURRENT_TIMESTAMP'=59 +'CURRENT_USER'=60 +'DAY'=61 +'DAYS'=62 +'DAYOFYEAR'=63 +'DATA'=64 +'DATABASE'=65 +'DATABASES'=66 +'DATEADD'=67 +'DATEDIFF'=68 +'DBPROPERTIES'=69 +'DEFAULT'=70 +'DEFINED'=71 +'DELETE'=72 +'DELIMITED'=73 +'DESC'=74 +'DESCRIBE'=75 +'DFS'=76 +'DIRECTORIES'=77 +'DIRECTORY'=78 +'DISTINCT'=79 +'DISTRIBUTE'=80 +'DIV'=81 +'DROP'=82 +'ELSE'=83 +'END'=84 +'ESCAPE'=85 +'ESCAPED'=86 +'EXCEPT'=87 +'EXCHANGE'=88 +'EXCLUDE'=89 +'EXISTS'=90 +'EXPLAIN'=91 +'EXPORT'=92 +'EXTENDED'=93 +'EXTERNAL'=94 +'EXTRACT'=95 +'FALSE'=96 +'FETCH'=97 +'FIELDS'=98 +'FILTER'=99 +'FILEFORMAT'=100 +'FIRST'=101 +'FOLLOWING'=102 +'FOR'=103 +'FOREIGN'=104 +'FORMAT'=105 +'FORMATTED'=106 +'FROM'=107 +'FULL'=108 +'FUNCTION'=109 +'FUNCTIONS'=110 +'GENERATED'=111 +'GLOBAL'=112 +'GRANT'=113 +'GROUP'=114 +'GROUPING'=115 +'HAVING'=116 +'HOUR'=117 +'HOURS'=118 +'IF'=119 +'IGNORE'=120 +'IMPORT'=121 +'IN'=122 +'INCLUDE'=123 +'INDEX'=124 +'INDEXES'=125 +'INNER'=126 +'INPATH'=127 +'INPUTFORMAT'=128 +'INSERT'=129 +'INTERSECT'=130 +'INTERVAL'=131 +'INTO'=132 +'IS'=133 +'ITEMS'=134 +'JOIN'=135 +'KEYS'=136 +'LAST'=137 +'LATERAL'=138 +'LAZY'=139 +'LEADING'=140 +'LEFT'=141 +'LIKE'=142 +'ILIKE'=143 +'LIMIT'=144 +'LINES'=145 +'LIST'=146 +'LOAD'=147 +'LOCAL'=148 +'LOCATION'=149 +'LOCK'=150 +'LOCKS'=151 +'LOGICAL'=152 +'MACRO'=153 +'MAP'=154 +'MATCHED'=155 +'MERGE'=156 +'MICROSECOND'=157 +'MICROSECONDS'=158 +'MILLISECOND'=159 +'MILLISECONDS'=160 +'MINUTE'=161 +'MINUTES'=162 +'MONTH'=163 +'MONTHS'=164 +'MSCK'=165 +'NAMESPACE'=166 +'NAMESPACES'=167 +'NANOSECOND'=168 +'NANOSECONDS'=169 +'NATURAL'=170 +'NO'=171 +'NULL'=173 +'NULLS'=174 +'OF'=175 +'OFFSET'=176 +'ON'=177 +'ONLY'=178 +'OPTION'=179 +'OPTIONS'=180 +'OR'=181 +'ORDER'=182 +'OUT'=183 +'OUTER'=184 +'OUTPUTFORMAT'=185 +'OVER'=186 +'OVERLAPS'=187 +'OVERLAY'=188 +'OVERWRITE'=189 +'PARTITION'=190 +'PARTITIONED'=191 +'PARTITIONS'=192 +'PERCENTILE_CONT'=193 +'PERCENTILE_DISC'=194 +'PERCENT'=195 +'PIVOT'=196 +'PLACING'=197 +'POSITION'=198 +'PRECEDING'=199 +'PRIMARY'=200 +'PRINCIPALS'=201 +'PROPERTIES'=202 +'PURGE'=203 +'QUARTER'=204 +'QUERY'=205 +'RANGE'=206 +'RECORDREADER'=207 +'RECORDWRITER'=208 +'RECOVER'=209 +'REDUCE'=210 +'REFERENCES'=211 +'REFRESH'=212 +'RENAME'=213 +'REPAIR'=214 +'REPEATABLE'=215 +'REPLACE'=216 +'RESET'=217 +'RESPECT'=218 +'RESTRICT'=219 +'REVOKE'=220 +'RIGHT'=221 +'ROLE'=223 +'ROLES'=224 +'ROLLBACK'=225 +'ROLLUP'=226 +'ROW'=227 +'ROWS'=228 +'SECOND'=229 +'SECONDS'=230 +'SCHEMA'=231 +'SCHEMAS'=232 +'SELECT'=233 +'SEMI'=234 +'SEPARATED'=235 +'SERDE'=236 +'SERDEPROPERTIES'=237 +'SESSION_USER'=238 +'SET'=239 +'MINUS'=240 +'SETS'=241 +'SHOW'=242 +'SKEWED'=243 +'SOME'=244 +'SORT'=245 +'SORTED'=246 +'SOURCE'=247 +'START'=248 +'STATISTICS'=249 +'STORED'=250 +'STRATIFY'=251 +'STRUCT'=252 +'SUBSTR'=253 +'SUBSTRING'=254 +'SYNC'=255 +'SYSTEM_TIME'=256 +'SYSTEM_VERSION'=257 +'TABLE'=258 +'TABLES'=259 +'TABLESAMPLE'=260 +'TARGET'=261 +'TBLPROPERTIES'=262 +'TERMINATED'=264 +'THEN'=265 +'TIME'=266 +'TIMESTAMP'=267 +'TIMESTAMPADD'=268 +'TIMESTAMPDIFF'=269 +'TO'=270 +'TOUCH'=271 +'TRAILING'=272 +'TRANSACTION'=273 +'TRANSACTIONS'=274 +'TRANSFORM'=275 +'TRIM'=276 +'TRUE'=277 +'TRUNCATE'=278 +'TRY_CAST'=279 +'TYPE'=280 +'UNARCHIVE'=281 +'UNBOUNDED'=282 +'UNCACHE'=283 +'UNION'=284 +'UNIQUE'=285 +'UNKNOWN'=286 +'UNLOCK'=287 +'UNPIVOT'=288 +'UNSET'=289 +'UPDATE'=290 +'USE'=291 +'USER'=292 +'USING'=293 +'VALUES'=294 +'VERSION'=295 +'VIEW'=296 +'VIEWS'=297 +'WEEK'=298 +'WEEKS'=299 +'WHEN'=300 +'WHERE'=301 +'WINDOW'=302 +'WITH'=303 +'WITHIN'=304 +'YEAR'=305 +'YEARS'=306 +'ZONE'=307 +'<=>'=309 +'<>'=310 +'!='=311 +'<'=312 +'>'=314 +'+'=316 +'-'=317 +'*'=318 +'/'=319 +'%'=320 +'~'=321 +'&'=322 +'|'=323 +'||'=324 +'^'=325 +':'=326 +'->'=327 +'/*+'=328 +'*/'=329 diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParserListener.py b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParserListener.py new file mode 100644 index 000000000..6a387e83a --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParserListener.py @@ -0,0 +1,2919 @@ +# Generated from SqlBaseParser.g4 by ANTLR 4.13.1 +from antlr4 import * +if "." in __name__: + from .SqlBaseParser import SqlBaseParser +else: + from SqlBaseParser import SqlBaseParser + +# This class defines a complete listener for a parse tree produced by SqlBaseParser. +class SqlBaseParserListener(ParseTreeListener): + + # Enter a parse tree produced by SqlBaseParser#singleStatement. + def enterSingleStatement(self, ctx:SqlBaseParser.SingleStatementContext): + pass + + # Exit a parse tree produced by SqlBaseParser#singleStatement. + def exitSingleStatement(self, ctx:SqlBaseParser.SingleStatementContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#singleExpression. + def enterSingleExpression(self, ctx:SqlBaseParser.SingleExpressionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#singleExpression. + def exitSingleExpression(self, ctx:SqlBaseParser.SingleExpressionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#singleTableIdentifier. + def enterSingleTableIdentifier(self, ctx:SqlBaseParser.SingleTableIdentifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#singleTableIdentifier. + def exitSingleTableIdentifier(self, ctx:SqlBaseParser.SingleTableIdentifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#singleMultipartIdentifier. + def enterSingleMultipartIdentifier(self, ctx:SqlBaseParser.SingleMultipartIdentifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#singleMultipartIdentifier. + def exitSingleMultipartIdentifier(self, ctx:SqlBaseParser.SingleMultipartIdentifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#singleFunctionIdentifier. + def enterSingleFunctionIdentifier(self, ctx:SqlBaseParser.SingleFunctionIdentifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#singleFunctionIdentifier. + def exitSingleFunctionIdentifier(self, ctx:SqlBaseParser.SingleFunctionIdentifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#singleDataType. + def enterSingleDataType(self, ctx:SqlBaseParser.SingleDataTypeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#singleDataType. + def exitSingleDataType(self, ctx:SqlBaseParser.SingleDataTypeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#singleTableSchema. + def enterSingleTableSchema(self, ctx:SqlBaseParser.SingleTableSchemaContext): + pass + + # Exit a parse tree produced by SqlBaseParser#singleTableSchema. + def exitSingleTableSchema(self, ctx:SqlBaseParser.SingleTableSchemaContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#statementDefault. + def enterStatementDefault(self, ctx:SqlBaseParser.StatementDefaultContext): + pass + + # Exit a parse tree produced by SqlBaseParser#statementDefault. + def exitStatementDefault(self, ctx:SqlBaseParser.StatementDefaultContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#dmlStatement. + def enterDmlStatement(self, ctx:SqlBaseParser.DmlStatementContext): + pass + + # Exit a parse tree produced by SqlBaseParser#dmlStatement. + def exitDmlStatement(self, ctx:SqlBaseParser.DmlStatementContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#use. + def enterUse(self, ctx:SqlBaseParser.UseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#use. + def exitUse(self, ctx:SqlBaseParser.UseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#useNamespace. + def enterUseNamespace(self, ctx:SqlBaseParser.UseNamespaceContext): + pass + + # Exit a parse tree produced by SqlBaseParser#useNamespace. + def exitUseNamespace(self, ctx:SqlBaseParser.UseNamespaceContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#setCatalog. + def enterSetCatalog(self, ctx:SqlBaseParser.SetCatalogContext): + pass + + # Exit a parse tree produced by SqlBaseParser#setCatalog. + def exitSetCatalog(self, ctx:SqlBaseParser.SetCatalogContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#createNamespace. + def enterCreateNamespace(self, ctx:SqlBaseParser.CreateNamespaceContext): + pass + + # Exit a parse tree produced by SqlBaseParser#createNamespace. + def exitCreateNamespace(self, ctx:SqlBaseParser.CreateNamespaceContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#setNamespaceProperties. + def enterSetNamespaceProperties(self, ctx:SqlBaseParser.SetNamespacePropertiesContext): + pass + + # Exit a parse tree produced by SqlBaseParser#setNamespaceProperties. + def exitSetNamespaceProperties(self, ctx:SqlBaseParser.SetNamespacePropertiesContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#setNamespaceLocation. + def enterSetNamespaceLocation(self, ctx:SqlBaseParser.SetNamespaceLocationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#setNamespaceLocation. + def exitSetNamespaceLocation(self, ctx:SqlBaseParser.SetNamespaceLocationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#dropNamespace. + def enterDropNamespace(self, ctx:SqlBaseParser.DropNamespaceContext): + pass + + # Exit a parse tree produced by SqlBaseParser#dropNamespace. + def exitDropNamespace(self, ctx:SqlBaseParser.DropNamespaceContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#showNamespaces. + def enterShowNamespaces(self, ctx:SqlBaseParser.ShowNamespacesContext): + pass + + # Exit a parse tree produced by SqlBaseParser#showNamespaces. + def exitShowNamespaces(self, ctx:SqlBaseParser.ShowNamespacesContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#createTable. + def enterCreateTable(self, ctx:SqlBaseParser.CreateTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#createTable. + def exitCreateTable(self, ctx:SqlBaseParser.CreateTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#createTableLike. + def enterCreateTableLike(self, ctx:SqlBaseParser.CreateTableLikeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#createTableLike. + def exitCreateTableLike(self, ctx:SqlBaseParser.CreateTableLikeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#replaceTable. + def enterReplaceTable(self, ctx:SqlBaseParser.ReplaceTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#replaceTable. + def exitReplaceTable(self, ctx:SqlBaseParser.ReplaceTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#analyze. + def enterAnalyze(self, ctx:SqlBaseParser.AnalyzeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#analyze. + def exitAnalyze(self, ctx:SqlBaseParser.AnalyzeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#analyzeTables. + def enterAnalyzeTables(self, ctx:SqlBaseParser.AnalyzeTablesContext): + pass + + # Exit a parse tree produced by SqlBaseParser#analyzeTables. + def exitAnalyzeTables(self, ctx:SqlBaseParser.AnalyzeTablesContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#addTableColumns. + def enterAddTableColumns(self, ctx:SqlBaseParser.AddTableColumnsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#addTableColumns. + def exitAddTableColumns(self, ctx:SqlBaseParser.AddTableColumnsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#renameTableColumn. + def enterRenameTableColumn(self, ctx:SqlBaseParser.RenameTableColumnContext): + pass + + # Exit a parse tree produced by SqlBaseParser#renameTableColumn. + def exitRenameTableColumn(self, ctx:SqlBaseParser.RenameTableColumnContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#dropTableColumns. + def enterDropTableColumns(self, ctx:SqlBaseParser.DropTableColumnsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#dropTableColumns. + def exitDropTableColumns(self, ctx:SqlBaseParser.DropTableColumnsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#renameTable. + def enterRenameTable(self, ctx:SqlBaseParser.RenameTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#renameTable. + def exitRenameTable(self, ctx:SqlBaseParser.RenameTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#setTableProperties. + def enterSetTableProperties(self, ctx:SqlBaseParser.SetTablePropertiesContext): + pass + + # Exit a parse tree produced by SqlBaseParser#setTableProperties. + def exitSetTableProperties(self, ctx:SqlBaseParser.SetTablePropertiesContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unsetTableProperties. + def enterUnsetTableProperties(self, ctx:SqlBaseParser.UnsetTablePropertiesContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unsetTableProperties. + def exitUnsetTableProperties(self, ctx:SqlBaseParser.UnsetTablePropertiesContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#alterTableAlterColumn. + def enterAlterTableAlterColumn(self, ctx:SqlBaseParser.AlterTableAlterColumnContext): + pass + + # Exit a parse tree produced by SqlBaseParser#alterTableAlterColumn. + def exitAlterTableAlterColumn(self, ctx:SqlBaseParser.AlterTableAlterColumnContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#hiveChangeColumn. + def enterHiveChangeColumn(self, ctx:SqlBaseParser.HiveChangeColumnContext): + pass + + # Exit a parse tree produced by SqlBaseParser#hiveChangeColumn. + def exitHiveChangeColumn(self, ctx:SqlBaseParser.HiveChangeColumnContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#hiveReplaceColumns. + def enterHiveReplaceColumns(self, ctx:SqlBaseParser.HiveReplaceColumnsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#hiveReplaceColumns. + def exitHiveReplaceColumns(self, ctx:SqlBaseParser.HiveReplaceColumnsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#setTableSerDe. + def enterSetTableSerDe(self, ctx:SqlBaseParser.SetTableSerDeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#setTableSerDe. + def exitSetTableSerDe(self, ctx:SqlBaseParser.SetTableSerDeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#addTablePartition. + def enterAddTablePartition(self, ctx:SqlBaseParser.AddTablePartitionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#addTablePartition. + def exitAddTablePartition(self, ctx:SqlBaseParser.AddTablePartitionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#renameTablePartition. + def enterRenameTablePartition(self, ctx:SqlBaseParser.RenameTablePartitionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#renameTablePartition. + def exitRenameTablePartition(self, ctx:SqlBaseParser.RenameTablePartitionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#dropTablePartitions. + def enterDropTablePartitions(self, ctx:SqlBaseParser.DropTablePartitionsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#dropTablePartitions. + def exitDropTablePartitions(self, ctx:SqlBaseParser.DropTablePartitionsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#setTableLocation. + def enterSetTableLocation(self, ctx:SqlBaseParser.SetTableLocationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#setTableLocation. + def exitSetTableLocation(self, ctx:SqlBaseParser.SetTableLocationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#recoverPartitions. + def enterRecoverPartitions(self, ctx:SqlBaseParser.RecoverPartitionsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#recoverPartitions. + def exitRecoverPartitions(self, ctx:SqlBaseParser.RecoverPartitionsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#dropTable. + def enterDropTable(self, ctx:SqlBaseParser.DropTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#dropTable. + def exitDropTable(self, ctx:SqlBaseParser.DropTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#dropView. + def enterDropView(self, ctx:SqlBaseParser.DropViewContext): + pass + + # Exit a parse tree produced by SqlBaseParser#dropView. + def exitDropView(self, ctx:SqlBaseParser.DropViewContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#createView. + def enterCreateView(self, ctx:SqlBaseParser.CreateViewContext): + pass + + # Exit a parse tree produced by SqlBaseParser#createView. + def exitCreateView(self, ctx:SqlBaseParser.CreateViewContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#createTempViewUsing. + def enterCreateTempViewUsing(self, ctx:SqlBaseParser.CreateTempViewUsingContext): + pass + + # Exit a parse tree produced by SqlBaseParser#createTempViewUsing. + def exitCreateTempViewUsing(self, ctx:SqlBaseParser.CreateTempViewUsingContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#alterViewQuery. + def enterAlterViewQuery(self, ctx:SqlBaseParser.AlterViewQueryContext): + pass + + # Exit a parse tree produced by SqlBaseParser#alterViewQuery. + def exitAlterViewQuery(self, ctx:SqlBaseParser.AlterViewQueryContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#createFunction. + def enterCreateFunction(self, ctx:SqlBaseParser.CreateFunctionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#createFunction. + def exitCreateFunction(self, ctx:SqlBaseParser.CreateFunctionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#dropFunction. + def enterDropFunction(self, ctx:SqlBaseParser.DropFunctionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#dropFunction. + def exitDropFunction(self, ctx:SqlBaseParser.DropFunctionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#explain. + def enterExplain(self, ctx:SqlBaseParser.ExplainContext): + pass + + # Exit a parse tree produced by SqlBaseParser#explain. + def exitExplain(self, ctx:SqlBaseParser.ExplainContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#showTables. + def enterShowTables(self, ctx:SqlBaseParser.ShowTablesContext): + pass + + # Exit a parse tree produced by SqlBaseParser#showTables. + def exitShowTables(self, ctx:SqlBaseParser.ShowTablesContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#showTableExtended. + def enterShowTableExtended(self, ctx:SqlBaseParser.ShowTableExtendedContext): + pass + + # Exit a parse tree produced by SqlBaseParser#showTableExtended. + def exitShowTableExtended(self, ctx:SqlBaseParser.ShowTableExtendedContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#showTblProperties. + def enterShowTblProperties(self, ctx:SqlBaseParser.ShowTblPropertiesContext): + pass + + # Exit a parse tree produced by SqlBaseParser#showTblProperties. + def exitShowTblProperties(self, ctx:SqlBaseParser.ShowTblPropertiesContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#showColumns. + def enterShowColumns(self, ctx:SqlBaseParser.ShowColumnsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#showColumns. + def exitShowColumns(self, ctx:SqlBaseParser.ShowColumnsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#showViews. + def enterShowViews(self, ctx:SqlBaseParser.ShowViewsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#showViews. + def exitShowViews(self, ctx:SqlBaseParser.ShowViewsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#showPartitions. + def enterShowPartitions(self, ctx:SqlBaseParser.ShowPartitionsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#showPartitions. + def exitShowPartitions(self, ctx:SqlBaseParser.ShowPartitionsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#showFunctions. + def enterShowFunctions(self, ctx:SqlBaseParser.ShowFunctionsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#showFunctions. + def exitShowFunctions(self, ctx:SqlBaseParser.ShowFunctionsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#showCreateTable. + def enterShowCreateTable(self, ctx:SqlBaseParser.ShowCreateTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#showCreateTable. + def exitShowCreateTable(self, ctx:SqlBaseParser.ShowCreateTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#showCurrentNamespace. + def enterShowCurrentNamespace(self, ctx:SqlBaseParser.ShowCurrentNamespaceContext): + pass + + # Exit a parse tree produced by SqlBaseParser#showCurrentNamespace. + def exitShowCurrentNamespace(self, ctx:SqlBaseParser.ShowCurrentNamespaceContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#showCatalogs. + def enterShowCatalogs(self, ctx:SqlBaseParser.ShowCatalogsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#showCatalogs. + def exitShowCatalogs(self, ctx:SqlBaseParser.ShowCatalogsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#describeFunction. + def enterDescribeFunction(self, ctx:SqlBaseParser.DescribeFunctionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#describeFunction. + def exitDescribeFunction(self, ctx:SqlBaseParser.DescribeFunctionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#describeNamespace. + def enterDescribeNamespace(self, ctx:SqlBaseParser.DescribeNamespaceContext): + pass + + # Exit a parse tree produced by SqlBaseParser#describeNamespace. + def exitDescribeNamespace(self, ctx:SqlBaseParser.DescribeNamespaceContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#describeRelation. + def enterDescribeRelation(self, ctx:SqlBaseParser.DescribeRelationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#describeRelation. + def exitDescribeRelation(self, ctx:SqlBaseParser.DescribeRelationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#describeQuery. + def enterDescribeQuery(self, ctx:SqlBaseParser.DescribeQueryContext): + pass + + # Exit a parse tree produced by SqlBaseParser#describeQuery. + def exitDescribeQuery(self, ctx:SqlBaseParser.DescribeQueryContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#commentNamespace. + def enterCommentNamespace(self, ctx:SqlBaseParser.CommentNamespaceContext): + pass + + # Exit a parse tree produced by SqlBaseParser#commentNamespace. + def exitCommentNamespace(self, ctx:SqlBaseParser.CommentNamespaceContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#commentTable. + def enterCommentTable(self, ctx:SqlBaseParser.CommentTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#commentTable. + def exitCommentTable(self, ctx:SqlBaseParser.CommentTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#refreshTable. + def enterRefreshTable(self, ctx:SqlBaseParser.RefreshTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#refreshTable. + def exitRefreshTable(self, ctx:SqlBaseParser.RefreshTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#refreshFunction. + def enterRefreshFunction(self, ctx:SqlBaseParser.RefreshFunctionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#refreshFunction. + def exitRefreshFunction(self, ctx:SqlBaseParser.RefreshFunctionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#refreshResource. + def enterRefreshResource(self, ctx:SqlBaseParser.RefreshResourceContext): + pass + + # Exit a parse tree produced by SqlBaseParser#refreshResource. + def exitRefreshResource(self, ctx:SqlBaseParser.RefreshResourceContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#cacheTable. + def enterCacheTable(self, ctx:SqlBaseParser.CacheTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#cacheTable. + def exitCacheTable(self, ctx:SqlBaseParser.CacheTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#uncacheTable. + def enterUncacheTable(self, ctx:SqlBaseParser.UncacheTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#uncacheTable. + def exitUncacheTable(self, ctx:SqlBaseParser.UncacheTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#clearCache. + def enterClearCache(self, ctx:SqlBaseParser.ClearCacheContext): + pass + + # Exit a parse tree produced by SqlBaseParser#clearCache. + def exitClearCache(self, ctx:SqlBaseParser.ClearCacheContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#loadData. + def enterLoadData(self, ctx:SqlBaseParser.LoadDataContext): + pass + + # Exit a parse tree produced by SqlBaseParser#loadData. + def exitLoadData(self, ctx:SqlBaseParser.LoadDataContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#truncateTable. + def enterTruncateTable(self, ctx:SqlBaseParser.TruncateTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#truncateTable. + def exitTruncateTable(self, ctx:SqlBaseParser.TruncateTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#repairTable. + def enterRepairTable(self, ctx:SqlBaseParser.RepairTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#repairTable. + def exitRepairTable(self, ctx:SqlBaseParser.RepairTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#manageResource. + def enterManageResource(self, ctx:SqlBaseParser.ManageResourceContext): + pass + + # Exit a parse tree produced by SqlBaseParser#manageResource. + def exitManageResource(self, ctx:SqlBaseParser.ManageResourceContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#failNativeCommand. + def enterFailNativeCommand(self, ctx:SqlBaseParser.FailNativeCommandContext): + pass + + # Exit a parse tree produced by SqlBaseParser#failNativeCommand. + def exitFailNativeCommand(self, ctx:SqlBaseParser.FailNativeCommandContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#setTimeZone. + def enterSetTimeZone(self, ctx:SqlBaseParser.SetTimeZoneContext): + pass + + # Exit a parse tree produced by SqlBaseParser#setTimeZone. + def exitSetTimeZone(self, ctx:SqlBaseParser.SetTimeZoneContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#setQuotedConfiguration. + def enterSetQuotedConfiguration(self, ctx:SqlBaseParser.SetQuotedConfigurationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#setQuotedConfiguration. + def exitSetQuotedConfiguration(self, ctx:SqlBaseParser.SetQuotedConfigurationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#setConfiguration. + def enterSetConfiguration(self, ctx:SqlBaseParser.SetConfigurationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#setConfiguration. + def exitSetConfiguration(self, ctx:SqlBaseParser.SetConfigurationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#resetQuotedConfiguration. + def enterResetQuotedConfiguration(self, ctx:SqlBaseParser.ResetQuotedConfigurationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#resetQuotedConfiguration. + def exitResetQuotedConfiguration(self, ctx:SqlBaseParser.ResetQuotedConfigurationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#resetConfiguration. + def enterResetConfiguration(self, ctx:SqlBaseParser.ResetConfigurationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#resetConfiguration. + def exitResetConfiguration(self, ctx:SqlBaseParser.ResetConfigurationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#createIndex. + def enterCreateIndex(self, ctx:SqlBaseParser.CreateIndexContext): + pass + + # Exit a parse tree produced by SqlBaseParser#createIndex. + def exitCreateIndex(self, ctx:SqlBaseParser.CreateIndexContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#dropIndex. + def enterDropIndex(self, ctx:SqlBaseParser.DropIndexContext): + pass + + # Exit a parse tree produced by SqlBaseParser#dropIndex. + def exitDropIndex(self, ctx:SqlBaseParser.DropIndexContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#timezone. + def enterTimezone(self, ctx:SqlBaseParser.TimezoneContext): + pass + + # Exit a parse tree produced by SqlBaseParser#timezone. + def exitTimezone(self, ctx:SqlBaseParser.TimezoneContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#configKey. + def enterConfigKey(self, ctx:SqlBaseParser.ConfigKeyContext): + pass + + # Exit a parse tree produced by SqlBaseParser#configKey. + def exitConfigKey(self, ctx:SqlBaseParser.ConfigKeyContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#configValue. + def enterConfigValue(self, ctx:SqlBaseParser.ConfigValueContext): + pass + + # Exit a parse tree produced by SqlBaseParser#configValue. + def exitConfigValue(self, ctx:SqlBaseParser.ConfigValueContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unsupportedHiveNativeCommands. + def enterUnsupportedHiveNativeCommands(self, ctx:SqlBaseParser.UnsupportedHiveNativeCommandsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unsupportedHiveNativeCommands. + def exitUnsupportedHiveNativeCommands(self, ctx:SqlBaseParser.UnsupportedHiveNativeCommandsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#createTableHeader. + def enterCreateTableHeader(self, ctx:SqlBaseParser.CreateTableHeaderContext): + pass + + # Exit a parse tree produced by SqlBaseParser#createTableHeader. + def exitCreateTableHeader(self, ctx:SqlBaseParser.CreateTableHeaderContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#replaceTableHeader. + def enterReplaceTableHeader(self, ctx:SqlBaseParser.ReplaceTableHeaderContext): + pass + + # Exit a parse tree produced by SqlBaseParser#replaceTableHeader. + def exitReplaceTableHeader(self, ctx:SqlBaseParser.ReplaceTableHeaderContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#bucketSpec. + def enterBucketSpec(self, ctx:SqlBaseParser.BucketSpecContext): + pass + + # Exit a parse tree produced by SqlBaseParser#bucketSpec. + def exitBucketSpec(self, ctx:SqlBaseParser.BucketSpecContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#skewSpec. + def enterSkewSpec(self, ctx:SqlBaseParser.SkewSpecContext): + pass + + # Exit a parse tree produced by SqlBaseParser#skewSpec. + def exitSkewSpec(self, ctx:SqlBaseParser.SkewSpecContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#locationSpec. + def enterLocationSpec(self, ctx:SqlBaseParser.LocationSpecContext): + pass + + # Exit a parse tree produced by SqlBaseParser#locationSpec. + def exitLocationSpec(self, ctx:SqlBaseParser.LocationSpecContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#commentSpec. + def enterCommentSpec(self, ctx:SqlBaseParser.CommentSpecContext): + pass + + # Exit a parse tree produced by SqlBaseParser#commentSpec. + def exitCommentSpec(self, ctx:SqlBaseParser.CommentSpecContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#insertOverwriteTable. + def enterInsertOverwriteTable(self, ctx:SqlBaseParser.InsertOverwriteTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#insertOverwriteTable. + def exitInsertOverwriteTable(self, ctx:SqlBaseParser.InsertOverwriteTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#insertIntoTable. + def enterInsertIntoTable(self, ctx:SqlBaseParser.InsertIntoTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#insertIntoTable. + def exitInsertIntoTable(self, ctx:SqlBaseParser.InsertIntoTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#insertIntoReplaceWhere. + def enterInsertIntoReplaceWhere(self, ctx:SqlBaseParser.InsertIntoReplaceWhereContext): + pass + + # Exit a parse tree produced by SqlBaseParser#insertIntoReplaceWhere. + def exitInsertIntoReplaceWhere(self, ctx:SqlBaseParser.InsertIntoReplaceWhereContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#insertOverwriteHiveDir. + def enterInsertOverwriteHiveDir(self, ctx:SqlBaseParser.InsertOverwriteHiveDirContext): + pass + + # Exit a parse tree produced by SqlBaseParser#insertOverwriteHiveDir. + def exitInsertOverwriteHiveDir(self, ctx:SqlBaseParser.InsertOverwriteHiveDirContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#insertOverwriteDir. + def enterInsertOverwriteDir(self, ctx:SqlBaseParser.InsertOverwriteDirContext): + pass + + # Exit a parse tree produced by SqlBaseParser#insertOverwriteDir. + def exitInsertOverwriteDir(self, ctx:SqlBaseParser.InsertOverwriteDirContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#partitionSpecLocation. + def enterPartitionSpecLocation(self, ctx:SqlBaseParser.PartitionSpecLocationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#partitionSpecLocation. + def exitPartitionSpecLocation(self, ctx:SqlBaseParser.PartitionSpecLocationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#partitionSpec. + def enterPartitionSpec(self, ctx:SqlBaseParser.PartitionSpecContext): + pass + + # Exit a parse tree produced by SqlBaseParser#partitionSpec. + def exitPartitionSpec(self, ctx:SqlBaseParser.PartitionSpecContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#partitionVal. + def enterPartitionVal(self, ctx:SqlBaseParser.PartitionValContext): + pass + + # Exit a parse tree produced by SqlBaseParser#partitionVal. + def exitPartitionVal(self, ctx:SqlBaseParser.PartitionValContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#namespace. + def enterNamespace(self, ctx:SqlBaseParser.NamespaceContext): + pass + + # Exit a parse tree produced by SqlBaseParser#namespace. + def exitNamespace(self, ctx:SqlBaseParser.NamespaceContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#namespaces. + def enterNamespaces(self, ctx:SqlBaseParser.NamespacesContext): + pass + + # Exit a parse tree produced by SqlBaseParser#namespaces. + def exitNamespaces(self, ctx:SqlBaseParser.NamespacesContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#describeFuncName. + def enterDescribeFuncName(self, ctx:SqlBaseParser.DescribeFuncNameContext): + pass + + # Exit a parse tree produced by SqlBaseParser#describeFuncName. + def exitDescribeFuncName(self, ctx:SqlBaseParser.DescribeFuncNameContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#describeColName. + def enterDescribeColName(self, ctx:SqlBaseParser.DescribeColNameContext): + pass + + # Exit a parse tree produced by SqlBaseParser#describeColName. + def exitDescribeColName(self, ctx:SqlBaseParser.DescribeColNameContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#ctes. + def enterCtes(self, ctx:SqlBaseParser.CtesContext): + pass + + # Exit a parse tree produced by SqlBaseParser#ctes. + def exitCtes(self, ctx:SqlBaseParser.CtesContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#query. + def enterQuery(self, ctx:SqlBaseParser.QueryContext): + pass + + # Exit a parse tree produced by SqlBaseParser#query. + def exitQuery(self, ctx:SqlBaseParser.QueryContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#namedQuery. + def enterNamedQuery(self, ctx:SqlBaseParser.NamedQueryContext): + pass + + # Exit a parse tree produced by SqlBaseParser#namedQuery. + def exitNamedQuery(self, ctx:SqlBaseParser.NamedQueryContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#queryTermDefault. + def enterQueryTermDefault(self, ctx:SqlBaseParser.QueryTermDefaultContext): + pass + + # Exit a parse tree produced by SqlBaseParser#queryTermDefault. + def exitQueryTermDefault(self, ctx:SqlBaseParser.QueryTermDefaultContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#setOperation. + def enterSetOperation(self, ctx:SqlBaseParser.SetOperationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#setOperation. + def exitSetOperation(self, ctx:SqlBaseParser.SetOperationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#transformQuerySpecification. + def enterTransformQuerySpecification(self, ctx:SqlBaseParser.TransformQuerySpecificationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#transformQuerySpecification. + def exitTransformQuerySpecification(self, ctx:SqlBaseParser.TransformQuerySpecificationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#regularQuerySpecification. + def enterRegularQuerySpecification(self, ctx:SqlBaseParser.RegularQuerySpecificationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#regularQuerySpecification. + def exitRegularQuerySpecification(self, ctx:SqlBaseParser.RegularQuerySpecificationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#queryPrimaryDefault. + def enterQueryPrimaryDefault(self, ctx:SqlBaseParser.QueryPrimaryDefaultContext): + pass + + # Exit a parse tree produced by SqlBaseParser#queryPrimaryDefault. + def exitQueryPrimaryDefault(self, ctx:SqlBaseParser.QueryPrimaryDefaultContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#fromStmt. + def enterFromStmt(self, ctx:SqlBaseParser.FromStmtContext): + pass + + # Exit a parse tree produced by SqlBaseParser#fromStmt. + def exitFromStmt(self, ctx:SqlBaseParser.FromStmtContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#table. + def enterTable(self, ctx:SqlBaseParser.TableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#table. + def exitTable(self, ctx:SqlBaseParser.TableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#inlineTableDefault1. + def enterInlineTableDefault1(self, ctx:SqlBaseParser.InlineTableDefault1Context): + pass + + # Exit a parse tree produced by SqlBaseParser#inlineTableDefault1. + def exitInlineTableDefault1(self, ctx:SqlBaseParser.InlineTableDefault1Context): + pass + + + # Enter a parse tree produced by SqlBaseParser#subquery. + def enterSubquery(self, ctx:SqlBaseParser.SubqueryContext): + pass + + # Exit a parse tree produced by SqlBaseParser#subquery. + def exitSubquery(self, ctx:SqlBaseParser.SubqueryContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#tableProvider. + def enterTableProvider(self, ctx:SqlBaseParser.TableProviderContext): + pass + + # Exit a parse tree produced by SqlBaseParser#tableProvider. + def exitTableProvider(self, ctx:SqlBaseParser.TableProviderContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#createTableClauses. + def enterCreateTableClauses(self, ctx:SqlBaseParser.CreateTableClausesContext): + pass + + # Exit a parse tree produced by SqlBaseParser#createTableClauses. + def exitCreateTableClauses(self, ctx:SqlBaseParser.CreateTableClausesContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#propertyList. + def enterPropertyList(self, ctx:SqlBaseParser.PropertyListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#propertyList. + def exitPropertyList(self, ctx:SqlBaseParser.PropertyListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#property. + def enterProperty(self, ctx:SqlBaseParser.PropertyContext): + pass + + # Exit a parse tree produced by SqlBaseParser#property. + def exitProperty(self, ctx:SqlBaseParser.PropertyContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#propertyKey. + def enterPropertyKey(self, ctx:SqlBaseParser.PropertyKeyContext): + pass + + # Exit a parse tree produced by SqlBaseParser#propertyKey. + def exitPropertyKey(self, ctx:SqlBaseParser.PropertyKeyContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#propertyValue. + def enterPropertyValue(self, ctx:SqlBaseParser.PropertyValueContext): + pass + + # Exit a parse tree produced by SqlBaseParser#propertyValue. + def exitPropertyValue(self, ctx:SqlBaseParser.PropertyValueContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#constantList. + def enterConstantList(self, ctx:SqlBaseParser.ConstantListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#constantList. + def exitConstantList(self, ctx:SqlBaseParser.ConstantListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#nestedConstantList. + def enterNestedConstantList(self, ctx:SqlBaseParser.NestedConstantListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#nestedConstantList. + def exitNestedConstantList(self, ctx:SqlBaseParser.NestedConstantListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#createFileFormat. + def enterCreateFileFormat(self, ctx:SqlBaseParser.CreateFileFormatContext): + pass + + # Exit a parse tree produced by SqlBaseParser#createFileFormat. + def exitCreateFileFormat(self, ctx:SqlBaseParser.CreateFileFormatContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#tableFileFormat. + def enterTableFileFormat(self, ctx:SqlBaseParser.TableFileFormatContext): + pass + + # Exit a parse tree produced by SqlBaseParser#tableFileFormat. + def exitTableFileFormat(self, ctx:SqlBaseParser.TableFileFormatContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#genericFileFormat. + def enterGenericFileFormat(self, ctx:SqlBaseParser.GenericFileFormatContext): + pass + + # Exit a parse tree produced by SqlBaseParser#genericFileFormat. + def exitGenericFileFormat(self, ctx:SqlBaseParser.GenericFileFormatContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#storageHandler. + def enterStorageHandler(self, ctx:SqlBaseParser.StorageHandlerContext): + pass + + # Exit a parse tree produced by SqlBaseParser#storageHandler. + def exitStorageHandler(self, ctx:SqlBaseParser.StorageHandlerContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#resource. + def enterResource(self, ctx:SqlBaseParser.ResourceContext): + pass + + # Exit a parse tree produced by SqlBaseParser#resource. + def exitResource(self, ctx:SqlBaseParser.ResourceContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#singleInsertQuery. + def enterSingleInsertQuery(self, ctx:SqlBaseParser.SingleInsertQueryContext): + pass + + # Exit a parse tree produced by SqlBaseParser#singleInsertQuery. + def exitSingleInsertQuery(self, ctx:SqlBaseParser.SingleInsertQueryContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#multiInsertQuery. + def enterMultiInsertQuery(self, ctx:SqlBaseParser.MultiInsertQueryContext): + pass + + # Exit a parse tree produced by SqlBaseParser#multiInsertQuery. + def exitMultiInsertQuery(self, ctx:SqlBaseParser.MultiInsertQueryContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#deleteFromTable. + def enterDeleteFromTable(self, ctx:SqlBaseParser.DeleteFromTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#deleteFromTable. + def exitDeleteFromTable(self, ctx:SqlBaseParser.DeleteFromTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#updateTable. + def enterUpdateTable(self, ctx:SqlBaseParser.UpdateTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#updateTable. + def exitUpdateTable(self, ctx:SqlBaseParser.UpdateTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#mergeIntoTable. + def enterMergeIntoTable(self, ctx:SqlBaseParser.MergeIntoTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#mergeIntoTable. + def exitMergeIntoTable(self, ctx:SqlBaseParser.MergeIntoTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#queryOrganization. + def enterQueryOrganization(self, ctx:SqlBaseParser.QueryOrganizationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#queryOrganization. + def exitQueryOrganization(self, ctx:SqlBaseParser.QueryOrganizationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#multiInsertQueryBody. + def enterMultiInsertQueryBody(self, ctx:SqlBaseParser.MultiInsertQueryBodyContext): + pass + + # Exit a parse tree produced by SqlBaseParser#multiInsertQueryBody. + def exitMultiInsertQueryBody(self, ctx:SqlBaseParser.MultiInsertQueryBodyContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#sortItem. + def enterSortItem(self, ctx:SqlBaseParser.SortItemContext): + pass + + # Exit a parse tree produced by SqlBaseParser#sortItem. + def exitSortItem(self, ctx:SqlBaseParser.SortItemContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#fromStatement. + def enterFromStatement(self, ctx:SqlBaseParser.FromStatementContext): + pass + + # Exit a parse tree produced by SqlBaseParser#fromStatement. + def exitFromStatement(self, ctx:SqlBaseParser.FromStatementContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#fromStatementBody. + def enterFromStatementBody(self, ctx:SqlBaseParser.FromStatementBodyContext): + pass + + # Exit a parse tree produced by SqlBaseParser#fromStatementBody. + def exitFromStatementBody(self, ctx:SqlBaseParser.FromStatementBodyContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#transformClause. + def enterTransformClause(self, ctx:SqlBaseParser.TransformClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#transformClause. + def exitTransformClause(self, ctx:SqlBaseParser.TransformClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#selectClause. + def enterSelectClause(self, ctx:SqlBaseParser.SelectClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#selectClause. + def exitSelectClause(self, ctx:SqlBaseParser.SelectClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#setClause. + def enterSetClause(self, ctx:SqlBaseParser.SetClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#setClause. + def exitSetClause(self, ctx:SqlBaseParser.SetClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#matchedClause. + def enterMatchedClause(self, ctx:SqlBaseParser.MatchedClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#matchedClause. + def exitMatchedClause(self, ctx:SqlBaseParser.MatchedClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#notMatchedClause. + def enterNotMatchedClause(self, ctx:SqlBaseParser.NotMatchedClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#notMatchedClause. + def exitNotMatchedClause(self, ctx:SqlBaseParser.NotMatchedClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#notMatchedBySourceClause. + def enterNotMatchedBySourceClause(self, ctx:SqlBaseParser.NotMatchedBySourceClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#notMatchedBySourceClause. + def exitNotMatchedBySourceClause(self, ctx:SqlBaseParser.NotMatchedBySourceClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#matchedAction. + def enterMatchedAction(self, ctx:SqlBaseParser.MatchedActionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#matchedAction. + def exitMatchedAction(self, ctx:SqlBaseParser.MatchedActionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#notMatchedAction. + def enterNotMatchedAction(self, ctx:SqlBaseParser.NotMatchedActionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#notMatchedAction. + def exitNotMatchedAction(self, ctx:SqlBaseParser.NotMatchedActionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#notMatchedBySourceAction. + def enterNotMatchedBySourceAction(self, ctx:SqlBaseParser.NotMatchedBySourceActionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#notMatchedBySourceAction. + def exitNotMatchedBySourceAction(self, ctx:SqlBaseParser.NotMatchedBySourceActionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#assignmentList. + def enterAssignmentList(self, ctx:SqlBaseParser.AssignmentListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#assignmentList. + def exitAssignmentList(self, ctx:SqlBaseParser.AssignmentListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#assignment. + def enterAssignment(self, ctx:SqlBaseParser.AssignmentContext): + pass + + # Exit a parse tree produced by SqlBaseParser#assignment. + def exitAssignment(self, ctx:SqlBaseParser.AssignmentContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#whereClause. + def enterWhereClause(self, ctx:SqlBaseParser.WhereClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#whereClause. + def exitWhereClause(self, ctx:SqlBaseParser.WhereClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#havingClause. + def enterHavingClause(self, ctx:SqlBaseParser.HavingClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#havingClause. + def exitHavingClause(self, ctx:SqlBaseParser.HavingClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#hint. + def enterHint(self, ctx:SqlBaseParser.HintContext): + pass + + # Exit a parse tree produced by SqlBaseParser#hint. + def exitHint(self, ctx:SqlBaseParser.HintContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#hintStatement. + def enterHintStatement(self, ctx:SqlBaseParser.HintStatementContext): + pass + + # Exit a parse tree produced by SqlBaseParser#hintStatement. + def exitHintStatement(self, ctx:SqlBaseParser.HintStatementContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#fromClause. + def enterFromClause(self, ctx:SqlBaseParser.FromClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#fromClause. + def exitFromClause(self, ctx:SqlBaseParser.FromClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#temporalClause. + def enterTemporalClause(self, ctx:SqlBaseParser.TemporalClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#temporalClause. + def exitTemporalClause(self, ctx:SqlBaseParser.TemporalClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#aggregationClause. + def enterAggregationClause(self, ctx:SqlBaseParser.AggregationClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#aggregationClause. + def exitAggregationClause(self, ctx:SqlBaseParser.AggregationClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#groupByClause. + def enterGroupByClause(self, ctx:SqlBaseParser.GroupByClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#groupByClause. + def exitGroupByClause(self, ctx:SqlBaseParser.GroupByClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#groupingAnalytics. + def enterGroupingAnalytics(self, ctx:SqlBaseParser.GroupingAnalyticsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#groupingAnalytics. + def exitGroupingAnalytics(self, ctx:SqlBaseParser.GroupingAnalyticsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#groupingElement. + def enterGroupingElement(self, ctx:SqlBaseParser.GroupingElementContext): + pass + + # Exit a parse tree produced by SqlBaseParser#groupingElement. + def exitGroupingElement(self, ctx:SqlBaseParser.GroupingElementContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#groupingSet. + def enterGroupingSet(self, ctx:SqlBaseParser.GroupingSetContext): + pass + + # Exit a parse tree produced by SqlBaseParser#groupingSet. + def exitGroupingSet(self, ctx:SqlBaseParser.GroupingSetContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#pivotClause. + def enterPivotClause(self, ctx:SqlBaseParser.PivotClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#pivotClause. + def exitPivotClause(self, ctx:SqlBaseParser.PivotClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#pivotColumn. + def enterPivotColumn(self, ctx:SqlBaseParser.PivotColumnContext): + pass + + # Exit a parse tree produced by SqlBaseParser#pivotColumn. + def exitPivotColumn(self, ctx:SqlBaseParser.PivotColumnContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#pivotValue. + def enterPivotValue(self, ctx:SqlBaseParser.PivotValueContext): + pass + + # Exit a parse tree produced by SqlBaseParser#pivotValue. + def exitPivotValue(self, ctx:SqlBaseParser.PivotValueContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unpivotClause. + def enterUnpivotClause(self, ctx:SqlBaseParser.UnpivotClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unpivotClause. + def exitUnpivotClause(self, ctx:SqlBaseParser.UnpivotClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unpivotNullClause. + def enterUnpivotNullClause(self, ctx:SqlBaseParser.UnpivotNullClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unpivotNullClause. + def exitUnpivotNullClause(self, ctx:SqlBaseParser.UnpivotNullClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unpivotOperator. + def enterUnpivotOperator(self, ctx:SqlBaseParser.UnpivotOperatorContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unpivotOperator. + def exitUnpivotOperator(self, ctx:SqlBaseParser.UnpivotOperatorContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unpivotSingleValueColumnClause. + def enterUnpivotSingleValueColumnClause(self, ctx:SqlBaseParser.UnpivotSingleValueColumnClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unpivotSingleValueColumnClause. + def exitUnpivotSingleValueColumnClause(self, ctx:SqlBaseParser.UnpivotSingleValueColumnClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unpivotMultiValueColumnClause. + def enterUnpivotMultiValueColumnClause(self, ctx:SqlBaseParser.UnpivotMultiValueColumnClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unpivotMultiValueColumnClause. + def exitUnpivotMultiValueColumnClause(self, ctx:SqlBaseParser.UnpivotMultiValueColumnClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unpivotColumnSet. + def enterUnpivotColumnSet(self, ctx:SqlBaseParser.UnpivotColumnSetContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unpivotColumnSet. + def exitUnpivotColumnSet(self, ctx:SqlBaseParser.UnpivotColumnSetContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unpivotValueColumn. + def enterUnpivotValueColumn(self, ctx:SqlBaseParser.UnpivotValueColumnContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unpivotValueColumn. + def exitUnpivotValueColumn(self, ctx:SqlBaseParser.UnpivotValueColumnContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unpivotNameColumn. + def enterUnpivotNameColumn(self, ctx:SqlBaseParser.UnpivotNameColumnContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unpivotNameColumn. + def exitUnpivotNameColumn(self, ctx:SqlBaseParser.UnpivotNameColumnContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unpivotColumnAndAlias. + def enterUnpivotColumnAndAlias(self, ctx:SqlBaseParser.UnpivotColumnAndAliasContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unpivotColumnAndAlias. + def exitUnpivotColumnAndAlias(self, ctx:SqlBaseParser.UnpivotColumnAndAliasContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unpivotColumn. + def enterUnpivotColumn(self, ctx:SqlBaseParser.UnpivotColumnContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unpivotColumn. + def exitUnpivotColumn(self, ctx:SqlBaseParser.UnpivotColumnContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unpivotAlias. + def enterUnpivotAlias(self, ctx:SqlBaseParser.UnpivotAliasContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unpivotAlias. + def exitUnpivotAlias(self, ctx:SqlBaseParser.UnpivotAliasContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#lateralView. + def enterLateralView(self, ctx:SqlBaseParser.LateralViewContext): + pass + + # Exit a parse tree produced by SqlBaseParser#lateralView. + def exitLateralView(self, ctx:SqlBaseParser.LateralViewContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#setQuantifier. + def enterSetQuantifier(self, ctx:SqlBaseParser.SetQuantifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#setQuantifier. + def exitSetQuantifier(self, ctx:SqlBaseParser.SetQuantifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#relation. + def enterRelation(self, ctx:SqlBaseParser.RelationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#relation. + def exitRelation(self, ctx:SqlBaseParser.RelationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#relationExtension. + def enterRelationExtension(self, ctx:SqlBaseParser.RelationExtensionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#relationExtension. + def exitRelationExtension(self, ctx:SqlBaseParser.RelationExtensionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#joinRelation. + def enterJoinRelation(self, ctx:SqlBaseParser.JoinRelationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#joinRelation. + def exitJoinRelation(self, ctx:SqlBaseParser.JoinRelationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#joinType. + def enterJoinType(self, ctx:SqlBaseParser.JoinTypeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#joinType. + def exitJoinType(self, ctx:SqlBaseParser.JoinTypeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#joinCriteria. + def enterJoinCriteria(self, ctx:SqlBaseParser.JoinCriteriaContext): + pass + + # Exit a parse tree produced by SqlBaseParser#joinCriteria. + def exitJoinCriteria(self, ctx:SqlBaseParser.JoinCriteriaContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#sample. + def enterSample(self, ctx:SqlBaseParser.SampleContext): + pass + + # Exit a parse tree produced by SqlBaseParser#sample. + def exitSample(self, ctx:SqlBaseParser.SampleContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#sampleByPercentile. + def enterSampleByPercentile(self, ctx:SqlBaseParser.SampleByPercentileContext): + pass + + # Exit a parse tree produced by SqlBaseParser#sampleByPercentile. + def exitSampleByPercentile(self, ctx:SqlBaseParser.SampleByPercentileContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#sampleByRows. + def enterSampleByRows(self, ctx:SqlBaseParser.SampleByRowsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#sampleByRows. + def exitSampleByRows(self, ctx:SqlBaseParser.SampleByRowsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#sampleByBucket. + def enterSampleByBucket(self, ctx:SqlBaseParser.SampleByBucketContext): + pass + + # Exit a parse tree produced by SqlBaseParser#sampleByBucket. + def exitSampleByBucket(self, ctx:SqlBaseParser.SampleByBucketContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#sampleByBytes. + def enterSampleByBytes(self, ctx:SqlBaseParser.SampleByBytesContext): + pass + + # Exit a parse tree produced by SqlBaseParser#sampleByBytes. + def exitSampleByBytes(self, ctx:SqlBaseParser.SampleByBytesContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#identifierList. + def enterIdentifierList(self, ctx:SqlBaseParser.IdentifierListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#identifierList. + def exitIdentifierList(self, ctx:SqlBaseParser.IdentifierListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#identifierSeq. + def enterIdentifierSeq(self, ctx:SqlBaseParser.IdentifierSeqContext): + pass + + # Exit a parse tree produced by SqlBaseParser#identifierSeq. + def exitIdentifierSeq(self, ctx:SqlBaseParser.IdentifierSeqContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#orderedIdentifierList. + def enterOrderedIdentifierList(self, ctx:SqlBaseParser.OrderedIdentifierListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#orderedIdentifierList. + def exitOrderedIdentifierList(self, ctx:SqlBaseParser.OrderedIdentifierListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#orderedIdentifier. + def enterOrderedIdentifier(self, ctx:SqlBaseParser.OrderedIdentifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#orderedIdentifier. + def exitOrderedIdentifier(self, ctx:SqlBaseParser.OrderedIdentifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#identifierCommentList. + def enterIdentifierCommentList(self, ctx:SqlBaseParser.IdentifierCommentListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#identifierCommentList. + def exitIdentifierCommentList(self, ctx:SqlBaseParser.IdentifierCommentListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#identifierComment. + def enterIdentifierComment(self, ctx:SqlBaseParser.IdentifierCommentContext): + pass + + # Exit a parse tree produced by SqlBaseParser#identifierComment. + def exitIdentifierComment(self, ctx:SqlBaseParser.IdentifierCommentContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#tableName. + def enterTableName(self, ctx:SqlBaseParser.TableNameContext): + pass + + # Exit a parse tree produced by SqlBaseParser#tableName. + def exitTableName(self, ctx:SqlBaseParser.TableNameContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#aliasedQuery. + def enterAliasedQuery(self, ctx:SqlBaseParser.AliasedQueryContext): + pass + + # Exit a parse tree produced by SqlBaseParser#aliasedQuery. + def exitAliasedQuery(self, ctx:SqlBaseParser.AliasedQueryContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#aliasedRelation. + def enterAliasedRelation(self, ctx:SqlBaseParser.AliasedRelationContext): + pass + + # Exit a parse tree produced by SqlBaseParser#aliasedRelation. + def exitAliasedRelation(self, ctx:SqlBaseParser.AliasedRelationContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#inlineTableDefault2. + def enterInlineTableDefault2(self, ctx:SqlBaseParser.InlineTableDefault2Context): + pass + + # Exit a parse tree produced by SqlBaseParser#inlineTableDefault2. + def exitInlineTableDefault2(self, ctx:SqlBaseParser.InlineTableDefault2Context): + pass + + + # Enter a parse tree produced by SqlBaseParser#tableValuedFunction. + def enterTableValuedFunction(self, ctx:SqlBaseParser.TableValuedFunctionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#tableValuedFunction. + def exitTableValuedFunction(self, ctx:SqlBaseParser.TableValuedFunctionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#inlineTable. + def enterInlineTable(self, ctx:SqlBaseParser.InlineTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#inlineTable. + def exitInlineTable(self, ctx:SqlBaseParser.InlineTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#functionTable. + def enterFunctionTable(self, ctx:SqlBaseParser.FunctionTableContext): + pass + + # Exit a parse tree produced by SqlBaseParser#functionTable. + def exitFunctionTable(self, ctx:SqlBaseParser.FunctionTableContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#tableAlias. + def enterTableAlias(self, ctx:SqlBaseParser.TableAliasContext): + pass + + # Exit a parse tree produced by SqlBaseParser#tableAlias. + def exitTableAlias(self, ctx:SqlBaseParser.TableAliasContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#rowFormatSerde. + def enterRowFormatSerde(self, ctx:SqlBaseParser.RowFormatSerdeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#rowFormatSerde. + def exitRowFormatSerde(self, ctx:SqlBaseParser.RowFormatSerdeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#rowFormatDelimited. + def enterRowFormatDelimited(self, ctx:SqlBaseParser.RowFormatDelimitedContext): + pass + + # Exit a parse tree produced by SqlBaseParser#rowFormatDelimited. + def exitRowFormatDelimited(self, ctx:SqlBaseParser.RowFormatDelimitedContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#multipartIdentifierList. + def enterMultipartIdentifierList(self, ctx:SqlBaseParser.MultipartIdentifierListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#multipartIdentifierList. + def exitMultipartIdentifierList(self, ctx:SqlBaseParser.MultipartIdentifierListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#multipartIdentifier. + def enterMultipartIdentifier(self, ctx:SqlBaseParser.MultipartIdentifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#multipartIdentifier. + def exitMultipartIdentifier(self, ctx:SqlBaseParser.MultipartIdentifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#multipartIdentifierPropertyList. + def enterMultipartIdentifierPropertyList(self, ctx:SqlBaseParser.MultipartIdentifierPropertyListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#multipartIdentifierPropertyList. + def exitMultipartIdentifierPropertyList(self, ctx:SqlBaseParser.MultipartIdentifierPropertyListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#multipartIdentifierProperty. + def enterMultipartIdentifierProperty(self, ctx:SqlBaseParser.MultipartIdentifierPropertyContext): + pass + + # Exit a parse tree produced by SqlBaseParser#multipartIdentifierProperty. + def exitMultipartIdentifierProperty(self, ctx:SqlBaseParser.MultipartIdentifierPropertyContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#tableIdentifier. + def enterTableIdentifier(self, ctx:SqlBaseParser.TableIdentifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#tableIdentifier. + def exitTableIdentifier(self, ctx:SqlBaseParser.TableIdentifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#functionIdentifier. + def enterFunctionIdentifier(self, ctx:SqlBaseParser.FunctionIdentifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#functionIdentifier. + def exitFunctionIdentifier(self, ctx:SqlBaseParser.FunctionIdentifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#namedExpression. + def enterNamedExpression(self, ctx:SqlBaseParser.NamedExpressionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#namedExpression. + def exitNamedExpression(self, ctx:SqlBaseParser.NamedExpressionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#namedExpressionSeq. + def enterNamedExpressionSeq(self, ctx:SqlBaseParser.NamedExpressionSeqContext): + pass + + # Exit a parse tree produced by SqlBaseParser#namedExpressionSeq. + def exitNamedExpressionSeq(self, ctx:SqlBaseParser.NamedExpressionSeqContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#partitionFieldList. + def enterPartitionFieldList(self, ctx:SqlBaseParser.PartitionFieldListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#partitionFieldList. + def exitPartitionFieldList(self, ctx:SqlBaseParser.PartitionFieldListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#partitionTransform. + def enterPartitionTransform(self, ctx:SqlBaseParser.PartitionTransformContext): + pass + + # Exit a parse tree produced by SqlBaseParser#partitionTransform. + def exitPartitionTransform(self, ctx:SqlBaseParser.PartitionTransformContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#partitionColumn. + def enterPartitionColumn(self, ctx:SqlBaseParser.PartitionColumnContext): + pass + + # Exit a parse tree produced by SqlBaseParser#partitionColumn. + def exitPartitionColumn(self, ctx:SqlBaseParser.PartitionColumnContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#identityTransform. + def enterIdentityTransform(self, ctx:SqlBaseParser.IdentityTransformContext): + pass + + # Exit a parse tree produced by SqlBaseParser#identityTransform. + def exitIdentityTransform(self, ctx:SqlBaseParser.IdentityTransformContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#applyTransform. + def enterApplyTransform(self, ctx:SqlBaseParser.ApplyTransformContext): + pass + + # Exit a parse tree produced by SqlBaseParser#applyTransform. + def exitApplyTransform(self, ctx:SqlBaseParser.ApplyTransformContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#transformArgument. + def enterTransformArgument(self, ctx:SqlBaseParser.TransformArgumentContext): + pass + + # Exit a parse tree produced by SqlBaseParser#transformArgument. + def exitTransformArgument(self, ctx:SqlBaseParser.TransformArgumentContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#expression. + def enterExpression(self, ctx:SqlBaseParser.ExpressionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#expression. + def exitExpression(self, ctx:SqlBaseParser.ExpressionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#expressionSeq. + def enterExpressionSeq(self, ctx:SqlBaseParser.ExpressionSeqContext): + pass + + # Exit a parse tree produced by SqlBaseParser#expressionSeq. + def exitExpressionSeq(self, ctx:SqlBaseParser.ExpressionSeqContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#logicalNot. + def enterLogicalNot(self, ctx:SqlBaseParser.LogicalNotContext): + pass + + # Exit a parse tree produced by SqlBaseParser#logicalNot. + def exitLogicalNot(self, ctx:SqlBaseParser.LogicalNotContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#predicated. + def enterPredicated(self, ctx:SqlBaseParser.PredicatedContext): + pass + + # Exit a parse tree produced by SqlBaseParser#predicated. + def exitPredicated(self, ctx:SqlBaseParser.PredicatedContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#exists. + def enterExists(self, ctx:SqlBaseParser.ExistsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#exists. + def exitExists(self, ctx:SqlBaseParser.ExistsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#logicalBinary. + def enterLogicalBinary(self, ctx:SqlBaseParser.LogicalBinaryContext): + pass + + # Exit a parse tree produced by SqlBaseParser#logicalBinary. + def exitLogicalBinary(self, ctx:SqlBaseParser.LogicalBinaryContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#predicate. + def enterPredicate(self, ctx:SqlBaseParser.PredicateContext): + pass + + # Exit a parse tree produced by SqlBaseParser#predicate. + def exitPredicate(self, ctx:SqlBaseParser.PredicateContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#valueExpressionDefault. + def enterValueExpressionDefault(self, ctx:SqlBaseParser.ValueExpressionDefaultContext): + pass + + # Exit a parse tree produced by SqlBaseParser#valueExpressionDefault. + def exitValueExpressionDefault(self, ctx:SqlBaseParser.ValueExpressionDefaultContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#comparison. + def enterComparison(self, ctx:SqlBaseParser.ComparisonContext): + pass + + # Exit a parse tree produced by SqlBaseParser#comparison. + def exitComparison(self, ctx:SqlBaseParser.ComparisonContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#arithmeticBinary. + def enterArithmeticBinary(self, ctx:SqlBaseParser.ArithmeticBinaryContext): + pass + + # Exit a parse tree produced by SqlBaseParser#arithmeticBinary. + def exitArithmeticBinary(self, ctx:SqlBaseParser.ArithmeticBinaryContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#arithmeticUnary. + def enterArithmeticUnary(self, ctx:SqlBaseParser.ArithmeticUnaryContext): + pass + + # Exit a parse tree produced by SqlBaseParser#arithmeticUnary. + def exitArithmeticUnary(self, ctx:SqlBaseParser.ArithmeticUnaryContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#datetimeUnit. + def enterDatetimeUnit(self, ctx:SqlBaseParser.DatetimeUnitContext): + pass + + # Exit a parse tree produced by SqlBaseParser#datetimeUnit. + def exitDatetimeUnit(self, ctx:SqlBaseParser.DatetimeUnitContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#struct. + def enterStruct(self, ctx:SqlBaseParser.StructContext): + pass + + # Exit a parse tree produced by SqlBaseParser#struct. + def exitStruct(self, ctx:SqlBaseParser.StructContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#dereference. + def enterDereference(self, ctx:SqlBaseParser.DereferenceContext): + pass + + # Exit a parse tree produced by SqlBaseParser#dereference. + def exitDereference(self, ctx:SqlBaseParser.DereferenceContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#timestampadd. + def enterTimestampadd(self, ctx:SqlBaseParser.TimestampaddContext): + pass + + # Exit a parse tree produced by SqlBaseParser#timestampadd. + def exitTimestampadd(self, ctx:SqlBaseParser.TimestampaddContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#substring. + def enterSubstring(self, ctx:SqlBaseParser.SubstringContext): + pass + + # Exit a parse tree produced by SqlBaseParser#substring. + def exitSubstring(self, ctx:SqlBaseParser.SubstringContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#cast. + def enterCast(self, ctx:SqlBaseParser.CastContext): + pass + + # Exit a parse tree produced by SqlBaseParser#cast. + def exitCast(self, ctx:SqlBaseParser.CastContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#lambda. + def enterLambda(self, ctx:SqlBaseParser.LambdaContext): + pass + + # Exit a parse tree produced by SqlBaseParser#lambda. + def exitLambda(self, ctx:SqlBaseParser.LambdaContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#parenthesizedExpression. + def enterParenthesizedExpression(self, ctx:SqlBaseParser.ParenthesizedExpressionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#parenthesizedExpression. + def exitParenthesizedExpression(self, ctx:SqlBaseParser.ParenthesizedExpressionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#any_value. + def enterAny_value(self, ctx:SqlBaseParser.Any_valueContext): + pass + + # Exit a parse tree produced by SqlBaseParser#any_value. + def exitAny_value(self, ctx:SqlBaseParser.Any_valueContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#trim. + def enterTrim(self, ctx:SqlBaseParser.TrimContext): + pass + + # Exit a parse tree produced by SqlBaseParser#trim. + def exitTrim(self, ctx:SqlBaseParser.TrimContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#simpleCase. + def enterSimpleCase(self, ctx:SqlBaseParser.SimpleCaseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#simpleCase. + def exitSimpleCase(self, ctx:SqlBaseParser.SimpleCaseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#currentLike. + def enterCurrentLike(self, ctx:SqlBaseParser.CurrentLikeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#currentLike. + def exitCurrentLike(self, ctx:SqlBaseParser.CurrentLikeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#columnReference. + def enterColumnReference(self, ctx:SqlBaseParser.ColumnReferenceContext): + pass + + # Exit a parse tree produced by SqlBaseParser#columnReference. + def exitColumnReference(self, ctx:SqlBaseParser.ColumnReferenceContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#rowConstructor. + def enterRowConstructor(self, ctx:SqlBaseParser.RowConstructorContext): + pass + + # Exit a parse tree produced by SqlBaseParser#rowConstructor. + def exitRowConstructor(self, ctx:SqlBaseParser.RowConstructorContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#last. + def enterLast(self, ctx:SqlBaseParser.LastContext): + pass + + # Exit a parse tree produced by SqlBaseParser#last. + def exitLast(self, ctx:SqlBaseParser.LastContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#star. + def enterStar(self, ctx:SqlBaseParser.StarContext): + pass + + # Exit a parse tree produced by SqlBaseParser#star. + def exitStar(self, ctx:SqlBaseParser.StarContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#overlay. + def enterOverlay(self, ctx:SqlBaseParser.OverlayContext): + pass + + # Exit a parse tree produced by SqlBaseParser#overlay. + def exitOverlay(self, ctx:SqlBaseParser.OverlayContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#subscript. + def enterSubscript(self, ctx:SqlBaseParser.SubscriptContext): + pass + + # Exit a parse tree produced by SqlBaseParser#subscript. + def exitSubscript(self, ctx:SqlBaseParser.SubscriptContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#timestampdiff. + def enterTimestampdiff(self, ctx:SqlBaseParser.TimestampdiffContext): + pass + + # Exit a parse tree produced by SqlBaseParser#timestampdiff. + def exitTimestampdiff(self, ctx:SqlBaseParser.TimestampdiffContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#subqueryExpression. + def enterSubqueryExpression(self, ctx:SqlBaseParser.SubqueryExpressionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#subqueryExpression. + def exitSubqueryExpression(self, ctx:SqlBaseParser.SubqueryExpressionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#constantDefault. + def enterConstantDefault(self, ctx:SqlBaseParser.ConstantDefaultContext): + pass + + # Exit a parse tree produced by SqlBaseParser#constantDefault. + def exitConstantDefault(self, ctx:SqlBaseParser.ConstantDefaultContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#extract. + def enterExtract(self, ctx:SqlBaseParser.ExtractContext): + pass + + # Exit a parse tree produced by SqlBaseParser#extract. + def exitExtract(self, ctx:SqlBaseParser.ExtractContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#percentile. + def enterPercentile(self, ctx:SqlBaseParser.PercentileContext): + pass + + # Exit a parse tree produced by SqlBaseParser#percentile. + def exitPercentile(self, ctx:SqlBaseParser.PercentileContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#functionCall. + def enterFunctionCall(self, ctx:SqlBaseParser.FunctionCallContext): + pass + + # Exit a parse tree produced by SqlBaseParser#functionCall. + def exitFunctionCall(self, ctx:SqlBaseParser.FunctionCallContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#searchedCase. + def enterSearchedCase(self, ctx:SqlBaseParser.SearchedCaseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#searchedCase. + def exitSearchedCase(self, ctx:SqlBaseParser.SearchedCaseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#position. + def enterPosition(self, ctx:SqlBaseParser.PositionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#position. + def exitPosition(self, ctx:SqlBaseParser.PositionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#first. + def enterFirst(self, ctx:SqlBaseParser.FirstContext): + pass + + # Exit a parse tree produced by SqlBaseParser#first. + def exitFirst(self, ctx:SqlBaseParser.FirstContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#nullLiteral. + def enterNullLiteral(self, ctx:SqlBaseParser.NullLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#nullLiteral. + def exitNullLiteral(self, ctx:SqlBaseParser.NullLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#parameterLiteral. + def enterParameterLiteral(self, ctx:SqlBaseParser.ParameterLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#parameterLiteral. + def exitParameterLiteral(self, ctx:SqlBaseParser.ParameterLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#intervalLiteral. + def enterIntervalLiteral(self, ctx:SqlBaseParser.IntervalLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#intervalLiteral. + def exitIntervalLiteral(self, ctx:SqlBaseParser.IntervalLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#typeConstructor. + def enterTypeConstructor(self, ctx:SqlBaseParser.TypeConstructorContext): + pass + + # Exit a parse tree produced by SqlBaseParser#typeConstructor. + def exitTypeConstructor(self, ctx:SqlBaseParser.TypeConstructorContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#numericLiteral. + def enterNumericLiteral(self, ctx:SqlBaseParser.NumericLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#numericLiteral. + def exitNumericLiteral(self, ctx:SqlBaseParser.NumericLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#booleanLiteral. + def enterBooleanLiteral(self, ctx:SqlBaseParser.BooleanLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#booleanLiteral. + def exitBooleanLiteral(self, ctx:SqlBaseParser.BooleanLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#stringLiteral. + def enterStringLiteral(self, ctx:SqlBaseParser.StringLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#stringLiteral. + def exitStringLiteral(self, ctx:SqlBaseParser.StringLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#comparisonOperator. + def enterComparisonOperator(self, ctx:SqlBaseParser.ComparisonOperatorContext): + pass + + # Exit a parse tree produced by SqlBaseParser#comparisonOperator. + def exitComparisonOperator(self, ctx:SqlBaseParser.ComparisonOperatorContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#arithmeticOperator. + def enterArithmeticOperator(self, ctx:SqlBaseParser.ArithmeticOperatorContext): + pass + + # Exit a parse tree produced by SqlBaseParser#arithmeticOperator. + def exitArithmeticOperator(self, ctx:SqlBaseParser.ArithmeticOperatorContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#predicateOperator. + def enterPredicateOperator(self, ctx:SqlBaseParser.PredicateOperatorContext): + pass + + # Exit a parse tree produced by SqlBaseParser#predicateOperator. + def exitPredicateOperator(self, ctx:SqlBaseParser.PredicateOperatorContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#booleanValue. + def enterBooleanValue(self, ctx:SqlBaseParser.BooleanValueContext): + pass + + # Exit a parse tree produced by SqlBaseParser#booleanValue. + def exitBooleanValue(self, ctx:SqlBaseParser.BooleanValueContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#interval. + def enterInterval(self, ctx:SqlBaseParser.IntervalContext): + pass + + # Exit a parse tree produced by SqlBaseParser#interval. + def exitInterval(self, ctx:SqlBaseParser.IntervalContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#errorCapturingMultiUnitsInterval. + def enterErrorCapturingMultiUnitsInterval(self, ctx:SqlBaseParser.ErrorCapturingMultiUnitsIntervalContext): + pass + + # Exit a parse tree produced by SqlBaseParser#errorCapturingMultiUnitsInterval. + def exitErrorCapturingMultiUnitsInterval(self, ctx:SqlBaseParser.ErrorCapturingMultiUnitsIntervalContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#multiUnitsInterval. + def enterMultiUnitsInterval(self, ctx:SqlBaseParser.MultiUnitsIntervalContext): + pass + + # Exit a parse tree produced by SqlBaseParser#multiUnitsInterval. + def exitMultiUnitsInterval(self, ctx:SqlBaseParser.MultiUnitsIntervalContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#errorCapturingUnitToUnitInterval. + def enterErrorCapturingUnitToUnitInterval(self, ctx:SqlBaseParser.ErrorCapturingUnitToUnitIntervalContext): + pass + + # Exit a parse tree produced by SqlBaseParser#errorCapturingUnitToUnitInterval. + def exitErrorCapturingUnitToUnitInterval(self, ctx:SqlBaseParser.ErrorCapturingUnitToUnitIntervalContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unitToUnitInterval. + def enterUnitToUnitInterval(self, ctx:SqlBaseParser.UnitToUnitIntervalContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unitToUnitInterval. + def exitUnitToUnitInterval(self, ctx:SqlBaseParser.UnitToUnitIntervalContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#intervalValue. + def enterIntervalValue(self, ctx:SqlBaseParser.IntervalValueContext): + pass + + # Exit a parse tree produced by SqlBaseParser#intervalValue. + def exitIntervalValue(self, ctx:SqlBaseParser.IntervalValueContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unitInMultiUnits. + def enterUnitInMultiUnits(self, ctx:SqlBaseParser.UnitInMultiUnitsContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unitInMultiUnits. + def exitUnitInMultiUnits(self, ctx:SqlBaseParser.UnitInMultiUnitsContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unitInUnitToUnit. + def enterUnitInUnitToUnit(self, ctx:SqlBaseParser.UnitInUnitToUnitContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unitInUnitToUnit. + def exitUnitInUnitToUnit(self, ctx:SqlBaseParser.UnitInUnitToUnitContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#colPosition. + def enterColPosition(self, ctx:SqlBaseParser.ColPositionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#colPosition. + def exitColPosition(self, ctx:SqlBaseParser.ColPositionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#complexDataType. + def enterComplexDataType(self, ctx:SqlBaseParser.ComplexDataTypeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#complexDataType. + def exitComplexDataType(self, ctx:SqlBaseParser.ComplexDataTypeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#yearMonthIntervalDataType. + def enterYearMonthIntervalDataType(self, ctx:SqlBaseParser.YearMonthIntervalDataTypeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#yearMonthIntervalDataType. + def exitYearMonthIntervalDataType(self, ctx:SqlBaseParser.YearMonthIntervalDataTypeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#dayTimeIntervalDataType. + def enterDayTimeIntervalDataType(self, ctx:SqlBaseParser.DayTimeIntervalDataTypeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#dayTimeIntervalDataType. + def exitDayTimeIntervalDataType(self, ctx:SqlBaseParser.DayTimeIntervalDataTypeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#primitiveDataType. + def enterPrimitiveDataType(self, ctx:SqlBaseParser.PrimitiveDataTypeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#primitiveDataType. + def exitPrimitiveDataType(self, ctx:SqlBaseParser.PrimitiveDataTypeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#qualifiedColTypeWithPositionList. + def enterQualifiedColTypeWithPositionList(self, ctx:SqlBaseParser.QualifiedColTypeWithPositionListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#qualifiedColTypeWithPositionList. + def exitQualifiedColTypeWithPositionList(self, ctx:SqlBaseParser.QualifiedColTypeWithPositionListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#qualifiedColTypeWithPosition. + def enterQualifiedColTypeWithPosition(self, ctx:SqlBaseParser.QualifiedColTypeWithPositionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#qualifiedColTypeWithPosition. + def exitQualifiedColTypeWithPosition(self, ctx:SqlBaseParser.QualifiedColTypeWithPositionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#colDefinitionDescriptorWithPosition. + def enterColDefinitionDescriptorWithPosition(self, ctx:SqlBaseParser.ColDefinitionDescriptorWithPositionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#colDefinitionDescriptorWithPosition. + def exitColDefinitionDescriptorWithPosition(self, ctx:SqlBaseParser.ColDefinitionDescriptorWithPositionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#defaultExpression. + def enterDefaultExpression(self, ctx:SqlBaseParser.DefaultExpressionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#defaultExpression. + def exitDefaultExpression(self, ctx:SqlBaseParser.DefaultExpressionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#colTypeList. + def enterColTypeList(self, ctx:SqlBaseParser.ColTypeListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#colTypeList. + def exitColTypeList(self, ctx:SqlBaseParser.ColTypeListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#colType. + def enterColType(self, ctx:SqlBaseParser.ColTypeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#colType. + def exitColType(self, ctx:SqlBaseParser.ColTypeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#createOrReplaceTableColTypeList. + def enterCreateOrReplaceTableColTypeList(self, ctx:SqlBaseParser.CreateOrReplaceTableColTypeListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#createOrReplaceTableColTypeList. + def exitCreateOrReplaceTableColTypeList(self, ctx:SqlBaseParser.CreateOrReplaceTableColTypeListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#createOrReplaceTableColType. + def enterCreateOrReplaceTableColType(self, ctx:SqlBaseParser.CreateOrReplaceTableColTypeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#createOrReplaceTableColType. + def exitCreateOrReplaceTableColType(self, ctx:SqlBaseParser.CreateOrReplaceTableColTypeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#colDefinitionOption. + def enterColDefinitionOption(self, ctx:SqlBaseParser.ColDefinitionOptionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#colDefinitionOption. + def exitColDefinitionOption(self, ctx:SqlBaseParser.ColDefinitionOptionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#generationExpression. + def enterGenerationExpression(self, ctx:SqlBaseParser.GenerationExpressionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#generationExpression. + def exitGenerationExpression(self, ctx:SqlBaseParser.GenerationExpressionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#complexColTypeList. + def enterComplexColTypeList(self, ctx:SqlBaseParser.ComplexColTypeListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#complexColTypeList. + def exitComplexColTypeList(self, ctx:SqlBaseParser.ComplexColTypeListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#complexColType. + def enterComplexColType(self, ctx:SqlBaseParser.ComplexColTypeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#complexColType. + def exitComplexColType(self, ctx:SqlBaseParser.ComplexColTypeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#whenClause. + def enterWhenClause(self, ctx:SqlBaseParser.WhenClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#whenClause. + def exitWhenClause(self, ctx:SqlBaseParser.WhenClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#windowClause. + def enterWindowClause(self, ctx:SqlBaseParser.WindowClauseContext): + pass + + # Exit a parse tree produced by SqlBaseParser#windowClause. + def exitWindowClause(self, ctx:SqlBaseParser.WindowClauseContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#namedWindow. + def enterNamedWindow(self, ctx:SqlBaseParser.NamedWindowContext): + pass + + # Exit a parse tree produced by SqlBaseParser#namedWindow. + def exitNamedWindow(self, ctx:SqlBaseParser.NamedWindowContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#windowRef. + def enterWindowRef(self, ctx:SqlBaseParser.WindowRefContext): + pass + + # Exit a parse tree produced by SqlBaseParser#windowRef. + def exitWindowRef(self, ctx:SqlBaseParser.WindowRefContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#windowDef. + def enterWindowDef(self, ctx:SqlBaseParser.WindowDefContext): + pass + + # Exit a parse tree produced by SqlBaseParser#windowDef. + def exitWindowDef(self, ctx:SqlBaseParser.WindowDefContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#windowFrame. + def enterWindowFrame(self, ctx:SqlBaseParser.WindowFrameContext): + pass + + # Exit a parse tree produced by SqlBaseParser#windowFrame. + def exitWindowFrame(self, ctx:SqlBaseParser.WindowFrameContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#frameBound. + def enterFrameBound(self, ctx:SqlBaseParser.FrameBoundContext): + pass + + # Exit a parse tree produced by SqlBaseParser#frameBound. + def exitFrameBound(self, ctx:SqlBaseParser.FrameBoundContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#qualifiedNameList. + def enterQualifiedNameList(self, ctx:SqlBaseParser.QualifiedNameListContext): + pass + + # Exit a parse tree produced by SqlBaseParser#qualifiedNameList. + def exitQualifiedNameList(self, ctx:SqlBaseParser.QualifiedNameListContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#functionName. + def enterFunctionName(self, ctx:SqlBaseParser.FunctionNameContext): + pass + + # Exit a parse tree produced by SqlBaseParser#functionName. + def exitFunctionName(self, ctx:SqlBaseParser.FunctionNameContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#qualifiedName. + def enterQualifiedName(self, ctx:SqlBaseParser.QualifiedNameContext): + pass + + # Exit a parse tree produced by SqlBaseParser#qualifiedName. + def exitQualifiedName(self, ctx:SqlBaseParser.QualifiedNameContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#errorCapturingIdentifier. + def enterErrorCapturingIdentifier(self, ctx:SqlBaseParser.ErrorCapturingIdentifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#errorCapturingIdentifier. + def exitErrorCapturingIdentifier(self, ctx:SqlBaseParser.ErrorCapturingIdentifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#errorIdent. + def enterErrorIdent(self, ctx:SqlBaseParser.ErrorIdentContext): + pass + + # Exit a parse tree produced by SqlBaseParser#errorIdent. + def exitErrorIdent(self, ctx:SqlBaseParser.ErrorIdentContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#realIdent. + def enterRealIdent(self, ctx:SqlBaseParser.RealIdentContext): + pass + + # Exit a parse tree produced by SqlBaseParser#realIdent. + def exitRealIdent(self, ctx:SqlBaseParser.RealIdentContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#identifier. + def enterIdentifier(self, ctx:SqlBaseParser.IdentifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#identifier. + def exitIdentifier(self, ctx:SqlBaseParser.IdentifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#unquotedIdentifier. + def enterUnquotedIdentifier(self, ctx:SqlBaseParser.UnquotedIdentifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#unquotedIdentifier. + def exitUnquotedIdentifier(self, ctx:SqlBaseParser.UnquotedIdentifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#quotedIdentifierAlternative. + def enterQuotedIdentifierAlternative(self, ctx:SqlBaseParser.QuotedIdentifierAlternativeContext): + pass + + # Exit a parse tree produced by SqlBaseParser#quotedIdentifierAlternative. + def exitQuotedIdentifierAlternative(self, ctx:SqlBaseParser.QuotedIdentifierAlternativeContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#quotedIdentifier. + def enterQuotedIdentifier(self, ctx:SqlBaseParser.QuotedIdentifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#quotedIdentifier. + def exitQuotedIdentifier(self, ctx:SqlBaseParser.QuotedIdentifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#backQuotedIdentifier. + def enterBackQuotedIdentifier(self, ctx:SqlBaseParser.BackQuotedIdentifierContext): + pass + + # Exit a parse tree produced by SqlBaseParser#backQuotedIdentifier. + def exitBackQuotedIdentifier(self, ctx:SqlBaseParser.BackQuotedIdentifierContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#exponentLiteral. + def enterExponentLiteral(self, ctx:SqlBaseParser.ExponentLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#exponentLiteral. + def exitExponentLiteral(self, ctx:SqlBaseParser.ExponentLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#decimalLiteral. + def enterDecimalLiteral(self, ctx:SqlBaseParser.DecimalLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#decimalLiteral. + def exitDecimalLiteral(self, ctx:SqlBaseParser.DecimalLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#legacyDecimalLiteral. + def enterLegacyDecimalLiteral(self, ctx:SqlBaseParser.LegacyDecimalLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#legacyDecimalLiteral. + def exitLegacyDecimalLiteral(self, ctx:SqlBaseParser.LegacyDecimalLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#integerLiteral. + def enterIntegerLiteral(self, ctx:SqlBaseParser.IntegerLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#integerLiteral. + def exitIntegerLiteral(self, ctx:SqlBaseParser.IntegerLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#bigIntLiteral. + def enterBigIntLiteral(self, ctx:SqlBaseParser.BigIntLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#bigIntLiteral. + def exitBigIntLiteral(self, ctx:SqlBaseParser.BigIntLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#smallIntLiteral. + def enterSmallIntLiteral(self, ctx:SqlBaseParser.SmallIntLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#smallIntLiteral. + def exitSmallIntLiteral(self, ctx:SqlBaseParser.SmallIntLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#tinyIntLiteral. + def enterTinyIntLiteral(self, ctx:SqlBaseParser.TinyIntLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#tinyIntLiteral. + def exitTinyIntLiteral(self, ctx:SqlBaseParser.TinyIntLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#doubleLiteral. + def enterDoubleLiteral(self, ctx:SqlBaseParser.DoubleLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#doubleLiteral. + def exitDoubleLiteral(self, ctx:SqlBaseParser.DoubleLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#floatLiteral. + def enterFloatLiteral(self, ctx:SqlBaseParser.FloatLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#floatLiteral. + def exitFloatLiteral(self, ctx:SqlBaseParser.FloatLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#bigDecimalLiteral. + def enterBigDecimalLiteral(self, ctx:SqlBaseParser.BigDecimalLiteralContext): + pass + + # Exit a parse tree produced by SqlBaseParser#bigDecimalLiteral. + def exitBigDecimalLiteral(self, ctx:SqlBaseParser.BigDecimalLiteralContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#alterColumnAction. + def enterAlterColumnAction(self, ctx:SqlBaseParser.AlterColumnActionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#alterColumnAction. + def exitAlterColumnAction(self, ctx:SqlBaseParser.AlterColumnActionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#stringLit. + def enterStringLit(self, ctx:SqlBaseParser.StringLitContext): + pass + + # Exit a parse tree produced by SqlBaseParser#stringLit. + def exitStringLit(self, ctx:SqlBaseParser.StringLitContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#comment. + def enterComment(self, ctx:SqlBaseParser.CommentContext): + pass + + # Exit a parse tree produced by SqlBaseParser#comment. + def exitComment(self, ctx:SqlBaseParser.CommentContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#version. + def enterVersion(self, ctx:SqlBaseParser.VersionContext): + pass + + # Exit a parse tree produced by SqlBaseParser#version. + def exitVersion(self, ctx:SqlBaseParser.VersionContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#ansiNonReserved. + def enterAnsiNonReserved(self, ctx:SqlBaseParser.AnsiNonReservedContext): + pass + + # Exit a parse tree produced by SqlBaseParser#ansiNonReserved. + def exitAnsiNonReserved(self, ctx:SqlBaseParser.AnsiNonReservedContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#strictNonReserved. + def enterStrictNonReserved(self, ctx:SqlBaseParser.StrictNonReservedContext): + pass + + # Exit a parse tree produced by SqlBaseParser#strictNonReserved. + def exitStrictNonReserved(self, ctx:SqlBaseParser.StrictNonReservedContext): + pass + + + # Enter a parse tree produced by SqlBaseParser#nonReserved. + def enterNonReserved(self, ctx:SqlBaseParser.NonReservedContext): + pass + + # Exit a parse tree produced by SqlBaseParser#nonReserved. + def exitNonReserved(self, ctx:SqlBaseParser.NonReservedContext): + pass + + + +del SqlBaseParser \ No newline at end of file diff --git a/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParserVisitor.py b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParserVisitor.py new file mode 100644 index 000000000..dc9ffa6f9 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/backends/grammar/generated/SqlBaseParserVisitor.py @@ -0,0 +1,1628 @@ +# Generated from SqlBaseParser.g4 by ANTLR 4.13.1 +from antlr4 import * +if "." in __name__: + from .SqlBaseParser import SqlBaseParser +else: + from SqlBaseParser import SqlBaseParser + +# This class defines a complete generic visitor for a parse tree produced by SqlBaseParser. + +class SqlBaseParserVisitor(ParseTreeVisitor): + + # Visit a parse tree produced by SqlBaseParser#singleStatement. + def visitSingleStatement(self, ctx:SqlBaseParser.SingleStatementContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#singleExpression. + def visitSingleExpression(self, ctx:SqlBaseParser.SingleExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#singleTableIdentifier. + def visitSingleTableIdentifier(self, ctx:SqlBaseParser.SingleTableIdentifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#singleMultipartIdentifier. + def visitSingleMultipartIdentifier(self, ctx:SqlBaseParser.SingleMultipartIdentifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#singleFunctionIdentifier. + def visitSingleFunctionIdentifier(self, ctx:SqlBaseParser.SingleFunctionIdentifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#singleDataType. + def visitSingleDataType(self, ctx:SqlBaseParser.SingleDataTypeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#singleTableSchema. + def visitSingleTableSchema(self, ctx:SqlBaseParser.SingleTableSchemaContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#statementDefault. + def visitStatementDefault(self, ctx:SqlBaseParser.StatementDefaultContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#dmlStatement. + def visitDmlStatement(self, ctx:SqlBaseParser.DmlStatementContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#use. + def visitUse(self, ctx:SqlBaseParser.UseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#useNamespace. + def visitUseNamespace(self, ctx:SqlBaseParser.UseNamespaceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#setCatalog. + def visitSetCatalog(self, ctx:SqlBaseParser.SetCatalogContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#createNamespace. + def visitCreateNamespace(self, ctx:SqlBaseParser.CreateNamespaceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#setNamespaceProperties. + def visitSetNamespaceProperties(self, ctx:SqlBaseParser.SetNamespacePropertiesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#setNamespaceLocation. + def visitSetNamespaceLocation(self, ctx:SqlBaseParser.SetNamespaceLocationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#dropNamespace. + def visitDropNamespace(self, ctx:SqlBaseParser.DropNamespaceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#showNamespaces. + def visitShowNamespaces(self, ctx:SqlBaseParser.ShowNamespacesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#createTable. + def visitCreateTable(self, ctx:SqlBaseParser.CreateTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#createTableLike. + def visitCreateTableLike(self, ctx:SqlBaseParser.CreateTableLikeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#replaceTable. + def visitReplaceTable(self, ctx:SqlBaseParser.ReplaceTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#analyze. + def visitAnalyze(self, ctx:SqlBaseParser.AnalyzeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#analyzeTables. + def visitAnalyzeTables(self, ctx:SqlBaseParser.AnalyzeTablesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#addTableColumns. + def visitAddTableColumns(self, ctx:SqlBaseParser.AddTableColumnsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#renameTableColumn. + def visitRenameTableColumn(self, ctx:SqlBaseParser.RenameTableColumnContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#dropTableColumns. + def visitDropTableColumns(self, ctx:SqlBaseParser.DropTableColumnsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#renameTable. + def visitRenameTable(self, ctx:SqlBaseParser.RenameTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#setTableProperties. + def visitSetTableProperties(self, ctx:SqlBaseParser.SetTablePropertiesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unsetTableProperties. + def visitUnsetTableProperties(self, ctx:SqlBaseParser.UnsetTablePropertiesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#alterTableAlterColumn. + def visitAlterTableAlterColumn(self, ctx:SqlBaseParser.AlterTableAlterColumnContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#hiveChangeColumn. + def visitHiveChangeColumn(self, ctx:SqlBaseParser.HiveChangeColumnContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#hiveReplaceColumns. + def visitHiveReplaceColumns(self, ctx:SqlBaseParser.HiveReplaceColumnsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#setTableSerDe. + def visitSetTableSerDe(self, ctx:SqlBaseParser.SetTableSerDeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#addTablePartition. + def visitAddTablePartition(self, ctx:SqlBaseParser.AddTablePartitionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#renameTablePartition. + def visitRenameTablePartition(self, ctx:SqlBaseParser.RenameTablePartitionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#dropTablePartitions. + def visitDropTablePartitions(self, ctx:SqlBaseParser.DropTablePartitionsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#setTableLocation. + def visitSetTableLocation(self, ctx:SqlBaseParser.SetTableLocationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#recoverPartitions. + def visitRecoverPartitions(self, ctx:SqlBaseParser.RecoverPartitionsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#dropTable. + def visitDropTable(self, ctx:SqlBaseParser.DropTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#dropView. + def visitDropView(self, ctx:SqlBaseParser.DropViewContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#createView. + def visitCreateView(self, ctx:SqlBaseParser.CreateViewContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#createTempViewUsing. + def visitCreateTempViewUsing(self, ctx:SqlBaseParser.CreateTempViewUsingContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#alterViewQuery. + def visitAlterViewQuery(self, ctx:SqlBaseParser.AlterViewQueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#createFunction. + def visitCreateFunction(self, ctx:SqlBaseParser.CreateFunctionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#dropFunction. + def visitDropFunction(self, ctx:SqlBaseParser.DropFunctionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#explain. + def visitExplain(self, ctx:SqlBaseParser.ExplainContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#showTables. + def visitShowTables(self, ctx:SqlBaseParser.ShowTablesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#showTableExtended. + def visitShowTableExtended(self, ctx:SqlBaseParser.ShowTableExtendedContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#showTblProperties. + def visitShowTblProperties(self, ctx:SqlBaseParser.ShowTblPropertiesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#showColumns. + def visitShowColumns(self, ctx:SqlBaseParser.ShowColumnsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#showViews. + def visitShowViews(self, ctx:SqlBaseParser.ShowViewsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#showPartitions. + def visitShowPartitions(self, ctx:SqlBaseParser.ShowPartitionsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#showFunctions. + def visitShowFunctions(self, ctx:SqlBaseParser.ShowFunctionsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#showCreateTable. + def visitShowCreateTable(self, ctx:SqlBaseParser.ShowCreateTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#showCurrentNamespace. + def visitShowCurrentNamespace(self, ctx:SqlBaseParser.ShowCurrentNamespaceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#showCatalogs. + def visitShowCatalogs(self, ctx:SqlBaseParser.ShowCatalogsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#describeFunction. + def visitDescribeFunction(self, ctx:SqlBaseParser.DescribeFunctionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#describeNamespace. + def visitDescribeNamespace(self, ctx:SqlBaseParser.DescribeNamespaceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#describeRelation. + def visitDescribeRelation(self, ctx:SqlBaseParser.DescribeRelationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#describeQuery. + def visitDescribeQuery(self, ctx:SqlBaseParser.DescribeQueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#commentNamespace. + def visitCommentNamespace(self, ctx:SqlBaseParser.CommentNamespaceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#commentTable. + def visitCommentTable(self, ctx:SqlBaseParser.CommentTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#refreshTable. + def visitRefreshTable(self, ctx:SqlBaseParser.RefreshTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#refreshFunction. + def visitRefreshFunction(self, ctx:SqlBaseParser.RefreshFunctionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#refreshResource. + def visitRefreshResource(self, ctx:SqlBaseParser.RefreshResourceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#cacheTable. + def visitCacheTable(self, ctx:SqlBaseParser.CacheTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#uncacheTable. + def visitUncacheTable(self, ctx:SqlBaseParser.UncacheTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#clearCache. + def visitClearCache(self, ctx:SqlBaseParser.ClearCacheContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#loadData. + def visitLoadData(self, ctx:SqlBaseParser.LoadDataContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#truncateTable. + def visitTruncateTable(self, ctx:SqlBaseParser.TruncateTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#repairTable. + def visitRepairTable(self, ctx:SqlBaseParser.RepairTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#manageResource. + def visitManageResource(self, ctx:SqlBaseParser.ManageResourceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#failNativeCommand. + def visitFailNativeCommand(self, ctx:SqlBaseParser.FailNativeCommandContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#setTimeZone. + def visitSetTimeZone(self, ctx:SqlBaseParser.SetTimeZoneContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#setQuotedConfiguration. + def visitSetQuotedConfiguration(self, ctx:SqlBaseParser.SetQuotedConfigurationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#setConfiguration. + def visitSetConfiguration(self, ctx:SqlBaseParser.SetConfigurationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#resetQuotedConfiguration. + def visitResetQuotedConfiguration(self, ctx:SqlBaseParser.ResetQuotedConfigurationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#resetConfiguration. + def visitResetConfiguration(self, ctx:SqlBaseParser.ResetConfigurationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#createIndex. + def visitCreateIndex(self, ctx:SqlBaseParser.CreateIndexContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#dropIndex. + def visitDropIndex(self, ctx:SqlBaseParser.DropIndexContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#timezone. + def visitTimezone(self, ctx:SqlBaseParser.TimezoneContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#configKey. + def visitConfigKey(self, ctx:SqlBaseParser.ConfigKeyContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#configValue. + def visitConfigValue(self, ctx:SqlBaseParser.ConfigValueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unsupportedHiveNativeCommands. + def visitUnsupportedHiveNativeCommands(self, ctx:SqlBaseParser.UnsupportedHiveNativeCommandsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#createTableHeader. + def visitCreateTableHeader(self, ctx:SqlBaseParser.CreateTableHeaderContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#replaceTableHeader. + def visitReplaceTableHeader(self, ctx:SqlBaseParser.ReplaceTableHeaderContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#bucketSpec. + def visitBucketSpec(self, ctx:SqlBaseParser.BucketSpecContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#skewSpec. + def visitSkewSpec(self, ctx:SqlBaseParser.SkewSpecContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#locationSpec. + def visitLocationSpec(self, ctx:SqlBaseParser.LocationSpecContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#commentSpec. + def visitCommentSpec(self, ctx:SqlBaseParser.CommentSpecContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#insertOverwriteTable. + def visitInsertOverwriteTable(self, ctx:SqlBaseParser.InsertOverwriteTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#insertIntoTable. + def visitInsertIntoTable(self, ctx:SqlBaseParser.InsertIntoTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#insertIntoReplaceWhere. + def visitInsertIntoReplaceWhere(self, ctx:SqlBaseParser.InsertIntoReplaceWhereContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#insertOverwriteHiveDir. + def visitInsertOverwriteHiveDir(self, ctx:SqlBaseParser.InsertOverwriteHiveDirContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#insertOverwriteDir. + def visitInsertOverwriteDir(self, ctx:SqlBaseParser.InsertOverwriteDirContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#partitionSpecLocation. + def visitPartitionSpecLocation(self, ctx:SqlBaseParser.PartitionSpecLocationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#partitionSpec. + def visitPartitionSpec(self, ctx:SqlBaseParser.PartitionSpecContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#partitionVal. + def visitPartitionVal(self, ctx:SqlBaseParser.PartitionValContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#namespace. + def visitNamespace(self, ctx:SqlBaseParser.NamespaceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#namespaces. + def visitNamespaces(self, ctx:SqlBaseParser.NamespacesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#describeFuncName. + def visitDescribeFuncName(self, ctx:SqlBaseParser.DescribeFuncNameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#describeColName. + def visitDescribeColName(self, ctx:SqlBaseParser.DescribeColNameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#ctes. + def visitCtes(self, ctx:SqlBaseParser.CtesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#query. + def visitQuery(self, ctx:SqlBaseParser.QueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#namedQuery. + def visitNamedQuery(self, ctx:SqlBaseParser.NamedQueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#queryTermDefault. + def visitQueryTermDefault(self, ctx:SqlBaseParser.QueryTermDefaultContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#setOperation. + def visitSetOperation(self, ctx:SqlBaseParser.SetOperationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#transformQuerySpecification. + def visitTransformQuerySpecification(self, ctx:SqlBaseParser.TransformQuerySpecificationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#regularQuerySpecification. + def visitRegularQuerySpecification(self, ctx:SqlBaseParser.RegularQuerySpecificationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#queryPrimaryDefault. + def visitQueryPrimaryDefault(self, ctx:SqlBaseParser.QueryPrimaryDefaultContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#fromStmt. + def visitFromStmt(self, ctx:SqlBaseParser.FromStmtContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#table. + def visitTable(self, ctx:SqlBaseParser.TableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#inlineTableDefault1. + def visitInlineTableDefault1(self, ctx:SqlBaseParser.InlineTableDefault1Context): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#subquery. + def visitSubquery(self, ctx:SqlBaseParser.SubqueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#tableProvider. + def visitTableProvider(self, ctx:SqlBaseParser.TableProviderContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#createTableClauses. + def visitCreateTableClauses(self, ctx:SqlBaseParser.CreateTableClausesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#propertyList. + def visitPropertyList(self, ctx:SqlBaseParser.PropertyListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#property. + def visitProperty(self, ctx:SqlBaseParser.PropertyContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#propertyKey. + def visitPropertyKey(self, ctx:SqlBaseParser.PropertyKeyContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#propertyValue. + def visitPropertyValue(self, ctx:SqlBaseParser.PropertyValueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#constantList. + def visitConstantList(self, ctx:SqlBaseParser.ConstantListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#nestedConstantList. + def visitNestedConstantList(self, ctx:SqlBaseParser.NestedConstantListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#createFileFormat. + def visitCreateFileFormat(self, ctx:SqlBaseParser.CreateFileFormatContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#tableFileFormat. + def visitTableFileFormat(self, ctx:SqlBaseParser.TableFileFormatContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#genericFileFormat. + def visitGenericFileFormat(self, ctx:SqlBaseParser.GenericFileFormatContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#storageHandler. + def visitStorageHandler(self, ctx:SqlBaseParser.StorageHandlerContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#resource. + def visitResource(self, ctx:SqlBaseParser.ResourceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#singleInsertQuery. + def visitSingleInsertQuery(self, ctx:SqlBaseParser.SingleInsertQueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#multiInsertQuery. + def visitMultiInsertQuery(self, ctx:SqlBaseParser.MultiInsertQueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#deleteFromTable. + def visitDeleteFromTable(self, ctx:SqlBaseParser.DeleteFromTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#updateTable. + def visitUpdateTable(self, ctx:SqlBaseParser.UpdateTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#mergeIntoTable. + def visitMergeIntoTable(self, ctx:SqlBaseParser.MergeIntoTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#queryOrganization. + def visitQueryOrganization(self, ctx:SqlBaseParser.QueryOrganizationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#multiInsertQueryBody. + def visitMultiInsertQueryBody(self, ctx:SqlBaseParser.MultiInsertQueryBodyContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#sortItem. + def visitSortItem(self, ctx:SqlBaseParser.SortItemContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#fromStatement. + def visitFromStatement(self, ctx:SqlBaseParser.FromStatementContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#fromStatementBody. + def visitFromStatementBody(self, ctx:SqlBaseParser.FromStatementBodyContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#transformClause. + def visitTransformClause(self, ctx:SqlBaseParser.TransformClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#selectClause. + def visitSelectClause(self, ctx:SqlBaseParser.SelectClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#setClause. + def visitSetClause(self, ctx:SqlBaseParser.SetClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#matchedClause. + def visitMatchedClause(self, ctx:SqlBaseParser.MatchedClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#notMatchedClause. + def visitNotMatchedClause(self, ctx:SqlBaseParser.NotMatchedClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#notMatchedBySourceClause. + def visitNotMatchedBySourceClause(self, ctx:SqlBaseParser.NotMatchedBySourceClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#matchedAction. + def visitMatchedAction(self, ctx:SqlBaseParser.MatchedActionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#notMatchedAction. + def visitNotMatchedAction(self, ctx:SqlBaseParser.NotMatchedActionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#notMatchedBySourceAction. + def visitNotMatchedBySourceAction(self, ctx:SqlBaseParser.NotMatchedBySourceActionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#assignmentList. + def visitAssignmentList(self, ctx:SqlBaseParser.AssignmentListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#assignment. + def visitAssignment(self, ctx:SqlBaseParser.AssignmentContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#whereClause. + def visitWhereClause(self, ctx:SqlBaseParser.WhereClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#havingClause. + def visitHavingClause(self, ctx:SqlBaseParser.HavingClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#hint. + def visitHint(self, ctx:SqlBaseParser.HintContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#hintStatement. + def visitHintStatement(self, ctx:SqlBaseParser.HintStatementContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#fromClause. + def visitFromClause(self, ctx:SqlBaseParser.FromClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#temporalClause. + def visitTemporalClause(self, ctx:SqlBaseParser.TemporalClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#aggregationClause. + def visitAggregationClause(self, ctx:SqlBaseParser.AggregationClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#groupByClause. + def visitGroupByClause(self, ctx:SqlBaseParser.GroupByClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#groupingAnalytics. + def visitGroupingAnalytics(self, ctx:SqlBaseParser.GroupingAnalyticsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#groupingElement. + def visitGroupingElement(self, ctx:SqlBaseParser.GroupingElementContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#groupingSet. + def visitGroupingSet(self, ctx:SqlBaseParser.GroupingSetContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#pivotClause. + def visitPivotClause(self, ctx:SqlBaseParser.PivotClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#pivotColumn. + def visitPivotColumn(self, ctx:SqlBaseParser.PivotColumnContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#pivotValue. + def visitPivotValue(self, ctx:SqlBaseParser.PivotValueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unpivotClause. + def visitUnpivotClause(self, ctx:SqlBaseParser.UnpivotClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unpivotNullClause. + def visitUnpivotNullClause(self, ctx:SqlBaseParser.UnpivotNullClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unpivotOperator. + def visitUnpivotOperator(self, ctx:SqlBaseParser.UnpivotOperatorContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unpivotSingleValueColumnClause. + def visitUnpivotSingleValueColumnClause(self, ctx:SqlBaseParser.UnpivotSingleValueColumnClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unpivotMultiValueColumnClause. + def visitUnpivotMultiValueColumnClause(self, ctx:SqlBaseParser.UnpivotMultiValueColumnClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unpivotColumnSet. + def visitUnpivotColumnSet(self, ctx:SqlBaseParser.UnpivotColumnSetContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unpivotValueColumn. + def visitUnpivotValueColumn(self, ctx:SqlBaseParser.UnpivotValueColumnContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unpivotNameColumn. + def visitUnpivotNameColumn(self, ctx:SqlBaseParser.UnpivotNameColumnContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unpivotColumnAndAlias. + def visitUnpivotColumnAndAlias(self, ctx:SqlBaseParser.UnpivotColumnAndAliasContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unpivotColumn. + def visitUnpivotColumn(self, ctx:SqlBaseParser.UnpivotColumnContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unpivotAlias. + def visitUnpivotAlias(self, ctx:SqlBaseParser.UnpivotAliasContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#lateralView. + def visitLateralView(self, ctx:SqlBaseParser.LateralViewContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#setQuantifier. + def visitSetQuantifier(self, ctx:SqlBaseParser.SetQuantifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#relation. + def visitRelation(self, ctx:SqlBaseParser.RelationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#relationExtension. + def visitRelationExtension(self, ctx:SqlBaseParser.RelationExtensionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#joinRelation. + def visitJoinRelation(self, ctx:SqlBaseParser.JoinRelationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#joinType. + def visitJoinType(self, ctx:SqlBaseParser.JoinTypeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#joinCriteria. + def visitJoinCriteria(self, ctx:SqlBaseParser.JoinCriteriaContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#sample. + def visitSample(self, ctx:SqlBaseParser.SampleContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#sampleByPercentile. + def visitSampleByPercentile(self, ctx:SqlBaseParser.SampleByPercentileContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#sampleByRows. + def visitSampleByRows(self, ctx:SqlBaseParser.SampleByRowsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#sampleByBucket. + def visitSampleByBucket(self, ctx:SqlBaseParser.SampleByBucketContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#sampleByBytes. + def visitSampleByBytes(self, ctx:SqlBaseParser.SampleByBytesContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#identifierList. + def visitIdentifierList(self, ctx:SqlBaseParser.IdentifierListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#identifierSeq. + def visitIdentifierSeq(self, ctx:SqlBaseParser.IdentifierSeqContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#orderedIdentifierList. + def visitOrderedIdentifierList(self, ctx:SqlBaseParser.OrderedIdentifierListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#orderedIdentifier. + def visitOrderedIdentifier(self, ctx:SqlBaseParser.OrderedIdentifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#identifierCommentList. + def visitIdentifierCommentList(self, ctx:SqlBaseParser.IdentifierCommentListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#identifierComment. + def visitIdentifierComment(self, ctx:SqlBaseParser.IdentifierCommentContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#tableName. + def visitTableName(self, ctx:SqlBaseParser.TableNameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#aliasedQuery. + def visitAliasedQuery(self, ctx:SqlBaseParser.AliasedQueryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#aliasedRelation. + def visitAliasedRelation(self, ctx:SqlBaseParser.AliasedRelationContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#inlineTableDefault2. + def visitInlineTableDefault2(self, ctx:SqlBaseParser.InlineTableDefault2Context): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#tableValuedFunction. + def visitTableValuedFunction(self, ctx:SqlBaseParser.TableValuedFunctionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#inlineTable. + def visitInlineTable(self, ctx:SqlBaseParser.InlineTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#functionTable. + def visitFunctionTable(self, ctx:SqlBaseParser.FunctionTableContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#tableAlias. + def visitTableAlias(self, ctx:SqlBaseParser.TableAliasContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#rowFormatSerde. + def visitRowFormatSerde(self, ctx:SqlBaseParser.RowFormatSerdeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#rowFormatDelimited. + def visitRowFormatDelimited(self, ctx:SqlBaseParser.RowFormatDelimitedContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#multipartIdentifierList. + def visitMultipartIdentifierList(self, ctx:SqlBaseParser.MultipartIdentifierListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#multipartIdentifier. + def visitMultipartIdentifier(self, ctx:SqlBaseParser.MultipartIdentifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#multipartIdentifierPropertyList. + def visitMultipartIdentifierPropertyList(self, ctx:SqlBaseParser.MultipartIdentifierPropertyListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#multipartIdentifierProperty. + def visitMultipartIdentifierProperty(self, ctx:SqlBaseParser.MultipartIdentifierPropertyContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#tableIdentifier. + def visitTableIdentifier(self, ctx:SqlBaseParser.TableIdentifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#functionIdentifier. + def visitFunctionIdentifier(self, ctx:SqlBaseParser.FunctionIdentifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#namedExpression. + def visitNamedExpression(self, ctx:SqlBaseParser.NamedExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#namedExpressionSeq. + def visitNamedExpressionSeq(self, ctx:SqlBaseParser.NamedExpressionSeqContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#partitionFieldList. + def visitPartitionFieldList(self, ctx:SqlBaseParser.PartitionFieldListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#partitionTransform. + def visitPartitionTransform(self, ctx:SqlBaseParser.PartitionTransformContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#partitionColumn. + def visitPartitionColumn(self, ctx:SqlBaseParser.PartitionColumnContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#identityTransform. + def visitIdentityTransform(self, ctx:SqlBaseParser.IdentityTransformContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#applyTransform. + def visitApplyTransform(self, ctx:SqlBaseParser.ApplyTransformContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#transformArgument. + def visitTransformArgument(self, ctx:SqlBaseParser.TransformArgumentContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#expression. + def visitExpression(self, ctx:SqlBaseParser.ExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#expressionSeq. + def visitExpressionSeq(self, ctx:SqlBaseParser.ExpressionSeqContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#logicalNot. + def visitLogicalNot(self, ctx:SqlBaseParser.LogicalNotContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#predicated. + def visitPredicated(self, ctx:SqlBaseParser.PredicatedContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#exists. + def visitExists(self, ctx:SqlBaseParser.ExistsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#logicalBinary. + def visitLogicalBinary(self, ctx:SqlBaseParser.LogicalBinaryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#predicate. + def visitPredicate(self, ctx:SqlBaseParser.PredicateContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#valueExpressionDefault. + def visitValueExpressionDefault(self, ctx:SqlBaseParser.ValueExpressionDefaultContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#comparison. + def visitComparison(self, ctx:SqlBaseParser.ComparisonContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#arithmeticBinary. + def visitArithmeticBinary(self, ctx:SqlBaseParser.ArithmeticBinaryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#arithmeticUnary. + def visitArithmeticUnary(self, ctx:SqlBaseParser.ArithmeticUnaryContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#datetimeUnit. + def visitDatetimeUnit(self, ctx:SqlBaseParser.DatetimeUnitContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#struct. + def visitStruct(self, ctx:SqlBaseParser.StructContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#dereference. + def visitDereference(self, ctx:SqlBaseParser.DereferenceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#timestampadd. + def visitTimestampadd(self, ctx:SqlBaseParser.TimestampaddContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#substring. + def visitSubstring(self, ctx:SqlBaseParser.SubstringContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#cast. + def visitCast(self, ctx:SqlBaseParser.CastContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#lambda. + def visitLambda(self, ctx:SqlBaseParser.LambdaContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#parenthesizedExpression. + def visitParenthesizedExpression(self, ctx:SqlBaseParser.ParenthesizedExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#any_value. + def visitAny_value(self, ctx:SqlBaseParser.Any_valueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#trim. + def visitTrim(self, ctx:SqlBaseParser.TrimContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#simpleCase. + def visitSimpleCase(self, ctx:SqlBaseParser.SimpleCaseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#currentLike. + def visitCurrentLike(self, ctx:SqlBaseParser.CurrentLikeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#columnReference. + def visitColumnReference(self, ctx:SqlBaseParser.ColumnReferenceContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#rowConstructor. + def visitRowConstructor(self, ctx:SqlBaseParser.RowConstructorContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#last. + def visitLast(self, ctx:SqlBaseParser.LastContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#star. + def visitStar(self, ctx:SqlBaseParser.StarContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#overlay. + def visitOverlay(self, ctx:SqlBaseParser.OverlayContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#subscript. + def visitSubscript(self, ctx:SqlBaseParser.SubscriptContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#timestampdiff. + def visitTimestampdiff(self, ctx:SqlBaseParser.TimestampdiffContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#subqueryExpression. + def visitSubqueryExpression(self, ctx:SqlBaseParser.SubqueryExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#constantDefault. + def visitConstantDefault(self, ctx:SqlBaseParser.ConstantDefaultContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#extract. + def visitExtract(self, ctx:SqlBaseParser.ExtractContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#percentile. + def visitPercentile(self, ctx:SqlBaseParser.PercentileContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#functionCall. + def visitFunctionCall(self, ctx:SqlBaseParser.FunctionCallContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#searchedCase. + def visitSearchedCase(self, ctx:SqlBaseParser.SearchedCaseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#position. + def visitPosition(self, ctx:SqlBaseParser.PositionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#first. + def visitFirst(self, ctx:SqlBaseParser.FirstContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#nullLiteral. + def visitNullLiteral(self, ctx:SqlBaseParser.NullLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#parameterLiteral. + def visitParameterLiteral(self, ctx:SqlBaseParser.ParameterLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#intervalLiteral. + def visitIntervalLiteral(self, ctx:SqlBaseParser.IntervalLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#typeConstructor. + def visitTypeConstructor(self, ctx:SqlBaseParser.TypeConstructorContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#numericLiteral. + def visitNumericLiteral(self, ctx:SqlBaseParser.NumericLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#booleanLiteral. + def visitBooleanLiteral(self, ctx:SqlBaseParser.BooleanLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#stringLiteral. + def visitStringLiteral(self, ctx:SqlBaseParser.StringLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#comparisonOperator. + def visitComparisonOperator(self, ctx:SqlBaseParser.ComparisonOperatorContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#arithmeticOperator. + def visitArithmeticOperator(self, ctx:SqlBaseParser.ArithmeticOperatorContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#predicateOperator. + def visitPredicateOperator(self, ctx:SqlBaseParser.PredicateOperatorContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#booleanValue. + def visitBooleanValue(self, ctx:SqlBaseParser.BooleanValueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#interval. + def visitInterval(self, ctx:SqlBaseParser.IntervalContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#errorCapturingMultiUnitsInterval. + def visitErrorCapturingMultiUnitsInterval(self, ctx:SqlBaseParser.ErrorCapturingMultiUnitsIntervalContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#multiUnitsInterval. + def visitMultiUnitsInterval(self, ctx:SqlBaseParser.MultiUnitsIntervalContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#errorCapturingUnitToUnitInterval. + def visitErrorCapturingUnitToUnitInterval(self, ctx:SqlBaseParser.ErrorCapturingUnitToUnitIntervalContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unitToUnitInterval. + def visitUnitToUnitInterval(self, ctx:SqlBaseParser.UnitToUnitIntervalContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#intervalValue. + def visitIntervalValue(self, ctx:SqlBaseParser.IntervalValueContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unitInMultiUnits. + def visitUnitInMultiUnits(self, ctx:SqlBaseParser.UnitInMultiUnitsContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unitInUnitToUnit. + def visitUnitInUnitToUnit(self, ctx:SqlBaseParser.UnitInUnitToUnitContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#colPosition. + def visitColPosition(self, ctx:SqlBaseParser.ColPositionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#complexDataType. + def visitComplexDataType(self, ctx:SqlBaseParser.ComplexDataTypeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#yearMonthIntervalDataType. + def visitYearMonthIntervalDataType(self, ctx:SqlBaseParser.YearMonthIntervalDataTypeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#dayTimeIntervalDataType. + def visitDayTimeIntervalDataType(self, ctx:SqlBaseParser.DayTimeIntervalDataTypeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#primitiveDataType. + def visitPrimitiveDataType(self, ctx:SqlBaseParser.PrimitiveDataTypeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#qualifiedColTypeWithPositionList. + def visitQualifiedColTypeWithPositionList(self, ctx:SqlBaseParser.QualifiedColTypeWithPositionListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#qualifiedColTypeWithPosition. + def visitQualifiedColTypeWithPosition(self, ctx:SqlBaseParser.QualifiedColTypeWithPositionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#colDefinitionDescriptorWithPosition. + def visitColDefinitionDescriptorWithPosition(self, ctx:SqlBaseParser.ColDefinitionDescriptorWithPositionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#defaultExpression. + def visitDefaultExpression(self, ctx:SqlBaseParser.DefaultExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#colTypeList. + def visitColTypeList(self, ctx:SqlBaseParser.ColTypeListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#colType. + def visitColType(self, ctx:SqlBaseParser.ColTypeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#createOrReplaceTableColTypeList. + def visitCreateOrReplaceTableColTypeList(self, ctx:SqlBaseParser.CreateOrReplaceTableColTypeListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#createOrReplaceTableColType. + def visitCreateOrReplaceTableColType(self, ctx:SqlBaseParser.CreateOrReplaceTableColTypeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#colDefinitionOption. + def visitColDefinitionOption(self, ctx:SqlBaseParser.ColDefinitionOptionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#generationExpression. + def visitGenerationExpression(self, ctx:SqlBaseParser.GenerationExpressionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#complexColTypeList. + def visitComplexColTypeList(self, ctx:SqlBaseParser.ComplexColTypeListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#complexColType. + def visitComplexColType(self, ctx:SqlBaseParser.ComplexColTypeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#whenClause. + def visitWhenClause(self, ctx:SqlBaseParser.WhenClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#windowClause. + def visitWindowClause(self, ctx:SqlBaseParser.WindowClauseContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#namedWindow. + def visitNamedWindow(self, ctx:SqlBaseParser.NamedWindowContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#windowRef. + def visitWindowRef(self, ctx:SqlBaseParser.WindowRefContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#windowDef. + def visitWindowDef(self, ctx:SqlBaseParser.WindowDefContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#windowFrame. + def visitWindowFrame(self, ctx:SqlBaseParser.WindowFrameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#frameBound. + def visitFrameBound(self, ctx:SqlBaseParser.FrameBoundContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#qualifiedNameList. + def visitQualifiedNameList(self, ctx:SqlBaseParser.QualifiedNameListContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#functionName. + def visitFunctionName(self, ctx:SqlBaseParser.FunctionNameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#qualifiedName. + def visitQualifiedName(self, ctx:SqlBaseParser.QualifiedNameContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#errorCapturingIdentifier. + def visitErrorCapturingIdentifier(self, ctx:SqlBaseParser.ErrorCapturingIdentifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#errorIdent. + def visitErrorIdent(self, ctx:SqlBaseParser.ErrorIdentContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#realIdent. + def visitRealIdent(self, ctx:SqlBaseParser.RealIdentContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#identifier. + def visitIdentifier(self, ctx:SqlBaseParser.IdentifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#unquotedIdentifier. + def visitUnquotedIdentifier(self, ctx:SqlBaseParser.UnquotedIdentifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#quotedIdentifierAlternative. + def visitQuotedIdentifierAlternative(self, ctx:SqlBaseParser.QuotedIdentifierAlternativeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#quotedIdentifier. + def visitQuotedIdentifier(self, ctx:SqlBaseParser.QuotedIdentifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#backQuotedIdentifier. + def visitBackQuotedIdentifier(self, ctx:SqlBaseParser.BackQuotedIdentifierContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#exponentLiteral. + def visitExponentLiteral(self, ctx:SqlBaseParser.ExponentLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#decimalLiteral. + def visitDecimalLiteral(self, ctx:SqlBaseParser.DecimalLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#legacyDecimalLiteral. + def visitLegacyDecimalLiteral(self, ctx:SqlBaseParser.LegacyDecimalLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#integerLiteral. + def visitIntegerLiteral(self, ctx:SqlBaseParser.IntegerLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#bigIntLiteral. + def visitBigIntLiteral(self, ctx:SqlBaseParser.BigIntLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#smallIntLiteral. + def visitSmallIntLiteral(self, ctx:SqlBaseParser.SmallIntLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#tinyIntLiteral. + def visitTinyIntLiteral(self, ctx:SqlBaseParser.TinyIntLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#doubleLiteral. + def visitDoubleLiteral(self, ctx:SqlBaseParser.DoubleLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#floatLiteral. + def visitFloatLiteral(self, ctx:SqlBaseParser.FloatLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#bigDecimalLiteral. + def visitBigDecimalLiteral(self, ctx:SqlBaseParser.BigDecimalLiteralContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#alterColumnAction. + def visitAlterColumnAction(self, ctx:SqlBaseParser.AlterColumnActionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#stringLit. + def visitStringLit(self, ctx:SqlBaseParser.StringLitContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#comment. + def visitComment(self, ctx:SqlBaseParser.CommentContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#version. + def visitVersion(self, ctx:SqlBaseParser.VersionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#ansiNonReserved. + def visitAnsiNonReserved(self, ctx:SqlBaseParser.AnsiNonReservedContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#strictNonReserved. + def visitStrictNonReserved(self, ctx:SqlBaseParser.StrictNonReservedContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by SqlBaseParser#nonReserved. + def visitNonReserved(self, ctx:SqlBaseParser.NonReservedContext): + return self.visitChildren(ctx) + + + +del SqlBaseParser \ No newline at end of file diff --git a/datajunction-server/datajunction_server/sql/parsing/types.py b/datajunction-server/datajunction_server/sql/parsing/types.py new file mode 100644 index 000000000..9aa3e1413 --- /dev/null +++ b/datajunction-server/datajunction_server/sql/parsing/types.py @@ -0,0 +1,954 @@ +"""DJ Column Types + + Example: + >>> StructType( \ + NestedField("required_field", StringType(), False, "a required field"), \ + NestedField("optional_field", IntegerType(), True, "an optional field") \ + ) + StructType(NestedField(name=Name(name='required_field', quote_style='', namespace=None), \ +field_type=StringType(), is_optional=False, doc='a required field'), \ +NestedField(name=Name(name='optional_field', quote_style='', namespace=None), \ +field_type=IntegerType(), is_optional=True, doc='an optional field')) + +""" + +import re +from typing import TYPE_CHECKING, Any, ClassVar, Dict, Generator, Optional, Tuple, cast + +from pydantic import BaseModel, Extra +from pydantic.class_validators import AnyCallable + +from datajunction_server.enum import StrEnum + +if TYPE_CHECKING: + from datajunction_server.sql.parsing import ast + + +DECIMAL_REGEX = re.compile(r"(?i)decimal\((?P\d+),\s*(?P\d+)\)") +FIXED_PARSER = re.compile(r"(?i)fixed\((?P\d+)\)") +VARCHAR_PARSER = re.compile(r"(?i)varchar(\((?P\d+)\))?") + + +class Singleton: + """ + Singleton for types + """ + + _instance = None + + def __new__(cls, *args, **kwargs): + if not isinstance(cls._instance, cls): + cls._instance = super(Singleton, cls).__new__(cls) + return cls._instance + + +class ColumnType(BaseModel): + """ + Base type for all Column Types + """ + + _initialized = False + + class Config: + extra = Extra.allow + arbitrary_types_allowed = True + underscore_attrs_are_private = False + + def __init__( + self, + type_string: str, + repr_string: str = None, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + self._type_string = type_string + self._repr_string = repr_string if repr_string else self._type_string + self._initialized = True + + def __repr__(self): + return self._repr_string + + def __str__(self): + return self._type_string + + def __deepcopy__(self, memo): + return self + + @classmethod + def __get_validators__(cls) -> Generator[AnyCallable, None, None]: + """ + One or more validators may be yielded which will be called in the + order to validate the input, each validator will receive as an input + the value returned from the previous validator + """ + yield cls.validate + + @classmethod + def validate( + cls, + v: Any, + ) -> "ColumnType": + """ + Parses the column type + """ + from datajunction_server.sql.parsing.backends.antlr4 import parse_rule + + return cast(ColumnType, parse_rule(str(v), "dataType")) + + def __eq__(self, other: "ColumnType"): # type: ignore + """ + Equality is dependent on the string representation of the column type. + """ + return str(other) == str(self) + + def __hash__(self): + """ + Equality is dependent on the string representation of the column type. + """ + return hash(str(self)) + + def is_compatible(self, other: "ColumnType") -> bool: + """ + Returns whether the two types are compatible with each other by + checking their ancestors. + """ + if self == NullType() or other == NullType() or self == other: + return True # quick return + + def has_common_ancestor(type1, type2) -> bool: + """ + Helper function to check whether two column types have common ancestors, + other than the highest-level ancestor types like ColumnType itself. This + determines whether they're part of the same type group and are compatible + with each other when performing type compatibility checks. + """ + base_types = (ColumnType, Singleton, PrimitiveType) + if type1 in base_types or type2 in base_types: + return False + if type1 == type2: + return True + current_has = False + for ancestor in type1.__bases__: + for ancestor2 in type2.__bases__: + current_has = current_has or has_common_ancestor( + ancestor, + ancestor2, + ) + if current_has: + return current_has + return False + + return has_common_ancestor(self.__class__, other.__class__) + + +class PrimitiveType(ColumnType): + """Base class for all Column Primitive Types""" + + +class NumberType(PrimitiveType): + """Base class for all Column Number Types""" + + +class NullType(PrimitiveType, Singleton): + """A data type for NULL + + Example: + >>> NullType() + NullType() + """ + + def __init__(self): + super().__init__("NULL", "NullType()") + + +class FixedType(PrimitiveType): + """A fixed data type. + + Example: + >>> FixedType(8) + FixedType(length=8) + >>> FixedType(8)==FixedType(8) + True + """ + + _instances: Dict[int, "FixedType"] = {} + + def __new__(cls, length: int): + cls._instances[length] = cls._instances.get(length) or object.__new__(cls) + return cls._instances[length] + + def __init__(self, length: int): + if not self._initialized: + super().__init__(f"fixed({length})", f"FixedType(length={length})") + self._length = length + + @property + def length(self) -> int: # pragma: no cover + """ + The length of the fixed type + """ + return self._length + + +class LambdaType(ColumnType): + """ + Type representing a lambda function + """ + + +class DecimalType(NumberType): + """A fixed data type. + + Example: + >>> DecimalType(32, 3) + DecimalType(precision=32, scale=3) + >>> DecimalType(8, 3)==DecimalType(8, 3) + True + """ + + max_precision: ClassVar[int] = 38 + max_scale: ClassVar[int] = 38 + _instances: Dict[Tuple[int, int], "DecimalType"] = {} + + def __new__(cls, precision: int, scale: int): + key = ( + min(precision, DecimalType.max_precision), + min(scale, DecimalType.max_scale), + ) + cls._instances[key] = cls._instances.get(key) or object.__new__(cls) + return cls._instances[key] + + def __init__(self, precision: int, scale: int): + if not self._initialized: + super().__init__( + f"decimal({precision}, {scale})", + f"DecimalType(precision={precision}, scale={scale})", + ) + self._precision = min(precision, DecimalType.max_precision) + self._scale = min(scale, DecimalType.max_scale) + + @property + def precision(self) -> int: # pragma: no cover + """ + Decimal's precision + """ + return self._precision + + @property + def scale(self) -> int: # pragma: no cover + """ + Decimal's scale + """ + return self._scale + + +class NestedField(ColumnType): + """Represents a field of a struct, a map key, a map value, or a list element. + + This is where field IDs, names, docs, and nullability are tracked. + """ + + _instances: Dict[ + Tuple[bool, str, ColumnType, Optional[str]], + "NestedField", + ] = {} + + def __new__( + cls, + name: "ast.Name", + field_type: ColumnType, + is_optional: bool = True, + doc: Optional[str] = None, + ): + if isinstance(name, str): # pragma: no cover + from datajunction_server.sql.parsing.ast import Name + + name = Name(name) + + key = (is_optional, name.name, field_type, doc) + cls._instances[key] = cls._instances.get(key) or object.__new__(cls) + return cls._instances[key] + + def __init__( + self, + name: "ast.Name", + field_type: ColumnType, + is_optional: bool = True, + doc: Optional[str] = None, + ): + if not self._initialized: + if isinstance(name, str): # pragma: no cover + from datajunction_server.sql.parsing.ast import Name + + name = Name(name) + doc_string = "" if doc is None else f", doc={repr(doc)}" + super().__init__( + ( + f"{name} {field_type}" + f"{' NOT NULL' if not is_optional else ''}" + + ("" if doc is None else f" {doc}") + ), + f"NestedField(name={repr(name)}, " + f"field_type={repr(field_type)}, " + f"is_optional={is_optional}" + f"{doc_string})", + ) + self._is_optional = is_optional + self._name = name + self._type = field_type + self._doc = doc + + @property + def is_optional(self) -> bool: + """ + Whether the field is optional + """ + return self._is_optional # pragma: no cover + + @property + def is_required(self) -> bool: + """ + Whether the field is required + """ + return not self._is_optional # pragma: no cover + + @property + def name(self) -> "ast.Name": + """ + The name of the field + """ + return self._name + + @property + def doc(self) -> Optional[str]: + """ + The docstring of the field + """ + return self._doc # pragma: no cover + + @property + def type(self) -> ColumnType: + """ + The field's type + """ + return self._type + + def __reduce__(self): # pragma: no cover + """ + Custom method for pickling. + Returns a tuple of (callable, args) to recreate the object. + """ + return (self.__class__, (self._name, self._type, self.is_optional, self.doc)) + + +class StructType(ColumnType): + """A struct type + + Example: + >>> StructType( \ + NestedField("required_field", StringType(), False, "a required field"), \ + NestedField("optional_field", IntegerType(), True, "an optional field") \ + ) + StructType(NestedField(name=Name(name='required_field', quote_style='', namespace=None), \ +field_type=StringType(), is_optional=False, doc='a required field'), \ +NestedField(name=Name(name='optional_field', quote_style='', namespace=None), \ +field_type=IntegerType(), is_optional=True, doc='an optional field')) + """ + + _instances: Dict[Tuple[NestedField, ...], "StructType"] = {} + + def __new__(cls, *fields: NestedField): + cls._instances[fields] = cls._instances.get(fields) or object.__new__(cls) + return cls._instances[fields] + + def __init__(self, *fields: NestedField): + if not self._initialized: + super().__init__( + f"struct<{','.join(map(str, fields))}>", + f"StructType{repr(fields)}", + ) + self._fields = fields + + @property + def fields(self) -> Tuple[NestedField, ...]: + """ + Returns the struct's fields. + """ + return self._fields # pragma: no cover + + @property + def fields_mapping(self) -> Dict[str, NestedField]: + """ + Returns the struct's fields. + """ + return {field.name.name: field for field in self._fields} # pragma: no cover + + +class ListType(ColumnType): + """A list type + + Example: + >>> ListType(element_type=StringType()) + ListType(element_type=StringType()) + """ + + _instances: Dict[Tuple[bool, int, ColumnType], "ListType"] = {} + + def __new__( + cls, + element_type: ColumnType = None, + ): + key = (element_type,) + cls._instances[key] = cls._instances.get(key) or super().__new__(cls) # type: ignore + return cls._instances[key] # type: ignore + + def __reduce__(self): + """ + Custom method for pickling. + Returns a tuple of (callable, args) to recreate the object. + """ + return (self.__class__, (self._element_field._type,)) # pragma: no cover + + def __init__( + self, + element_type: ColumnType, + ): + if not self._initialized: + super().__init__( + f"array<{element_type}>", + f"ListType(element_type={repr(element_type)})", + ) + self._element_field = NestedField( + name="col", # type: ignore + field_type=element_type, + is_optional=False, # type: ignore + ) + + @property + def element(self) -> NestedField: + """ + Returns the list's element + """ + return self._element_field + + +class MapType(ColumnType): + """A map type""" + + _instances: Dict[Tuple[ColumnType, ColumnType], "MapType"] = {} + + def __new__( + cls, + key_type: ColumnType, + value_type: ColumnType, + ): + impl_key = (key_type, value_type) + cls._instances[impl_key] = cls._instances.get(impl_key) or object.__new__(cls) + return cls._instances[impl_key] + + def __init__( + self, + key_type: ColumnType, + value_type: ColumnType, + ): + if not self._initialized: + super().__init__( + f"map<{key_type}, {value_type}>", + ) + self._key_field = NestedField( + name="key", # type: ignore + field_type=key_type, + is_optional=False, # type: ignore + ) + self._value_field = NestedField( + name="value", # type: ignore + field_type=value_type, + is_optional=False, # type: ignore + ) + + @property + def key(self) -> NestedField: + """ + The map's key + """ + return self._key_field + + @property + def value(self) -> NestedField: + """ + The map's value + """ + return self._value_field + + def __reduce__(self): # pragma: no cover + """ + Custom method for pickling. + Returns a tuple of (callable, args) to recreate the object. + """ + return (self.__class__, (self._key_field, self._value_field)) + + +class BooleanType(PrimitiveType, Singleton): + """A boolean data type can be represented using an instance of this class. + + Example: + >>> column_foo = BooleanType() + >>> isinstance(column_foo, BooleanType) + True + """ + + def __init__(self): + super().__init__("boolean", "BooleanType()") + + +class IntegerBase(NumberType, Singleton): + """Base class for all integer types""" + + max: ClassVar[int] + min: ClassVar[int] + + def check_bounds(self, value: int) -> bool: + """ + Check whether a value fits within the Integer min and max + """ + return self.__class__.min < value < self.__class__.max + + +class IntegerType(IntegerBase): + """An Integer data type can be represented using an instance of this class. Integers are + 32-bit signed and can be promoted to Longs. + + Example: + >>> column_foo = IntegerType() + >>> isinstance(column_foo, IntegerType) + True + + Attributes: + max (int): The maximum allowed value for Integers, inherited from the + canonical Column implementation + in Java (returns `2147483647`) + min (int): The minimum allowed value for Integers, inherited from the + canonical Column implementation + in Java (returns `-2147483648`) + """ + + max: ClassVar[int] = 2147483647 + + min: ClassVar[int] = -2147483648 + + def __init__(self): + super().__init__("int", "IntegerType()") + + +class TinyIntType(IntegerBase): + """A TinyInt data type can be represented using an instance of this class. TinyInts are + 8-bit signed integers. + + Example: + >>> column_foo = TinyIntType() + >>> isinstance(column_foo, TinyIntType) + True + + Attributes: + max (int): The maximum allowed value for TinyInts (returns `127`). + min (int): The minimum allowed value for TinyInts (returns `-128`). + """ + + max: ClassVar[int] = 127 + + min: ClassVar[int] = -128 + + def __init__(self): + super().__init__("tinyint", "TinyIntType()") + + +class SmallIntType(IntegerBase): + """A SmallInt data type can be represented using an instance of this class. SmallInts are + 16-bit signed integers. + + Example: + >>> column_foo = SmallIntType() + >>> isinstance(column_foo, SmallIntType) + True + + Attributes: + max (int): The maximum allowed value for SmallInts (returns `32767`). + min (int): The minimum allowed value for SmallInts (returns `-32768`). + """ + + max: ClassVar[int] = 32767 + + min: ClassVar[int] = -32768 + + def __init__(self): + super().__init__("smallint", "SmallIntType()") + + +class BigIntType(IntegerBase): + """A Long data type can be represented using an instance of this class. Longs are + 64-bit signed integers. + + Example: + >>> column_foo = BigIntType() + >>> isinstance(column_foo, BigIntType) + True + + Attributes: + max (int): The maximum allowed value for Longs, inherited from the + canonical Column implementation + in Java. (returns `9223372036854775807`) + min (int): The minimum allowed value for Longs, inherited from the + canonical Column implementation + in Java (returns `-9223372036854775808`) + """ + + max: ClassVar[int] = 9223372036854775807 + + min: ClassVar[int] = -9223372036854775808 + + def __init__(self): + super().__init__("bigint", "BigIntType()") + + +class LongType(BigIntType): + """A Long data type can be represented using an instance of this class. Longs are + 64-bit signed integers. + + Example: + >>> column_foo = LongType() + >>> column_foo == LongType() + True + + Attributes: + max (int): The maximum allowed value for Longs, inherited from the + canonical Column implementation + in Java. (returns `9223372036854775807`) + min (int): The minimum allowed value for Longs, inherited from the + canonical Column implementation + in Java (returns `-9223372036854775808`) + """ + + def __new__(cls, *args, **kwargs): + self = super().__new__(BigIntType, *args, **kwargs) + super(BigIntType, self).__init__("long", "LongType()") + return self + + +class FloatingBase(NumberType, Singleton): + """Base class for all floating types""" + + +class FloatType(FloatingBase): + """A Float data type can be represented using an instance of this class. Floats are + 32-bit IEEE 754 floating points and can be promoted to Doubles. + + Example: + >>> column_foo = FloatType() + >>> isinstance(column_foo, FloatType) + True + """ + + def __init__(self): + super().__init__("float", "FloatType()") + + +class DoubleType(FloatingBase): + """A Double data type can be represented using an instance of this class. Doubles are + 64-bit IEEE 754 floating points. + + Example: + >>> column_foo = DoubleType() + >>> isinstance(column_foo, DoubleType) + True + """ + + def __init__(self): + super().__init__("double", "DoubleType()") + + +class DateTimeBase(PrimitiveType, Singleton): + """ + Base class for date and time types. + """ + + class Unit(StrEnum): + """ + Units used for date and time functions and intervals + """ + + dayofyear = "DAYOFYEAR" + year = "YEAR" + day = "DAY" + microsecond = "MICROSECOND" + month = "MONTH" + week = "WEEK" + minute = "MINUTE" + second = "SECOND" + quarter = "QUARTER" + hour = "HOUR" + millisecond = "MILLISECOND" + + +class DateType(DateTimeBase): + """A Date data type can be represented using an instance of this class. Dates are + calendar dates without a timezone or time. + + Example: + >>> column_foo = DateType() + >>> isinstance(column_foo, DateType) + True + """ + + def __init__(self): + super().__init__("date", "DateType()") + + +class TimeType(DateTimeBase): + """A Time data type can be represented using an instance of this class. Times + have microsecond precision and are a time of day without a date or timezone. + + Example: + >>> column_foo = TimeType() + >>> isinstance(column_foo, TimeType) + True + """ + + def __init__(self): + super().__init__("time", "TimeType()") + + +class TimestampType(DateTimeBase): + """A Timestamp data type can be represented using an instance of this class. Timestamps in + Column have microsecond precision and include a date and a time of day without a timezone. + + Example: + >>> column_foo = TimestampType() + >>> isinstance(column_foo, TimestampType) + True + """ + + def __init__(self): + super().__init__("timestamp", "TimestampType()") + + +class TimestamptzType(PrimitiveType, Singleton): + """A Timestamptz data type can be represented using an instance of this class. Timestamptzs in + Column are stored as UTC and include a date and a time of day with a timezone. + + Example: + >>> column_foo = TimestamptzType() + >>> isinstance(column_foo, TimestamptzType) + True + """ + + def __init__(self): + super().__init__("timestamptz", "TimestamptzType()") + + +class IntervalTypeBase(PrimitiveType): + """A base class for all interval types""" + + +class DayTimeIntervalType(IntervalTypeBase): + """A DayTimeIntervalType type. + + Example: + >>> DayTimeIntervalType()==DayTimeIntervalType("DAY", "SECOND") + True + """ + + _instances: Dict[Tuple[str, str], "DayTimeIntervalType"] = {} + + def __new__( + cls, + from_: DateTimeBase.Unit = DateTimeBase.Unit.day, + to_: Optional[DateTimeBase.Unit] = DateTimeBase.Unit.second, + ): + key = (from_.upper(), to_.upper() if to_ else None) + cls._instances[key] = cls._instances.get(key) or object.__new__(cls) # type: ignore + return cls._instances[key] # type: ignore + + def __init__( + self, + from_: DateTimeBase.Unit = DateTimeBase.Unit.day, + to_: Optional[DateTimeBase.Unit] = DateTimeBase.Unit.second, + ): + if not self._initialized: + from_ = from_.upper() # type: ignore + to_ = to_.upper() if to_ else None # type: ignore + to_str = f" TO {to_}" if to_ else "" + to_repr = f', to="{to_}"' if to_ else "" + super().__init__( + f"INTERVAL {from_}{to_str}", + f'DayTimeIntervalType(from="{from_}"{to_repr})', + ) + self._from = from_ + self._to = to_ + + @property + def from_(self) -> str: + return self._from # pragma: no cover + + @property + def to_( + self, + ) -> Optional[str]: + return self._to # pragma: no cover + + +class YearMonthIntervalType(IntervalTypeBase): + """A YearMonthIntervalType type. + + Example: + >>> YearMonthIntervalType()==YearMonthIntervalType("YEAR", "MONTH") + True + """ + + _instances: Dict[Tuple[str, str], "YearMonthIntervalType"] = {} + + def __new__( + cls, + from_: DateTimeBase.Unit = DateTimeBase.Unit.year, + to_: Optional[DateTimeBase.Unit] = DateTimeBase.Unit.month, + ): + key = (from_.upper(), to_.upper() if to_ else None) + cls._instances[key] = cls._instances.get(key) or object.__new__(cls) # type: ignore + return cls._instances[key] # type: ignore + + def __init__( + self, + from_: DateTimeBase.Unit = DateTimeBase.Unit.year, + to_: Optional[DateTimeBase.Unit] = DateTimeBase.Unit.month, + ): + if not self._initialized: + from_ = from_.upper() # type: ignore + to_ = to_.upper() if to_ else None # type: ignore + to_str = f" TO {to_}" if to_ else "" + to_repr = f', to="{to_}"' if to_ else "" + super().__init__( + f"INTERVAL {from_}{to_str}", + f'YearMonthIntervalType(from="{from_}"{to_repr})', + ) + self._from = from_ + self._to = to_ + + @property + def from_(self) -> str: + return self._from # pragma: no cover + + @property + def to_( + self, + ) -> Optional[str]: + return self._to # pragma: no cover + + +class StringBase(PrimitiveType, Singleton): + """Base class for all string types""" + + +class StringType(StringBase): + """A String data type can be represented using an instance of this class. Strings in + Column are arbitrary-length character sequences and are encoded with UTF-8. + + Example: + >>> column_foo = StringType() + >>> isinstance(column_foo, StringType) + True + """ + + def __init__(self): + super().__init__("string", "StringType()") + + +class VarcharType(StringBase): + """A VarcharType data type can be represented using an instance of this class. + Varchars in Column are arbitrary-length character sequences and are + encoded with UTF-8. + + Example: + >>> column_foo = VarcharType() + >>> isinstance(column_foo, VarcharType) + True + """ + + def __init__(self, length: Optional[int] = None): + super().__init__("varchar", "VarcharType()") + self._length = length + + def __str__(self): + return ( + f"{self._type_string}({self._length})" + if self._length + else self._type_string + ) + + +class UUIDType(PrimitiveType, Singleton): + """A UUID data type can be represented using an instance of this class. UUIDs in + Column are universally unique identifiers. + + Example: + >>> column_foo = UUIDType() + >>> isinstance(column_foo, UUIDType) + True + """ + + def __init__(self): + super().__init__("uuid", "UUIDType()") + + +class BinaryType(PrimitiveType, Singleton): + """A Binary data type can be represented using an instance of this class. Binarys in + Column are arbitrary-length byte arrays. + + Example: + >>> column_foo = BinaryType() + >>> isinstance(column_foo, BinaryType) + True + """ + + def __init__(self): + super().__init__("binary", "BinaryType()") + + +class WildcardType(PrimitiveType, Singleton): + """A Wildcard datatype. + + Example: + >>> column_foo = WildcardType() + >>> isinstance(column_foo, WildcardType) + True + """ + + def __init__(self): + super().__init__("wildcard", "WildcardType()") + + +# Define the primitive data types and their corresponding Python classes +PRIMITIVE_TYPES: Dict[str, PrimitiveType] = { + "bool": BooleanType(), + "boolean": BooleanType(), + "varchar": VarcharType(), + "int": IntegerType(), + "integer": IntegerType(), + "tinyint": TinyIntType(), + "smallint": SmallIntType(), + "bigint": BigIntType(), + "long": BigIntType(), + "float": FloatType(), + "double": DoubleType(), + "date": DateType(), + "time": TimeType(), + "timestamp": TimestampType(), + "timestamptz": TimestamptzType(), + "string": StringType(), + "str": StringType(), + "uuid": UUIDType(), + "byte": BinaryType(), + "binary": BinaryType(), + "none": NullType(), + "null": NullType(), + "wildcard": WildcardType(), +} diff --git a/datajunction-server/datajunction_server/superset.py b/datajunction-server/datajunction_server/superset.py new file mode 100644 index 000000000..2ebf5124d --- /dev/null +++ b/datajunction-server/datajunction_server/superset.py @@ -0,0 +1,137 @@ +""" +A DB engine spec for Superset. +""" + +import re +from datetime import timedelta +from typing import TYPE_CHECKING, Any, List, Optional, Set, TypedDict + +import requests +from sqlalchemy.engine.reflection import Inspector + +try: + from superset.db_engine_specs.base import BaseEngineSpec +except ImportError: # pragma: no cover + # we don't really need the base class, so we can just mock it if Apache Superset is + # not installed + BaseEngineSpec = object + +if TYPE_CHECKING: + from superset.models.core import Database + + +SELECT_STAR_MESSAGE = ( + "DJ does not support data preview, since the `metrics` table is a virtual table " + "representing the whole repository of metrics. An administrator should configure the " + "DJ database with the `disable_data_preview` attribute set to `true` in the `extra` " + "field." +) +GET_METRICS_TIMEOUT = timedelta(seconds=60) + + +class MetricType(TypedDict, total=False): + """ + Type for metrics return by `get_metrics`. + """ + + metric_name: str + expression: str + verbose_name: Optional[str] + metric_type: Optional[str] + description: Optional[str] + d3format: Optional[str] + warning_text: Optional[str] + extra: Optional[str] + + +class DJEngineSpec(BaseEngineSpec): + """ + Engine spec for the DataJunction metric repository + + See https://github.com/DataJunction/dj for more information. + """ + + engine = "dj" + engine_name = "DJ" + + sqlalchemy_uri_placeholder = "dj://host:port/database_id" + + _time_grain_expressions = { + None: "{col}", + "PT1S": "DATE_TRUNC('second', {col})", + "PT1M": "DATE_TRUNC('minute', {col})", + "PT1H": "DATE_TRUNC('hour', {col})", + "P1D": "DATE_TRUNC('day', {col})", + "P1W": "DATE_TRUNC('week', {col})", + "P1M": "DATE_TRUNC('month', {col})", + "P3M": "DATE_TRUNC('quarter', {col})", + "P1Y": "DATE_TRUNC('year', {col})", + } + + @classmethod + def select_star( + cls, + *args: Any, + **kwargs: Any, + ) -> str: + """ + Return a ``SELECT *`` query. + + Since DJ doesn't have tables per se, a ``SELECT *`` query doesn't make sense. + """ + message = SELECT_STAR_MESSAGE.replace("'", "''") + return f"SELECT '{message}' AS warning" + + @classmethod + def get_metrics( + cls, + database: "Database", + inspector: Inspector, + table_name: str, + schema: Optional[str], + ) -> List[MetricType]: + """ + Get all metrics from a given schema and table. + """ + with database.get_sqla_engine_with_context() as engine: + base_url = engine.connect().connection.base_url + + response = requests.get( + base_url / "metrics/", + timeout=GET_METRICS_TIMEOUT.total_seconds(), + ) + payload = response.json() + return [ + { + "metric_name": metric_name, + "expression": f'"{metric_name}"', + "description": "", + } + for metric_name in payload + ] + + @classmethod + def execute( + cls, + cursor: Any, + query: str, + **kwargs: Any, + ) -> None: + """ + Quote ``__timestamp`` and other identifiers starting with an underscore. + """ + query = re.sub(r" AS (_.*?)(\b|$)", r' AS "\1"', query) + + return super().execute(cursor, query, **kwargs) + + @classmethod + def get_view_names( + cls, + database: "Database", + inspector: Inspector, + schema: Optional[str], + ) -> Set[str]: + """ + Return all views. + """ + return set() diff --git a/datajunction-server/datajunction_server/transpilation.py b/datajunction-server/datajunction_server/transpilation.py new file mode 100644 index 000000000..b0b937449 --- /dev/null +++ b/datajunction-server/datajunction_server/transpilation.py @@ -0,0 +1,98 @@ +"""SQL transpilation plugins manager.""" + +import importlib +from abc import ABC, abstractmethod +from typing import Optional + +from datajunction_server.errors import DJPluginNotFoundException +from datajunction_server.models.engine import Dialect + + +class SQLTranspilationPlugin(ABC): + """ + SQL transpilation plugin base class. To add support for a new SQL transpilation library, + implement this class with a custom `transpile_sql` method that works for the library. The + base implementation here will handle checking that the package exists before loading and + using it, or skipping transpilation if the package cannot be loaded. + """ + + package_name: Optional[str] = None + + def __init__(self): + """ + Load the package + """ + self.check_package() + + def check_package(self): + """ + Check that the selected SQL transpilation package is installed and loads it. + """ + if self.package_name: # pragma: no cover + try: + importlib.import_module(self.package_name) + except ImportError as import_err: + raise DJPluginNotFoundException( + message=f"The SQL transpilation package is not installed: {self.package_name}", + ) from import_err + + @abstractmethod + def transpile_sql( + self, + query: str, + *, + input_dialect: Optional[Dialect] = None, + output_dialect: Optional[Dialect] = None, + ) -> str: + """Transpile a given SQL query using the specific library.""" + + +class SQLGlotTranspilationPlugin(SQLTranspilationPlugin): + """ + Implement sqlglot as a transpilation option + """ + + package_name: str = "sqlglot" + + def transpile_sql( + self, + query: str, + *, + input_dialect: Optional[Dialect] = None, + output_dialect: Optional[Dialect] = None, + ) -> str: + """ + Transpile a given SQL query using the specific library. + """ + import sqlglot + + # Check to make sure that the output dialect is supported by sqlglot before transpiling + if ( + input_dialect + and output_dialect + and output_dialect.name in dir(sqlglot.dialects.Dialects) + ): + value = sqlglot.transpile( + query, + read=str(input_dialect.name.lower()), # type: ignore + write=str(output_dialect.name.lower()), # type: ignore + pretty=True, + )[0] + return value + return query + + +def get_transpilation_plugin(package_name: str) -> SQLTranspilationPlugin: + """ + Retrieves the configured SQL transpilation plugin + """ + transpilation_plugins = [ + clazz + for clazz in SQLTranspilationPlugin.__subclasses__() + if clazz.package_name == package_name + ] + if not transpilation_plugins: + raise DJPluginNotFoundException( + message=f"No SQL transpilation plugin found for package `{package_name}`!", + ) + return transpilation_plugins[0]() # type: ignore diff --git a/datajunction-server/datajunction_server/typing.py b/datajunction-server/datajunction_server/typing.py new file mode 100644 index 000000000..f3aae7119 --- /dev/null +++ b/datajunction-server/datajunction_server/typing.py @@ -0,0 +1,331 @@ +""" +Custom types for annotations. +""" + +from __future__ import annotations + +import datetime +from types import ModuleType +from typing import Any, Iterator, List, Literal, Optional, Tuple, TypedDict, Union + +from pydantic.datetime_parse import parse_datetime +from typing_extensions import Protocol + +from datajunction_server.enum import StrEnum + + +class SQLADialect(Protocol): + """ + A SQLAlchemy dialect. + """ + + dbapi: ModuleType + + +# The ``type_code`` in a cursor description -- can really be anything +TypeCode = Any + + +# Cursor description +Description = Optional[ + List[ + Tuple[ + str, + TypeCode, + Optional[str], + Optional[str], + Optional[str], + Optional[str], + Optional[bool], + ] + ] +] + + +# A stream of data +Row = Tuple[Any, ...] +Stream = Iterator[Row] + + +class TypeEnum(StrEnum): + """ + PEP 249 basic types. + + Unfortunately SQLAlchemy doesn't seem to offer an API for determining the types of the + columns in a (SQL Core) query, and the DB API 2.0 cursor only offers very coarse + types. + """ + + STRING = "STRING" + BINARY = "BINARY" + NUMBER = "NUMBER" + TIMESTAMP = "TIMESTAMP" + UNKNOWN = "UNKNOWN" + + +class QueryState(StrEnum): + """ + Different states of a query. + """ + + UNKNOWN = "UNKNOWN" + ACCEPTED = "ACCEPTED" + SCHEDULED = "SCHEDULED" + RUNNING = "RUNNING" + FINISHED = "FINISHED" + CANCELED = "CANCELED" + FAILED = "FAILED" + + +END_JOB_STATES = [QueryState.FINISHED, QueryState.CANCELED, QueryState.FAILED] + +# sqloxide type hints +# Reference: https://github.com/sqlparser-rs/sqlparser-rs/blob/main/src/ast/query.rs + + +class Value(TypedDict, total=False): + Number: Tuple[str, bool] + SingleQuotedString: str + Boolean: bool + + +class Limit(TypedDict): + Value: Value + + +class Identifier(TypedDict): + quote_style: Optional[str] + value: str + + +class Bound(TypedDict, total=False): + Following: int + Preceding: int + + +class WindowFrame(TypedDict): + end_bound: Bound + start_bound: Bound + units: str + + +class Expression(TypedDict, total=False): + CompoundIdentifier: List["Identifier"] + Identifier: Identifier + Value: Value + Function: Function # type: ignore + UnaryOp: UnaryOp # type: ignore + BinaryOp: BinaryOp # type: ignore + Case: Case # type: ignore + + +class Case(TypedDict): + conditions: List[Expression] + else_result: Optional[Expression] + operand: Optional[Expression] + results: List[Expression] + + +class UnnamedArgument(TypedDict): + Expr: Expression + + +class Argument(TypedDict, total=False): + Unnamed: Union[UnnamedArgument, Wildcard] + + +class Over(TypedDict): + order_by: List[Expression] + partition_by: List[Expression] + window_frame: WindowFrame + + +class Function(TypedDict): + args: List[Argument] + distinct: bool + name: List[Identifier] + over: Optional[Over] + + +class ExpressionWithAlias(TypedDict): + alias: Identifier + expr: Expression + + +class Offset(TypedDict): + rows: str + value: Expression + + +class OrderBy(TypedDict, total=False): + asc: Optional[bool] + expr: Expression + nulls_first: Optional[bool] + + +class Projection(TypedDict, total=False): + ExprWithAlias: ExpressionWithAlias + UnnamedExpr: Expression + + +Wildcard = Literal["Wildcard"] + + +class Fetch(TypedDict): + percent: bool + quantity: Value + with_ties: bool + + +Top = Fetch + + +class UnaryOp(TypedDict): + op: str + expr: Expression + + +class BinaryOp(TypedDict): + left: Expression + op: str + right: Expression + + +class LateralView(TypedDict): + lateral_col_alias: List[Identifier] + lateral_view: Expression + lateral_view_name: List[Identifier] + outer: bool + + +class TableAlias(TypedDict): + columns: List[Identifier] + name: Identifier + + +class Table(TypedDict): + alias: Optional[TableAlias] + args: List[Argument] + name: List[Identifier] + with_hints: List[Expression] + + +class Derived(TypedDict): + lateral: bool + subquery: "Body" # type: ignore + alias: Optional[TableAlias] + + +class Relation(TypedDict, total=False): + Table: Table + Derived: Derived + + +class JoinConstraint(TypedDict): + On: Expression + Using: List[Identifier] + + +class JoinOperator(TypedDict, total=False): + Inner: JoinConstraint + LeftOuter: JoinConstraint + RightOuter: JoinConstraint + FullOuter: JoinConstraint + + +CrossJoin = Literal["CrossJoin"] +CrossApply = Literal["CrossApply"] +OuterApply = Literal["Outerapply"] + + +class Join(TypedDict): + join_operator: Union[JoinOperator, CrossJoin, CrossApply, OuterApply] + relation: Relation + + +class From(TypedDict): + joins: List[Join] + relation: Relation + + +Select = TypedDict( + "Select", + { + "cluster_by": List[Expression], + "distinct": bool, + "distribute_by": List[Expression], + "from": List[From], + "group_by": List[Expression], + "having": Optional[BinaryOp], + "lateral_views": List[LateralView], + "projection": List[Union[Projection, Wildcard]], + "selection": Optional[BinaryOp], + "sort_by": List[Expression], + "top": Optional[Top], + }, +) + + +class Body(TypedDict): + Select: Select + + +CTETable = TypedDict( + "CTETable", + { + "alias": TableAlias, + "from": Optional[Identifier], + "query": "Query", # type: ignore + }, +) + + +class With(TypedDict): + cte_tables: List[CTETable] + + +Query = TypedDict( + "Query", + { + "body": Body, + "fetch": Optional[Fetch], + "limit": Optional[Limit], + "lock": Optional[Literal["Share", "Update"]], + "offset": Optional[Offset], + "order_by": List[OrderBy], + "with": Optional[With], + }, +) + + +# We could support more than just ``SELECT`` here. +class Statement(TypedDict): + Query: Query + + +# A parse tree, result of ``sqloxide.parse_sql``. +ParseTree = List[Statement] # type: ignore + + +class UTCDatetime(datetime.datetime): + """ + A UTC extension of pydantic's normal datetime handling + """ + + @classmethod + def __get_validators__(cls): + """ + Extend the builtin pydantic datetime parser with a custom validate method + """ + yield parse_datetime + yield cls.validate + + @classmethod + def validate(cls, value) -> str: + """ + Convert to UTC + """ + if value.tzinfo is None: + return value.replace(tzinfo=datetime.timezone.utc) # pragma: no cover + + return value.astimezone(datetime.timezone.utc) diff --git a/datajunction-server/datajunction_server/utils.py b/datajunction-server/datajunction_server/utils.py new file mode 100644 index 000000000..8da8e42ef --- /dev/null +++ b/datajunction-server/datajunction_server/utils.py @@ -0,0 +1,319 @@ +""" +Utility functions. +""" + +import asyncio +import logging +import os +import re +from functools import lru_cache +from http import HTTPStatus + +from typing import AsyncIterator, List, Optional + +from dotenv import load_dotenv +from fastapi import Depends +from rich.logging import RichHandler +from sqlalchemy import AsyncAdaptedQueuePool +from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.exc import MissingGreenlet +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + async_scoped_session, + async_sessionmaker, + create_async_engine, +) +from starlette.requests import Request +from yarl import URL + +from datajunction_server.config import Settings +from datajunction_server.database.user import User +from datajunction_server.enum import StrEnum +from datajunction_server.errors import ( + DJAuthenticationException, + DJInternalErrorException, + DJInvalidInputException, + DJUninitializedResourceException, +) +from datajunction_server.service_clients import QueryServiceClient + + +def setup_logging(loglevel: str) -> None: + """ + Setup basic logging. + """ + level = getattr(logging, loglevel.upper(), None) + if not isinstance(level, int): + raise ValueError(f"Invalid log level: {loglevel}") + + logformat = "[%(asctime)s] %(levelname)s: %(name)s: %(message)s" + logging.basicConfig( + level=level, + format=logformat, + datefmt="[%X]", + handlers=[RichHandler(rich_tracebacks=True)], + force=True, + ) + + +@lru_cache +def get_settings() -> Settings: + """ + Return a cached settings object. + """ + dotenv_file = os.environ.get("DOTENV_FILE", ".env") + load_dotenv(dotenv_file) + return Settings() + + +class DatabaseSessionManager: + """ + DB session context manager + """ + + def __init__(self): + self.engine: AsyncEngine | None = None + self.session_maker = None + self.session = None + + def init_db(self): + """ + Initialize the database engine + """ + settings = get_settings() + self.engine = create_async_engine( + settings.index, + future=True, + echo=settings.db_echo, + pool_pre_ping=settings.db_pool_pre_ping, + pool_size=settings.db_pool_size, + max_overflow=settings.db_max_overflow, + pool_timeout=settings.db_pool_timeout, + poolclass=AsyncAdaptedQueuePool, + connect_args={ + "connect_timeout": settings.db_connect_timeout, + "keepalives": settings.db_keepalives, + "keepalives_idle": settings.db_keepalives_idle, + "keepalives_interval": settings.db_keepalives_interval, + "keepalives_count": settings.db_keepalives_count, + }, + ) + + async_session_factory = async_sessionmaker( + bind=self.engine, + autocommit=False, + expire_on_commit=False, # prevents attributes from being expired on commit + ) + # Create a scoped session + self.session = async_scoped_session( # pragma: no cover + async_session_factory, + scopefunc=asyncio.current_task, + ) + + async def close(self): + """ + Close database session + """ + if self.engine is None: # pragma: no cover + raise DJUninitializedResourceException( + "DatabaseSessionManager is not initialized", + ) + await self.engine.dispose() # pragma: no cover + + +@lru_cache(maxsize=None) +def get_session_manager() -> DatabaseSessionManager: + """ + Get session manager + """ + session_manager = DatabaseSessionManager() + session_manager.init_db() + return session_manager + + +@lru_cache(maxsize=None) +def get_engine() -> AsyncEngine: + """ + Create the metadata engine. + """ + settings = get_settings() + engine = create_async_engine( + settings.index, + future=True, + echo=settings.db_echo, + pool_pre_ping=settings.db_pool_pre_ping, + pool_size=settings.db_pool_size, + max_overflow=settings.db_max_overflow, + pool_timeout=settings.db_pool_timeout, + poolclass=AsyncAdaptedQueuePool, + connect_args={ + "connect_timeout": settings.db_connect_timeout, + }, + ) + return engine + + +async def get_session() -> AsyncIterator[AsyncSession]: + """ + Async database session. + """ + session_manager = get_session_manager() + session = session_manager.session() + try: + yield session + except Exception as exc: + await session.rollback() # pragma: no cover + raise exc # pragma: no cover + finally: + await session.close() + + +async def refresh_if_needed(session: AsyncSession, obj, attributes: list[str]): + """ + Conditionally refresh a list of attributes for a SQLAlchemy ORM object. + """ + attributes_to_refresh = [] + + for attr_name in attributes: + try: + getattr(obj, attr_name) + except MissingGreenlet: + attributes_to_refresh.append(attr_name) + + if attributes_to_refresh: + await session.refresh(obj, attributes_to_refresh) + + +def get_query_service_client( + request: Request = None, +) -> Optional[QueryServiceClient]: + """ + Return query service client + """ + settings = get_settings() + if not settings.query_service: # pragma: no cover + return None + return QueryServiceClient(settings.query_service) + + +def get_issue_url( + baseurl: URL = URL("https://github.com/DataJunction/dj/issues/new"), + title: Optional[str] = None, + body: Optional[str] = None, + labels: Optional[List[str]] = None, +) -> URL: + """ + Return the URL to file an issue on GitHub. + + https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-an-issue#creating-an-issue-from-a-url-query + """ + query_arguments = { + "title": title, + "body": body, + "labels": ",".join(label.strip() for label in labels) if labels else None, + } + query_arguments = {k: v for k, v in query_arguments.items() if v is not None} + + return baseurl % query_arguments + + +class VersionUpgrade(StrEnum): + """ + The version upgrade type + """ + + MAJOR = "major" + MINOR = "minor" + + +class Version: + """ + Represents a basic semantic version with only major & minor parts. + Used for tracking node versioning. + """ + + def __init__(self, major, minor): + self.major = major + self.minor = minor + + def __str__(self) -> str: + return f"v{self.major}.{self.minor}" + + @classmethod + def parse(cls, version_string) -> "Version": + """ + Parse a version string. + """ + version_regex = re.compile(r"^v(?P[0-9]+)\.(?P[0-9]+)") + matcher = version_regex.search(version_string) + if not matcher: + raise DJInternalErrorException(f"Unparseable version {version_string}!") + results = matcher.groupdict() + return Version(int(results["major"]), int(results["minor"])) + + def next_minor_version(self) -> "Version": + """ + Returns the next minor version + """ + return Version(self.major, self.minor + 1) + + def next_major_version(self) -> "Version": + """ + Returns the next major version + """ + return Version(self.major + 1, 0) + + +def get_namespace_from_name(name: str) -> str: + """ + Splits a qualified node name into it's namespace and name parts + """ + if "." in name: + node_namespace, _ = name.rsplit(".", 1) + else: # pragma: no cover + raise DJInvalidInputException(f"No namespace provided: {name}") + return node_namespace + + +async def get_current_user(request: Request) -> "User": + """ + Returns the current authenticated user + """ + if not hasattr(request.state, "user"): # pragma: no cover + raise DJAuthenticationException( + message="Unauthorized, request state has no user", + http_status_code=HTTPStatus.UNAUTHORIZED, + ) + return request.state.user + + +async def get_and_update_current_user( + session: AsyncSession = Depends(get_session), + current_user: "User" = Depends(get_current_user), +) -> "User": + """ + Wrapper for the get_current_user dependency that creates a DJ user object if required + """ + statement = insert(User).values( + username=current_user.username, + email=current_user.email, + name=current_user.name, + oauth_provider=current_user.oauth_provider, + ) + update_dict = { + "email": current_user.email, + "name": current_user.name, + "oauth_provider": current_user.oauth_provider, + } + statement = statement.on_conflict_do_update( + index_elements=["username"], + set_=update_dict, + ) + await session.execute(statement) + await session.commit() + refreshed_user = await User.get_by_username(session, current_user.username) + return refreshed_user # type: ignore + + +SEPARATOR = "." diff --git a/datajunction-server/init_data/seed.sql b/datajunction-server/init_data/seed.sql new file mode 100644 index 000000000..5d1c93633 --- /dev/null +++ b/datajunction-server/init_data/seed.sql @@ -0,0 +1,5 @@ +INSERT INTO users (username, password, oauth_provider, is_admin) +VALUES + ('dj', '$2b$12$K5oXl1Qs/UiNzvysOckn2uJjJmGHrhnk97hFRlMboP4NbvNbtoQ4a', 'BASIC', false) +ON CONFLICT (username) +DO NOTHING; diff --git a/datajunction-server/pdm.lock b/datajunction-server/pdm.lock new file mode 100644 index 000000000..ac6cf5647 --- /dev/null +++ b/datajunction-server/pdm.lock @@ -0,0 +1,3237 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "test", "transpilation", "uvicorn"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:f22ea1f50b47cc29b1754de6666c3a3a24ecb8ecb02df2491a5ab21d1c7cc378" + +[[metadata.targets]] +requires_python = "~=3.10" + +[[package]] +name = "alembic" +version = "1.14.1" +requires_python = ">=3.8" +summary = "A database migration tool for SQLAlchemy." +groups = ["default"] +dependencies = [ + "Mako", + "SQLAlchemy>=1.3.0", + "importlib-metadata; python_version < \"3.9\"", + "importlib-resources; python_version < \"3.9\"", + "typing-extensions>=4", +] +files = [ + {file = "alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5"}, + {file = "alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213"}, +] + +[[package]] +name = "amqp" +version = "5.3.1" +requires_python = ">=3.6" +summary = "Low-level AMQP client for Python (fork of amqplib)." +groups = ["default"] +dependencies = [ + "vine<6.0.0,>=5.0.0", +] +files = [ + {file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"}, + {file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"}, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.13.1" +summary = "ANTLR 4.13.1 runtime for Python 3" +groups = ["default"] +dependencies = [ + "typing; python_version < \"3.5\"", +] +files = [ + {file = "antlr4-python3-runtime-4.13.1.tar.gz", hash = "sha256:3cd282f5ea7cfb841537fe01f143350fdb1c0b1ce7981443a2fa8513fddb6d1a"}, + {file = "antlr4_python3_runtime-4.13.1-py3-none-any.whl", hash = "sha256:78ec57aad12c97ac039ca27403ad61cb98aaec8a3f9bb8144f889aa0fa28b943"}, +] + +[[package]] +name = "anyio" +version = "4.8.0" +requires_python = ">=3.9" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +groups = ["default", "test", "uvicorn"] +dependencies = [ + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions>=4.5; python_version < \"3.13\"", +] +files = [ + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, +] + +[[package]] +name = "asgiref" +version = "3.8.1" +requires_python = ">=3.8" +summary = "ASGI specs, helper code, and adapters" +groups = ["default"] +dependencies = [ + "typing-extensions>=4; python_version < \"3.11\"", +] +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[[package]] +name = "astroid" +version = "3.3.8" +requires_python = ">=3.9.0" +summary = "An abstract syntax tree for Python with inference support." +groups = ["test"] +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.11\"", +] +files = [ + {file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"}, + {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +requires_python = ">=3.8" +summary = "Timeout context manager for asyncio programs" +groups = ["default"] +marker = "python_full_version <= \"3.11.2\"" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "25.1.0" +requires_python = ">=3.8" +summary = "Classes Without Boilerplate" +groups = ["default"] +files = [ + {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, + {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, +] + +[[package]] +name = "bcrypt" +version = "4.2.1" +requires_python = ">=3.7" +summary = "Modern password hashing for your software and your servers" +groups = ["default"] +files = [ + {file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139"}, + {file = "bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005"}, + {file = "bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526"}, + {file = "bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d"}, + {file = "bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf"}, + {file = "bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c"}, + {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e"}, + {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f"}, + {file = "bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe"}, +] + +[[package]] +name = "billiard" +version = "4.2.1" +requires_python = ">=3.7" +summary = "Python multiprocessing fork with improvements and bugfixes" +groups = ["default"] +files = [ + {file = "billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"}, + {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, +] + +[[package]] +name = "cachelib" +version = "0.13.0" +requires_python = ">=3.8" +summary = "A collection of cache libraries in the same API interface." +groups = ["default"] +files = [ + {file = "cachelib-0.13.0-py3-none-any.whl", hash = "sha256:8c8019e53b6302967d4e8329a504acf75e7bc46130291d30188a6e4e58162516"}, + {file = "cachelib-0.13.0.tar.gz", hash = "sha256:209d8996e3c57595bee274ff97116d1d73c4980b2fd9a34c7846cd07fd2e1a48"}, +] + +[[package]] +name = "cachetools" +version = "5.5.1" +requires_python = ">=3.7" +summary = "Extensible memoizing collections and decorators" +groups = ["default"] +files = [ + {file = "cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb"}, + {file = "cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95"}, +] + +[[package]] +name = "celery" +version = "5.4.0" +requires_python = ">=3.8" +summary = "Distributed Task Queue." +groups = ["default"] +dependencies = [ + "backports-zoneinfo>=0.2.1; python_version < \"3.9\"", + "billiard<5.0,>=4.2.0", + "click-didyoumean>=0.3.0", + "click-plugins>=1.1.1", + "click-repl>=0.2.0", + "click<9.0,>=8.1.2", + "importlib-metadata>=3.6; python_version < \"3.8\"", + "kombu<6.0,>=5.3.4", + "python-dateutil>=2.8.2", + "tzdata>=2022.7", + "vine<6.0,>=5.1.0", +] +files = [ + {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, + {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, +] + +[[package]] +name = "certifi" +version = "2024.12.14" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default", "test"] +files = [ + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +groups = ["default", "test"] +marker = "platform_python_implementation != \"PyPy\"" +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +requires_python = ">=3.8" +summary = "Validate configuration and produce human readable error messages." +groups = ["test"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +requires_python = ">=3.7" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["default", "test"] +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + +[[package]] +name = "click" +version = "8.1.8" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["default", "uvicorn"] +dependencies = [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +requires_python = ">=3.6.2" +summary = "Enables git-like *did-you-mean* feature in click" +groups = ["default"] +dependencies = [ + "click>=7", +] +files = [ + {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, + {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, +] + +[[package]] +name = "click-plugins" +version = "1.1.1" +summary = "An extension module for click to enable registering CLI commands via setuptools entry-points." +groups = ["default"] +dependencies = [ + "click>=4.0", +] +files = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +requires_python = ">=3.6" +summary = "REPL plugin for Click" +groups = ["default"] +dependencies = [ + "click>=7.0", + "prompt-toolkit>=3.0.36", +] +files = [ + {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, + {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, +] + +[[package]] +name = "codespell" +version = "2.4.0" +requires_python = ">=3.8" +summary = "Fix common misspellings in text files" +groups = ["test"] +files = [ + {file = "codespell-2.4.0-py3-none-any.whl", hash = "sha256:b4c5b779f747dd481587aeecb5773301183f52b94b96ed51a28126d0482eec1d"}, + {file = "codespell-2.4.0.tar.gz", hash = "sha256:587d45b14707fb8ce51339ba4cce50ae0e98ce228ef61f3c5e160e34f681be58"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["default", "test", "uvicorn"] +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.10" +requires_python = ">=3.9" +summary = "Code coverage measurement for Python" +groups = ["test"] +files = [ + {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, +] + +[[package]] +name = "coverage" +version = "7.6.10" +extras = ["toml"] +requires_python = ">=3.9" +summary = "Code coverage measurement for Python" +groups = ["test"] +dependencies = [ + "coverage==7.6.10", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, +] + +[[package]] +name = "cryptography" +version = "44.0.0" +requires_python = "!=3.9.0,!=3.9.1,>=3.7" +summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +groups = ["default"] +dependencies = [ + "cffi>=1.12; platform_python_implementation != \"PyPy\"", +] +files = [ + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, +] + +[[package]] +name = "deprecated" +version = "1.2.17" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +summary = "Python @deprecated decorator to deprecate old python classes, functions or methods." +groups = ["default"] +dependencies = [ + "wrapt<2,>=1.10", +] +files = [ + {file = "Deprecated-1.2.17-py2.py3-none-any.whl", hash = "sha256:69cdc0a751671183f569495e2efb14baee4344b0236342eec29f1fde25d61818"}, + {file = "deprecated-1.2.17.tar.gz", hash = "sha256:0114a10f0bbb750b90b2c2296c90cf7e9eaeb0abb5cf06c80de2c60138de0a82"}, +] + +[[package]] +name = "dill" +version = "0.3.9" +requires_python = ">=3.8" +summary = "serialize all of Python" +groups = ["test"] +files = [ + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, +] + +[[package]] +name = "distlib" +version = "0.3.9" +summary = "Distribution utilities" +groups = ["test"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "docker" +version = "7.1.0" +requires_python = ">=3.8" +summary = "A Python library for the Docker Engine API." +groups = ["test"] +dependencies = [ + "pywin32>=304; sys_platform == \"win32\"", + "requests>=2.26.0", + "urllib3>=1.26.0", +] +files = [ + {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, + {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, +] + +[[package]] +name = "duckdb" +version = "0.8.1" +summary = "DuckDB embedded database" +groups = ["test"] +files = [ + {file = "duckdb-0.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:14781d21580ee72aba1f5dcae7734674c9b6c078dd60470a08b2b420d15b996d"}, + {file = "duckdb-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f13bf7ab0e56ddd2014ef762ae4ee5ea4df5a69545ce1191b8d7df8118ba3167"}, + {file = "duckdb-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4032042d8363e55365bbca3faafc6dc336ed2aad088f10ae1a534ebc5bcc181"}, + {file = "duckdb-0.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a71bd8f0b0ca77c27fa89b99349ef22599ffefe1e7684ae2e1aa2904a08684"}, + {file = "duckdb-0.8.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24568d6e48f3dbbf4a933109e323507a46b9399ed24c5d4388c4987ddc694fd0"}, + {file = "duckdb-0.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297226c0dadaa07f7c5ae7cbdb9adba9567db7b16693dbd1b406b739ce0d7924"}, + {file = "duckdb-0.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5792cf777ece2c0591194006b4d3e531f720186102492872cb32ddb9363919cf"}, + {file = "duckdb-0.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:12803f9f41582b68921d6b21f95ba7a51e1d8f36832b7d8006186f58c3d1b344"}, + {file = "duckdb-0.8.1-cp310-cp310-win32.whl", hash = "sha256:d0953d5a2355ddc49095e7aef1392b7f59c5be5cec8cdc98b9d9dc1f01e7ce2b"}, + {file = "duckdb-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e6583c98a7d6637e83bcadfbd86e1f183917ea539f23b6b41178f32f813a5eb"}, + {file = "duckdb-0.8.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fad7ed0d4415f633d955ac24717fa13a500012b600751d4edb050b75fb940c25"}, + {file = "duckdb-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81ae602f34d38d9c48dd60f94b89f28df3ef346830978441b83c5b4eae131d08"}, + {file = "duckdb-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d75cfe563aaa058d3b4ccaaa371c6271e00e3070df5de72361fd161b2fe6780"}, + {file = "duckdb-0.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbb55e7a3336f2462e5e916fc128c47fe1c03b6208d6bd413ac11ed95132aa0"}, + {file = "duckdb-0.8.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6df53efd63b6fdf04657385a791a4e3c4fb94bfd5db181c4843e2c46b04fef5"}, + {file = "duckdb-0.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b188b80b70d1159b17c9baaf541c1799c1ce8b2af4add179a9eed8e2616be96"}, + {file = "duckdb-0.8.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ad481ee353f31250b45d64b4a104e53b21415577943aa8f84d0af266dc9af85"}, + {file = "duckdb-0.8.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1d1b1729993611b1892509d21c21628917625cdbe824a61ce891baadf684b32"}, + {file = "duckdb-0.8.1-cp311-cp311-win32.whl", hash = "sha256:2d8f9cc301e8455a4f89aa1088b8a2d628f0c1f158d4cf9bc78971ed88d82eea"}, + {file = "duckdb-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:07457a43605223f62d93d2a5a66b3f97731f79bbbe81fdd5b79954306122f612"}, + {file = "duckdb-0.8.1.tar.gz", hash = "sha256:a54d37f4abc2afc4f92314aaa56ecf215a411f40af4bffe1e86bd25e62aceee9"}, +] + +[[package]] +name = "ecdsa" +version = "0.19.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6" +summary = "ECDSA cryptographic signature library (pure python)" +groups = ["default"] +dependencies = [ + "six>=1.9.0", +] +files = [ + {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, + {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["default", "test", "uvicorn"] +marker = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[[package]] +name = "execnet" +version = "2.1.1" +requires_python = ">=3.8" +summary = "execnet: rapid multi-Python deployment" +groups = ["test"] +files = [ + {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, + {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, +] + +[[package]] +name = "fastapi" +version = "0.115.7" +requires_python = ">=3.8" +summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +groups = ["default"] +dependencies = [ + "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", + "starlette<0.46.0,>=0.40.0", + "typing-extensions>=4.8.0", +] +files = [ + {file = "fastapi-0.115.7-py3-none-any.whl", hash = "sha256:eb6a8c8bf7f26009e8147111ff15b5177a0e19bb4a45bc3486ab14804539d21e"}, + {file = "fastapi-0.115.7.tar.gz", hash = "sha256:0f106da6c01d88a6786b3248fb4d7a940d071f6f488488898ad5d354b25ed015"}, +] + +[[package]] +name = "fastapi-cache2" +version = "0.2.2" +requires_python = "<4.0,>=3.8" +summary = "Cache for FastAPI" +groups = ["default"] +dependencies = [ + "fastapi", + "importlib-metadata<7.0.0,>=6.6.0; python_version < \"3.8\"", + "pendulum<4.0.0,>=3.0.0", + "typing-extensions>=4.1.0", + "uvicorn", +] +files = [ + {file = "fastapi_cache2-0.2.2-py3-none-any.whl", hash = "sha256:e1fae86d8eaaa6c8501dfe08407f71d69e87cc6748042d59d51994000532846c"}, + {file = "fastapi_cache2-0.2.2.tar.gz", hash = "sha256:71bf4450117dc24224ec120be489dbe09e331143c9f74e75eb6f576b78926026"}, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.1" +summary = "Fastest Python implementation of JSON schema" +groups = ["default"] +files = [ + {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, + {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, +] + +[[package]] +name = "filelock" +version = "3.17.0" +requires_python = ">=3.9" +summary = "A platform independent file lock." +groups = ["test"] +files = [ + {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, + {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, +] + +[[package]] +name = "freezegun" +version = "1.5.1" +requires_python = ">=3.7" +summary = "Let your Python tests travel through time" +groups = ["test"] +dependencies = [ + "python-dateutil>=2.7", +] +files = [ + {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, + {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, +] + +[[package]] +name = "gevent" +version = "24.11.1" +requires_python = ">=3.9" +summary = "Coroutine-based network library" +groups = ["test"] +dependencies = [ + "cffi>=1.17.1; platform_python_implementation == \"CPython\" and sys_platform == \"win32\"", + "greenlet>=3.1.1; platform_python_implementation == \"CPython\"", + "zope-event", + "zope-interface", +] +files = [ + {file = "gevent-24.11.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:92fe5dfee4e671c74ffaa431fd7ffd0ebb4b339363d24d0d944de532409b935e"}, + {file = "gevent-24.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7bfcfe08d038e1fa6de458891bca65c1ada6d145474274285822896a858c870"}, + {file = "gevent-24.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7398c629d43b1b6fd785db8ebd46c0a353880a6fab03d1cf9b6788e7240ee32e"}, + {file = "gevent-24.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7886b63ebfb865178ab28784accd32f287d5349b3ed71094c86e4d3ca738af5"}, + {file = "gevent-24.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9ca80711e6553880974898d99357fb649e062f9058418a92120ca06c18c3c59"}, + {file = "gevent-24.11.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e24181d172f50097ac8fc272c8c5b030149b630df02d1c639ee9f878a470ba2b"}, + {file = "gevent-24.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1d4fadc319b13ef0a3c44d2792f7918cf1bca27cacd4d41431c22e6b46668026"}, + {file = "gevent-24.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d882faa24f347f761f934786dde6c73aa6c9187ee710189f12dcc3a63ed4a50"}, + {file = "gevent-24.11.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:351d1c0e4ef2b618ace74c91b9b28b3eaa0dd45141878a964e03c7873af09f62"}, + {file = "gevent-24.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5efe72e99b7243e222ba0c2c2ce9618d7d36644c166d63373af239da1036bab"}, + {file = "gevent-24.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d3b249e4e1f40c598ab8393fc01ae6a3b4d51fc1adae56d9ba5b315f6b2d758"}, + {file = "gevent-24.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81d918e952954675f93fb39001da02113ec4d5f4921bf5a0cc29719af6824e5d"}, + {file = "gevent-24.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c935b83d40c748b6421625465b7308d87c7b3717275acd587eef2bd1c39546"}, + {file = "gevent-24.11.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff96c5739834c9a594db0e12bf59cb3fa0e5102fc7b893972118a3166733d61c"}, + {file = "gevent-24.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d6c0a065e31ef04658f799215dddae8752d636de2bed61365c358f9c91e7af61"}, + {file = "gevent-24.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:97e2f3999a5c0656f42065d02939d64fffaf55861f7d62b0107a08f52c984897"}, + {file = "gevent-24.11.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:a3d75fa387b69c751a3d7c5c3ce7092a171555126e136c1d21ecd8b50c7a6e46"}, + {file = "gevent-24.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beede1d1cff0c6fafae3ab58a0c470d7526196ef4cd6cc18e7769f207f2ea4eb"}, + {file = "gevent-24.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85329d556aaedced90a993226d7d1186a539c843100d393f2349b28c55131c85"}, + {file = "gevent-24.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:816b3883fa6842c1cf9d2786722014a0fd31b6312cca1f749890b9803000bad6"}, + {file = "gevent-24.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b24d800328c39456534e3bc3e1684a28747729082684634789c2f5a8febe7671"}, + {file = "gevent-24.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5f1701ce0f7832f333dd2faf624484cbac99e60656bfbb72504decd42970f0f"}, + {file = "gevent-24.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d740206e69dfdfdcd34510c20adcb9777ce2cc18973b3441ab9767cd8948ca8a"}, + {file = "gevent-24.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:68bee86b6e1c041a187347ef84cf03a792f0b6c7238378bf6ba4118af11feaae"}, + {file = "gevent-24.11.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d618e118fdb7af1d6c1a96597a5cd6ac84a9f3732b5be8515c6a66e098d498b6"}, + {file = "gevent-24.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2142704c2adce9cd92f6600f371afb2860a446bfd0be5bd86cca5b3e12130766"}, + {file = "gevent-24.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92e0d7759de2450a501effd99374256b26359e801b2d8bf3eedd3751973e87f5"}, + {file = "gevent-24.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca845138965c8c56d1550499d6b923eb1a2331acfa9e13b817ad8305dde83d11"}, + {file = "gevent-24.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:356b73d52a227d3313f8f828025b665deada57a43d02b1cf54e5d39028dbcf8d"}, + {file = "gevent-24.11.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:58851f23c4bdb70390f10fc020c973ffcf409eb1664086792c8b1e20f25eef43"}, + {file = "gevent-24.11.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1ea50009ecb7f1327347c37e9eb6561bdbc7de290769ee1404107b9a9cba7cf1"}, + {file = "gevent-24.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:ec68e270543ecd532c4c1d70fca020f90aa5486ad49c4f3b8b2e64a66f5c9274"}, + {file = "gevent-24.11.1-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:f43f47e702d0c8e1b8b997c00f1601486f9f976f84ab704f8f11536e3fa144c9"}, + {file = "gevent-24.11.1.tar.gz", hash = "sha256:8bd1419114e9e4a3ed33a5bad766afff9a3cf765cb440a582a1b3a9bc80c1aca"}, +] + +[[package]] +name = "google-api-core" +version = "2.24.1rc1" +requires_python = ">=3.7" +summary = "Google API client core library" +groups = ["default"] +dependencies = [ + "google-auth<3.0.dev0,>=2.14.1", + "googleapis-common-protos<2.0.dev0,>=1.56.2", + "proto-plus<2.0.0dev,>=1.22.3", + "proto-plus<2.0.0dev,>=1.25.0; python_version >= \"3.13\"", + "protobuf!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.19.5", + "requests<3.0.0.dev0,>=2.18.0", +] +files = [ + {file = "google_api_core-2.24.1rc1-py3-none-any.whl", hash = "sha256:92ee3eed90a397a9f4dd13c034a36cbe7dba2a58e01e5668619847b68a527b73"}, + {file = "google_api_core-2.24.1rc1.tar.gz", hash = "sha256:d1cf8265c8b0b171a87d84adc8709a5e48147ca529d6f96d6a2be613a195eb78"}, +] + +[[package]] +name = "google-api-python-client" +version = "2.159.0" +requires_python = ">=3.7" +summary = "Google API Client Library for Python" +groups = ["default"] +dependencies = [ + "google-api-core!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0.dev0,>=1.31.5", + "google-auth!=2.24.0,!=2.25.0,<3.0.0.dev0,>=1.32.0", + "google-auth-httplib2<1.0.0,>=0.2.0", + "httplib2<1.dev0,>=0.19.0", + "uritemplate<5,>=3.0.1", +] +files = [ + {file = "google_api_python_client-2.159.0-py2.py3-none-any.whl", hash = "sha256:baef0bb631a60a0bd7c0bf12a5499e3a40cd4388484de7ee55c1950bf820a0cf"}, + {file = "google_api_python_client-2.159.0.tar.gz", hash = "sha256:55197f430f25c907394b44fa078545ffef89d33fd4dca501b7db9f0d8e224bd6"}, +] + +[[package]] +name = "google-auth" +version = "2.38.0" +requires_python = ">=3.7" +summary = "Google Authentication Library" +groups = ["default"] +dependencies = [ + "cachetools<6.0,>=2.0.0", + "pyasn1-modules>=0.2.1", + "rsa<5,>=3.1.4", +] +files = [ + {file = "google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a"}, + {file = "google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4"}, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.2.0" +summary = "Google Authentication Library: httplib2 transport" +groups = ["default"] +dependencies = [ + "google-auth", + "httplib2>=0.19.0", +] +files = [ + {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, + {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, +] + +[[package]] +name = "google-auth-oauthlib" +version = "1.2.1" +requires_python = ">=3.6" +summary = "Google Authentication Library" +groups = ["default"] +dependencies = [ + "google-auth>=2.15.0", + "requests-oauthlib>=0.7.0", +] +files = [ + {file = "google_auth_oauthlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f"}, + {file = "google_auth_oauthlib-1.2.1.tar.gz", hash = "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263"}, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.66.0" +requires_python = ">=3.7" +summary = "Common protobufs used in Google APIs" +groups = ["default"] +dependencies = [ + "protobuf!=3.20.0,!=3.20.1,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.20.2", +] +files = [ + {file = "googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"}, + {file = "googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c"}, +] + +[[package]] +name = "graphql-core" +version = "3.2.5" +requires_python = "<4,>=3.6" +summary = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." +groups = ["default"] +dependencies = [ + "typing-extensions<5,>=4; python_version < \"3.10\"", +] +files = [ + {file = "graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a"}, + {file = "graphql_core-3.2.5.tar.gz", hash = "sha256:e671b90ed653c808715645e3998b7ab67d382d55467b7e2978549111bbabf8d5"}, +] + +[[package]] +name = "greenlet" +version = "3.1.1" +requires_python = ">=3.7" +summary = "Lightweight in-process concurrent programming" +groups = ["default", "test"] +files = [ + {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, + {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, + {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, + {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, + {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, + {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, + {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, + {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, + {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["default", "test", "uvicorn"] +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +requires_python = ">=3.8" +summary = "A minimal low-level HTTP client." +groups = ["test"] +dependencies = [ + "certifi", + "h11<0.15,>=0.13", +] +files = [ + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, +] + +[[package]] +name = "httplib2" +version = "0.22.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "A comprehensive HTTP client library." +groups = ["default"] +dependencies = [ + "pyparsing!=3.0.0,!=3.0.1,!=3.0.2,!=3.0.3,<4,>=2.4.2; python_version > \"3.0\"", + "pyparsing<3,>=2.4.2; python_version < \"3.0\"", +] +files = [ + {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, + {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, +] + +[[package]] +name = "httptools" +version = "0.6.4" +requires_python = ">=3.8.0" +summary = "A collection of framework independent HTTP protocol utils." +groups = ["uvicorn"] +files = [ + {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, + {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, + {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, + {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, + {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, + {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, + {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, +] + +[[package]] +name = "httpx" +version = "0.28.1" +requires_python = ">=3.8" +summary = "The next generation HTTP client." +groups = ["test"] +dependencies = [ + "anyio", + "certifi", + "httpcore==1.*", + "idna", +] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[[package]] +name = "identify" +version = "2.6.6" +requires_python = ">=3.9" +summary = "File identification library for Python" +groups = ["test"] +files = [ + {file = "identify-2.6.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881"}, + {file = "identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251"}, +] + +[[package]] +name = "idna" +version = "3.10" +requires_python = ">=3.6" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default", "test", "uvicorn"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +requires_python = ">=3.8" +summary = "Read metadata from Python packages" +groups = ["default"] +dependencies = [ + "typing-extensions>=3.6.4; python_version < \"3.8\"", + "zipp>=3.20", +] +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +groups = ["test"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.13.2" +requires_python = ">=3.8.0" +summary = "A Python utility / library to sort Python imports." +groups = ["test"] +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +groups = ["default"] +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +requires_python = ">=3.8" +summary = "An implementation of JSON Schema validation for Python" +groups = ["default"] +dependencies = [ + "attrs>=22.2.0", + "importlib-resources>=1.4.0; python_version < \"3.9\"", + "jsonschema-specifications>=2023.03.6", + "pkgutil-resolve-name>=1.3.10; python_version < \"3.9\"", + "referencing>=0.28.4", + "rpds-py>=0.7.1", +] +files = [ + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, +] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +requires_python = ">=3.9" +summary = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +groups = ["default"] +dependencies = [ + "referencing>=0.31.0", +] +files = [ + {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, + {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, +] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +requires_python = ">=3.8" +summary = "Jupyter core package. A base package on which Jupyter projects rely." +groups = ["default"] +dependencies = [ + "platformdirs>=2.5", + "pywin32>=300; sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"", + "traitlets>=5.3", +] +files = [ + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, +] + +[[package]] +name = "kombu" +version = "5.4.2" +requires_python = ">=3.8" +summary = "Messaging library for Python." +groups = ["default"] +dependencies = [ + "amqp<6.0.0,>=5.1.1", + "backports-zoneinfo[tzdata]>=0.2.1; python_version < \"3.9\"", + "typing-extensions==4.12.2; python_version < \"3.10\"", + "tzdata; python_version >= \"3.9\"", + "vine==5.1.0", +] +files = [ + {file = "kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763"}, + {file = "kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf"}, +] + +[[package]] +name = "line-profiler" +version = "4.2.0" +requires_python = ">=3.8" +summary = "Line-by-line profiler" +groups = ["default"] +files = [ + {file = "line_profiler-4.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:70e2503f52ee6464ac908b578d73ad6dae21d689c95f2252fee97d7aa8426693"}, + {file = "line_profiler-4.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b6047c8748d7a2453522eaea3edc8d9febc658b57f2ea189c03fe3d5e34595b5"}, + {file = "line_profiler-4.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0048360a2afbd92c0b423f8207af1f6581d85c064c0340b0d02c63c8e0c8292c"}, + {file = "line_profiler-4.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e71fa1c85f21e3de575c7c617fd4eb607b052cc7b4354035fecc18f3f2a4317"}, + {file = "line_profiler-4.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5ec99d48cffdf36efbcd7297e81cc12bf2c0a7e0627a567f3ab0347e607b242"}, + {file = "line_profiler-4.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bfc9582f19a64283434fc6a3fd41a3a51d59e3cce2dc7adc5fe859fcae67e746"}, + {file = "line_profiler-4.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2b5dcfb3205e18c98c94388065f1604dc9d709df4dd62300ff8c5bbbd9bd163f"}, + {file = "line_profiler-4.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:4999eb1db5d52cb34a5293941986eea4357fb9fe3305a160694e5f13c9ec4008"}, + {file = "line_profiler-4.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:402406f200401a496fb93e1788387bf2d87c921d7f8f7e5f88324ac9efb672ac"}, + {file = "line_profiler-4.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d9a0b5696f1ad42bb31e90706e5d57845833483d1d07f092b66b4799847a2f76"}, + {file = "line_profiler-4.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2f950fa19f797a9ab55c8d7b33a7cdd95c396cf124c3adbc1cf93a1978d2767"}, + {file = "line_profiler-4.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d09fd8f580716da5a0b9a7f544a306b468f38eee28ba2465c56e0aa5d7d1822"}, + {file = "line_profiler-4.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:628f585960c6538873a9760d112db20b76b6035d3eaad7711a8bd80fa909d7ea"}, + {file = "line_profiler-4.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:63ed929c7d41e230cc1c4838c25bbee165d7f2fa974ca28d730ea69e501fc44d"}, + {file = "line_profiler-4.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6bda74fc206ba375396068526e9e7b5466a24c7e54cbd6ee1c98c1e0d1f0fd99"}, + {file = "line_profiler-4.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:eaf6eb827c202c07b8b8d82363bb039a6747fbf84ca04279495a91b7da3b773f"}, + {file = "line_profiler-4.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82d29887f1226938a86db30ca3a125b1bde89913768a2a486fa14d0d3f8c0d91"}, + {file = "line_profiler-4.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bf60706467203db0a872b93775a5e5902a02b11d79f8f75a8f8ef381b75789e1"}, + {file = "line_profiler-4.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:934fd964eed9bed87e3c01e8871ee6bdc54d10edf7bf14d20e72f7be03567ae3"}, + {file = "line_profiler-4.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d623e5b37fa48c7ad0c29b4353244346a5dcb1bf75e117e19400b8ffd3393d1b"}, + {file = "line_profiler-4.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efcdbed9ba9003792d8bfd56c11bb3d4e29ad7e0d2f583e1c774de73bbf02933"}, + {file = "line_profiler-4.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:df0149c191a95f2dbc93155b2f9faaee563362d61e78b8986cdb67babe017cdc"}, + {file = "line_profiler-4.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e3a1ca491a8606ed674882b59354087f6e9ab6b94aa6d5fa5d565c6f2acc7a8"}, + {file = "line_profiler-4.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:a85ff57d4ef9d899ca12d6b0883c3cab1786388b29d2fb5f30f909e70bb9a691"}, + {file = "line_profiler-4.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:49db0804e9e330076f0b048d63fd3206331ca0104dd549f61b2466df0f10ecda"}, + {file = "line_profiler-4.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2e983ed4fb2cd68bb8896f6bad7f29ddf9112b978f700448510477bc9fde18db"}, + {file = "line_profiler-4.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d6b27c5880b29369e6bebfe434a16c60cbcd290aa4c384ac612e5777737893f8"}, + {file = "line_profiler-4.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2584dc0af3107efa60bd2ccaa7233dca98e3dff4b11138c0ac30355bc87f1a"}, + {file = "line_profiler-4.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6767d8b922a7368b6917a47c164c3d96d48b82109ad961ef518e78800947cef4"}, + {file = "line_profiler-4.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3137672a769717be4da3a6e006c3bd7b66ad4a341ba89ee749ef96c158a15b22"}, + {file = "line_profiler-4.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:727e970d358616a1a33d51d696efec932a5ef7730785df62658bd7e74aa58951"}, + {file = "line_profiler-4.2.0.tar.gz", hash = "sha256:09e10f25f876514380b3faee6de93fb0c228abba85820ba1a591ddb3eb451a96"}, +] + +[[package]] +name = "mako" +version = "1.3.8" +requires_python = ">=3.8" +summary = "A super-fast templating language that borrows the best ideas from the existing templating languages." +groups = ["default"] +dependencies = [ + "MarkupSafe>=0.9.2", +] +files = [ + {file = "Mako-1.3.8-py3-none-any.whl", hash = "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627"}, + {file = "mako-1.3.8.tar.gz", hash = "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +requires_python = ">=3.9" +summary = "Safely add untrusted strings to HTML/XML markup." +groups = ["default"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +requires_python = ">=3.6" +summary = "McCabe checker, plugin for flake8" +groups = ["test"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["default"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "msgpack" +version = "1.1.0" +requires_python = ">=3.8" +summary = "MessagePack serializer" +groups = ["default"] +files = [ + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, + {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, + {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, + {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, + {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, + {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, + {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, + {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, + {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, + {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, +] + +[[package]] +name = "multidict" +version = "6.1.0" +requires_python = ">=3.8" +summary = "multidict implementation" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.1.0; python_version < \"3.11\"", +] +files = [ + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +requires_python = ">=3.8" +summary = "The Jupyter Notebook format" +groups = ["default"] +dependencies = [ + "fastjsonschema>=2.15", + "jsonschema>=2.6", + "jupyter-core!=5.0.*,>=4.12", + "traitlets>=5.1", +] +files = [ + {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, + {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Node.js virtual environment builder" +groups = ["test"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +requires_python = ">=3.6" +summary = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +groups = ["default"] +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[[package]] +name = "opentelemetry-api" +version = "1.29.0" +requires_python = ">=3.8" +summary = "OpenTelemetry Python API" +groups = ["default"] +dependencies = [ + "deprecated>=1.2.6", + "importlib-metadata<=8.5.0,>=6.0", +] +files = [ + {file = "opentelemetry_api-1.29.0-py3-none-any.whl", hash = "sha256:5fcd94c4141cc49c736271f3e1efb777bebe9cc535759c54c936cca4f1b312b8"}, + {file = "opentelemetry_api-1.29.0.tar.gz", hash = "sha256:d04a6cf78aad09614f52964ecb38021e248f5714dc32c2e0d8fd99517b4d69cf"}, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.38b0" +requires_python = ">=3.7" +summary = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +groups = ["default"] +dependencies = [ + "opentelemetry-api~=1.4", + "setuptools>=16.0", + "wrapt<2.0.0,>=1.0.0", +] +files = [ + {file = "opentelemetry_instrumentation-0.38b0-py3-none-any.whl", hash = "sha256:48eed87e5db9d2cddd57a8ea359bd15318560c0ffdd80d90a5fc65816e15b7f4"}, + {file = "opentelemetry_instrumentation-0.38b0.tar.gz", hash = "sha256:3dbe93248eec7652d5725d3c6d2f9dd048bb8fda6b0505aadbc99e51638d833c"}, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.38b0" +requires_python = ">=3.7" +summary = "ASGI instrumentation for OpenTelemetry" +groups = ["default"] +dependencies = [ + "asgiref~=3.0", + "opentelemetry-api~=1.12", + "opentelemetry-instrumentation==0.38b0", + "opentelemetry-semantic-conventions==0.38b0", + "opentelemetry-util-http==0.38b0", +] +files = [ + {file = "opentelemetry_instrumentation_asgi-0.38b0-py3-none-any.whl", hash = "sha256:c5bba11505008a3cd1b2c42b72f85f3f4f5af50ab931eddd0b01bde376dc5971"}, + {file = "opentelemetry_instrumentation_asgi-0.38b0.tar.gz", hash = "sha256:32d1034c253de6048d0d0166b304f9125267ca9329e374202ebe011a206eba53"}, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.38b0" +requires_python = ">=3.7" +summary = "OpenTelemetry FastAPI Instrumentation" +groups = ["default"] +dependencies = [ + "opentelemetry-api~=1.12", + "opentelemetry-instrumentation-asgi==0.38b0", + "opentelemetry-instrumentation==0.38b0", + "opentelemetry-semantic-conventions==0.38b0", + "opentelemetry-util-http==0.38b0", +] +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.38b0-py3-none-any.whl", hash = "sha256:91139586732e437b1c3d5cf838dc5be910bce27b4b679612112be03fcc4fa2aa"}, + {file = "opentelemetry_instrumentation_fastapi-0.38b0.tar.gz", hash = "sha256:8946fd414084b305ad67556a1907e2d4a497924d023effc5ea3b4b1b0c55b256"}, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.38b0" +requires_python = ">=3.7" +summary = "OpenTelemetry Semantic Conventions" +groups = ["default"] +files = [ + {file = "opentelemetry_semantic_conventions-0.38b0-py3-none-any.whl", hash = "sha256:b0ba36e8b70bfaab16ee5a553d809309cc11ff58aec3d2550d451e79d45243a7"}, + {file = "opentelemetry_semantic_conventions-0.38b0.tar.gz", hash = "sha256:37f09e47dd5fc316658bf9ee9f37f9389b21e708faffa4a65d6a3de484d22309"}, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.38b0" +requires_python = ">=3.7" +summary = "Web util for OpenTelemetry" +groups = ["default"] +files = [ + {file = "opentelemetry_util_http-0.38b0-py3-none-any.whl", hash = "sha256:8e5f0451eeb5307b2c628dd799886adc5e113fb13a7207c29c672e8d168eabd8"}, + {file = "opentelemetry_util_http-0.38b0.tar.gz", hash = "sha256:85eb032b6129c4d7620583acf574e99fe2e73c33d60e256b54af436f76ceb5ae"}, +] + +[[package]] +name = "packaging" +version = "24.2" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["test"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "passlib" +version = "1.7.4" +summary = "comprehensive password hashing framework supporting over 30 schemes" +groups = ["default"] +files = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] + +[[package]] +name = "pendulum" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python datetimes made easy" +groups = ["default"] +dependencies = [ + "backports-zoneinfo>=0.2.1; python_version < \"3.9\"", + "importlib-resources>=5.9.0; python_version < \"3.9\"", + "python-dateutil>=2.6", + "time-machine>=2.6.0; implementation_name != \"pypy\"", + "tzdata>=2020.1", +] +files = [ + {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, + {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, + {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, + {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, + {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, + {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, + {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, + {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +requires_python = ">=3.8" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["default", "test"] +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +requires_python = ">=3.8" +summary = "plugin and hook calling mechanisms for python" +groups = ["test"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[[package]] +name = "pre-commit" +version = "4.1.0" +requires_python = ">=3.9" +summary = "A framework for managing and maintaining multi-language pre-commit hooks." +groups = ["test"] +dependencies = [ + "cfgv>=2.0.0", + "identify>=1.0.0", + "nodeenv>=0.11.1", + "pyyaml>=5.1", + "virtualenv>=20.10.0", +] +files = [ + {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, + {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.50" +requires_python = ">=3.8.0" +summary = "Library for building powerful interactive command lines in Python" +groups = ["default"] +dependencies = [ + "wcwidth", +] +files = [ + {file = "prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"}, + {file = "prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab"}, +] + +[[package]] +name = "propcache" +version = "0.2.1" +requires_python = ">=3.9" +summary = "Accelerated property cache" +groups = ["default"] +files = [ + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"}, + {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"}, + {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"}, + {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"}, + {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"}, + {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"}, + {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"}, + {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"}, + {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"}, + {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"}, + {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, +] + +[[package]] +name = "proto-plus" +version = "1.26.0rc1" +requires_python = ">=3.7" +summary = "Beautiful, Pythonic protocol buffers" +groups = ["default"] +dependencies = [ + "protobuf<6.0.0dev,>=3.19.0", +] +files = [ + {file = "proto_plus-1.26.0rc1-py3-none-any.whl", hash = "sha256:a0ad6fbc2e194dbbb813edc22ee2e509a7c38df7ecea2fd2803bce0536eaf0f4"}, + {file = "proto_plus-1.26.0rc1.tar.gz", hash = "sha256:04eeceecd6a038285e2aa8996b53c045d04a568c5c48b7eaa79c097a4984a4c7"}, +] + +[[package]] +name = "protobuf" +version = "5.29.3" +requires_python = ">=3.8" +summary = "" +groups = ["default"] +files = [ + {file = "protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888"}, + {file = "protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a"}, + {file = "protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f"}, + {file = "protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f"}, + {file = "protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620"}, +] + +[[package]] +name = "psycopg" +version = "3.2.4" +requires_python = ">=3.8" +summary = "PostgreSQL database adapter for Python" +groups = ["default"] +dependencies = [ + "backports-zoneinfo>=0.2.0; python_version < \"3.9\"", + "typing-extensions>=4.6; python_version < \"3.13\"", + "tzdata; sys_platform == \"win32\"", +] +files = [ + {file = "psycopg-3.2.4-py3-none-any.whl", hash = "sha256:43665368ccd48180744cab26b74332f46b63b7e06e8ce0775547a3533883d381"}, + {file = "psycopg-3.2.4.tar.gz", hash = "sha256:f26f1346d6bf1ef5f5ef1714dd405c67fb365cfd1c6cea07de1792747b167b92"}, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +requires_python = ">=3.8" +summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +groups = ["default"] +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +requires_python = ">=3.8" +summary = "A collection of ASN.1-based protocols modules" +groups = ["default"] +dependencies = [ + "pyasn1<0.7.0,>=0.4.6", +] +files = [ + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +requires_python = ">=3.8" +summary = "C parser in Python" +groups = ["default", "test"] +marker = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pydantic" +version = "1.10.21" +requires_python = ">=3.7" +summary = "Data validation and settings management using python type hints" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.2.0", +] +files = [ + {file = "pydantic-1.10.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:245e486e0fec53ec2366df9cf1cba36e0bbf066af7cd9c974bbbd9ba10e1e586"}, + {file = "pydantic-1.10.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c54f8d4c151c1de784c5b93dfbb872067e3414619e10e21e695f7bb84d1d1fd"}, + {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b64708009cfabd9c2211295144ff455ec7ceb4c4fb45a07a804309598f36187"}, + {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a148410fa0e971ba333358d11a6dea7b48e063de127c2b09ece9d1c1137dde4"}, + {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:36ceadef055af06e7756eb4b871cdc9e5a27bdc06a45c820cd94b443de019bbf"}, + {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0501e1d12df6ab1211b8cad52d2f7b2cd81f8e8e776d39aa5e71e2998d0379f"}, + {file = "pydantic-1.10.21-cp310-cp310-win_amd64.whl", hash = "sha256:c261127c275d7bce50b26b26c7d8427dcb5c4803e840e913f8d9df3f99dca55f"}, + {file = "pydantic-1.10.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b6350b68566bb6b164fb06a3772e878887f3c857c46c0c534788081cb48adf4"}, + {file = "pydantic-1.10.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:935b19fdcde236f4fbf691959fa5c3e2b6951fff132964e869e57c70f2ad1ba3"}, + {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6a04efdcd25486b27f24c1648d5adc1633ad8b4506d0e96e5367f075ed2e0b"}, + {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1ba253eb5af8d89864073e6ce8e6c8dec5f49920cff61f38f5c3383e38b1c9f"}, + {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:57f0101e6c97b411f287a0b7cf5ebc4e5d3b18254bf926f45a11615d29475793"}, + {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e85834f0370d737c77a386ce505c21b06bfe7086c1c568b70e15a568d9670d"}, + {file = "pydantic-1.10.21-cp311-cp311-win_amd64.whl", hash = "sha256:6a497bc66b3374b7d105763d1d3de76d949287bf28969bff4656206ab8a53aa9"}, + {file = "pydantic-1.10.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ed4a5f13cf160d64aa331ab9017af81f3481cd9fd0e49f1d707b57fe1b9f3ae"}, + {file = "pydantic-1.10.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b7693bb6ed3fbe250e222f9415abb73111bb09b73ab90d2d4d53f6390e0ccc1"}, + {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185d5f1dff1fead51766da9b2de4f3dc3b8fca39e59383c273f34a6ae254e3e2"}, + {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38e6d35cf7cd1727822c79e324fa0677e1a08c88a34f56695101f5ad4d5e20e5"}, + {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1d7c332685eafacb64a1a7645b409a166eb7537f23142d26895746f628a3149b"}, + {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c9b782db6f993a36092480eeaab8ba0609f786041b01f39c7c52252bda6d85f"}, + {file = "pydantic-1.10.21-cp312-cp312-win_amd64.whl", hash = "sha256:7ce64d23d4e71d9698492479505674c5c5b92cda02b07c91dfc13633b2eef805"}, + {file = "pydantic-1.10.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0067935d35044950be781933ab91b9a708eaff124bf860fa2f70aeb1c4be7212"}, + {file = "pydantic-1.10.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5e8148c2ce4894ce7e5a4925d9d3fdce429fb0e821b5a8783573f3611933a251"}, + {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4973232c98b9b44c78b1233693e5e1938add5af18042f031737e1214455f9b8"}, + {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:662bf5ce3c9b1cef32a32a2f4debe00d2f4839fefbebe1d6956e681122a9c839"}, + {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98737c3ab5a2f8a85f2326eebcd214510f898881a290a7939a45ec294743c875"}, + {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0bb58bbe65a43483d49f66b6c8474424d551a3fbe8a7796c42da314bac712738"}, + {file = "pydantic-1.10.21-cp313-cp313-win_amd64.whl", hash = "sha256:e622314542fb48542c09c7bd1ac51d71c5632dd3c92dc82ede6da233f55f4848"}, + {file = "pydantic-1.10.21-py3-none-any.whl", hash = "sha256:db70c920cba9d05c69ad4a9e7f8e9e83011abb2c6490e561de9ae24aee44925c"}, + {file = "pydantic-1.10.21.tar.gz", hash = "sha256:64b48e2b609a6c22178a56c408ee1215a7206077ecb8a193e2fda31858b2362a"}, +] + +[[package]] +name = "pygments" +version = "2.19.1" +requires_python = ">=3.8" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["default"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[[package]] +name = "pylint" +version = "3.3.3" +requires_python = ">=3.9.0" +summary = "python code static checker" +groups = ["test"] +dependencies = [ + "astroid<=3.4.0-dev0,>=3.3.8", + "colorama>=0.4.5; sys_platform == \"win32\"", + "dill>=0.2; python_version < \"3.11\"", + "dill>=0.3.6; python_version >= \"3.11\"", + "dill>=0.3.7; python_version >= \"3.12\"", + "isort!=5.13.0,<6,>=4.2.5", + "mccabe<0.8,>=0.6", + "platformdirs>=2.2.0", + "tomli>=1.1.0; python_version < \"3.11\"", + "tomlkit>=0.10.1", + "typing-extensions>=3.10.0; python_version < \"3.10\"", +] +files = [ + {file = "pylint-3.3.3-py3-none-any.whl", hash = "sha256:26e271a2bc8bce0fc23833805a9076dd9b4d5194e2a02164942cb3cdc37b4183"}, + {file = "pylint-3.3.3.tar.gz", hash = "sha256:07c607523b17e6d16e2ae0d7ef59602e332caa762af64203c24b41c27139f36a"}, +] + +[[package]] +name = "pyparsing" +version = "3.2.1" +requires_python = ">=3.9" +summary = "pyparsing module - Classes and methods to define and execute parsing grammars" +groups = ["default"] +marker = "python_version > \"3.0\"" +files = [ + {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, + {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, +] + +[[package]] +name = "pytest" +version = "8.3.4" +requires_python = ">=3.8" +summary = "pytest: simple powerful testing with Python" +groups = ["test"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2,>=1.5", + "tomli>=1; python_version < \"3.11\"", +] +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[[package]] +name = "pytest-asyncio" +version = "0.21.2" +requires_python = ">=3.7" +summary = "Pytest support for asyncio" +groups = ["test"] +dependencies = [ + "pytest>=7.0.0", + "typing-extensions>=3.7.2; python_version < \"3.8\"", +] +files = [ + {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, + {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +requires_python = ">=3.9" +summary = "Pytest plugin for measuring coverage." +groups = ["test"] +dependencies = [ + "coverage[toml]>=7.5", + "pytest>=4.6", +] +files = [ + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, +] + +[[package]] +name = "pytest-integration" +version = "0.2.3" +requires_python = ">=3.6" +summary = "Organizing pytests by integration or not" +groups = ["test"] +files = [ + {file = "pytest_integration-0.2.3-py3-none-any.whl", hash = "sha256:7f59ed1fa1cc8cb240f9495b68bc02c0421cce48589f78e49b7b842231604b12"}, + {file = "pytest_integration-0.2.3.tar.gz", hash = "sha256:b00988a5de8a6826af82d4c7a3485b43fbf32c11235e9f4a8b7225eef5fbcf65"}, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +requires_python = ">=3.8" +summary = "Thin-wrapper around the mock package for easier use with pytest" +groups = ["test"] +dependencies = [ + "pytest>=6.2.5", +] +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[[package]] +name = "pytest-xdist" +version = "3.6.1" +requires_python = ">=3.8" +summary = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +groups = ["test"] +dependencies = [ + "execnet>=2.1", + "pytest>=7.0.0", +] +files = [ + {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, + {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +groups = ["default", "test"] +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[[package]] +name = "python-dotenv" +version = "0.21.1" +requires_python = ">=3.7" +summary = "Read key-value pairs from a .env file and set them as environment variables" +groups = ["default", "test", "uvicorn"] +files = [ + {file = "python-dotenv-0.21.1.tar.gz", hash = "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49"}, + {file = "python_dotenv-0.21.1-py3-none-any.whl", hash = "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a"}, +] + +[[package]] +name = "python-jose" +version = "3.3.0" +summary = "JOSE implementation in Python" +groups = ["default"] +dependencies = [ + "ecdsa!=0.15", + "pyasn1", + "rsa", +] +files = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +requires_python = ">=3.8" +summary = "A streaming multipart parser for Python" +groups = ["default"] +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + +[[package]] +name = "pywin32" +version = "308" +summary = "Python for Window Extensions" +groups = ["default", "test"] +marker = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, + {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, + {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, + {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, + {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, + {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, + {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, + {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, + {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, + {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, + {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, + {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +requires_python = ">=3.8" +summary = "YAML parser and emitter for Python" +groups = ["test", "uvicorn"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "redis" +version = "4.6.0" +requires_python = ">=3.7" +summary = "Python client for Redis database and key-value store" +groups = ["default"] +dependencies = [ + "async-timeout>=4.0.2; python_full_version <= \"3.11.2\"", + "importlib-metadata>=1.0; python_version < \"3.8\"", + "typing-extensions; python_version < \"3.8\"", +] +files = [ + {file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"}, + {file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"}, +] + +[[package]] +name = "referencing" +version = "0.36.2" +requires_python = ">=3.9" +summary = "JSON Referencing + Python" +groups = ["default"] +dependencies = [ + "attrs>=22.2.0", + "rpds-py>=0.7.0", + "typing-extensions>=4.4.0; python_version < \"3.13\"", +] +files = [ + {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, + {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, +] + +[[package]] +name = "requests" +version = "2.29.0" +requires_python = ">=3.7" +summary = "Python HTTP for Humans." +groups = ["default", "test"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<1.27,>=1.21.1", +] +files = [ + {file = "requests-2.29.0-py3-none-any.whl", hash = "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b"}, + {file = "requests-2.29.0.tar.gz", hash = "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059"}, +] + +[[package]] +name = "requests-mock" +version = "1.12.1" +requires_python = ">=3.5" +summary = "Mock out responses from the requests package" +groups = ["test"] +dependencies = [ + "requests<3,>=2.22", +] +files = [ + {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, + {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +requires_python = ">=3.4" +summary = "OAuthlib authentication support for Requests." +groups = ["default"] +dependencies = [ + "oauthlib>=3.0.0", + "requests>=2.0.0", +] +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[[package]] +name = "rich" +version = "13.9.4" +requires_python = ">=3.8.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +groups = ["default"] +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", + "typing-extensions<5.0,>=4.0.0; python_version < \"3.11\"", +] +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[[package]] +name = "rpds-py" +version = "0.22.3" +requires_python = ">=3.9" +summary = "Python bindings to Rust's persistent data structures (rpds)" +groups = ["default"] +files = [ + {file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"}, + {file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf"}, + {file = "rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652"}, + {file = "rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a"}, + {file = "rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64"}, + {file = "rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7"}, + {file = "rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627"}, + {file = "rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f"}, + {file = "rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de"}, + {file = "rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d"}, + {file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"}, +] + +[[package]] +name = "rsa" +version = "4.9" +requires_python = ">=3.6,<4" +summary = "Pure-Python RSA implementation" +groups = ["default"] +dependencies = [ + "pyasn1>=0.1.3", +] +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[[package]] +name = "setuptools" +version = "75.8.0" +requires_python = ">=3.9" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +groups = ["default", "test"] +files = [ + {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, + {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, +] + +[[package]] +name = "six" +version = "1.17.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Python 2 and 3 compatibility utilities" +groups = ["default", "test"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +groups = ["default", "test", "uvicorn"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.37" +requires_python = ">=3.7" +summary = "Database Abstraction Library" +groups = ["default"] +dependencies = [ + "greenlet!=0.4.17; (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version < \"3.14\"", + "importlib-metadata; python_version < \"3.8\"", + "typing-extensions>=4.6.0", +] +files = [ + {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-win32.whl", hash = "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-win_amd64.whl", hash = "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78361be6dc9073ed17ab380985d1e45e48a642313ab68ab6afa2457354ff692c"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b661b49d0cb0ab311a189b31e25576b7ac3e20783beb1e1817d72d9d02508bf5"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d57bafbab289e147d064ffbd5cca2d7b1394b63417c0636cea1f2e93d16eb9e8"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2c0913f02341d25fb858e4fb2031e6b0813494cca1ba07d417674128ce11b"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9df21b8d9e5c136ea6cde1c50d2b1c29a2b5ff2b1d610165c23ff250e0704087"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db18ff6b8c0f1917f8b20f8eca35c28bbccb9f83afa94743e03d40203ed83de9"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-win32.whl", hash = "sha256:46954173612617a99a64aee103bcd3f078901b9a8dcfc6ae80cbf34ba23df989"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-win_amd64.whl", hash = "sha256:7b7e772dc4bc507fdec4ee20182f15bd60d2a84f1e087a8accf5b5b7a0dcf2ba"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2952748ecd67ed3b56773c185e85fc084f6bdcdec10e5032a7c25a6bc7d682ef"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3151822aa1db0eb5afd65ccfafebe0ef5cda3a7701a279c8d0bf17781a793bb4"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaa8039b6d20137a4e02603aba37d12cd2dde7887500b8855356682fc33933f4"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cdba1f73b64530c47b27118b7053b8447e6d6f3c8104e3ac59f3d40c33aa9fd"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1b2690456528a87234a75d1a1644cdb330a6926f455403c8e4f6cad6921f9098"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf5ae8a9dcf657fd72144a7fd01f243236ea39e7344e579a121c4205aedf07bb"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-win32.whl", hash = "sha256:ea308cec940905ba008291d93619d92edaf83232ec85fbd514dcb329f3192761"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-win_amd64.whl", hash = "sha256:635d8a21577341dfe4f7fa59ec394b346da12420b86624a69e466d446de16aff"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c4096727193762e72ce9437e2a86a110cf081241919ce3fab8e89c02f6b6658"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4fb5ac86d8fe8151966814f6720996430462e633d225497566b3996966b9bdb"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e56a139bfe136a22c438478a86f8204c1eb5eed36f4e15c4224e4b9db01cb3e4"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f95fc8e3f34b5f6b3effb49d10ac97c569ec8e32f985612d9b25dd12d0d2e94"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c505edd429abdfe3643fa3b2e83efb3445a34a9dc49d5f692dd087be966020e0"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12b0f1ec623cccf058cf21cb544f0e74656618165b083d78145cafde156ea7b6"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-win32.whl", hash = "sha256:293f9ade06b2e68dd03cfb14d49202fac47b7bb94bffcff174568c951fbc7af2"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-win_amd64.whl", hash = "sha256:d70f53a0646cc418ca4853da57cf3ddddbccb8c98406791f24426f2dd77fd0e2"}, + {file = "SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1"}, + {file = "sqlalchemy-2.0.37.tar.gz", hash = "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb"}, +] + +[[package]] +name = "sqlalchemy-utils" +version = "0.41.2" +requires_python = ">=3.7" +summary = "Various utility functions for SQLAlchemy." +groups = ["default"] +dependencies = [ + "SQLAlchemy>=1.3", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990"}, + {file = "SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e"}, +] + +[[package]] +name = "sqlglot" +version = "26.3.8" +requires_python = ">=3.7" +summary = "An easily customizable SQL parser and transpiler" +groups = ["transpilation"] +files = [ + {file = "sqlglot-26.3.8-py3-none-any.whl", hash = "sha256:c408b328b2090d1178ab076514714946c27b55b88dac68f5e565543849502f9d"}, + {file = "sqlglot-26.3.8.tar.gz", hash = "sha256:225a3645f1fbff3cdb4bf5ef8c8aea867ddc79b567fc5bc5fd21811875c7099a"}, +] + +[[package]] +name = "sqlparse" +version = "0.5.3" +requires_python = ">=3.8" +summary = "A non-validating SQL parser." +groups = ["test"] +files = [ + {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, + {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, +] + +[[package]] +name = "sse-starlette" +version = "2.0.0" +requires_python = ">=3.8" +summary = "SSE plugin for Starlette" +groups = ["default"] +dependencies = [ + "anyio", + "starlette", + "uvicorn", +] +files = [ + {file = "sse_starlette-2.0.0-py3-none-any.whl", hash = "sha256:c4dd134302cb9708d47cae23c365fe0a089aa2a875d2f887ac80f235a9ee5744"}, + {file = "sse_starlette-2.0.0.tar.gz", hash = "sha256:0c43cc43aca4884c88c8416b65777c4de874cc4773e6458d3579c0a353dc2fb7"}, +] + +[[package]] +name = "starlette" +version = "0.45.3" +requires_python = ">=3.9" +summary = "The little ASGI library that shines." +groups = ["default"] +dependencies = [ + "anyio<5,>=3.6.2", + "typing-extensions>=3.10.0; python_version < \"3.10\"", +] +files = [ + {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"}, + {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"}, +] + +[[package]] +name = "strawberry-graphql" +version = "0.258.0" +requires_python = "<4.0,>=3.9" +summary = "A library for creating GraphQL APIs" +groups = ["default"] +dependencies = [ + "graphql-core<3.4.0,>=3.2.0", + "python-dateutil<3.0.0,>=2.7.0", + "typing-extensions>=4.5.0", +] +files = [ + {file = "strawberry_graphql-0.258.0-py3-none-any.whl", hash = "sha256:041adda6e9a97ca337793f6c07fe4db0a9793226907394a00022782f132040ec"}, + {file = "strawberry_graphql-0.258.0.tar.gz", hash = "sha256:3975c638f751e9b87cefd5eb1a29c1f33e639b1f218f199578114fb839dec94c"}, +] + +[[package]] +name = "testcontainers" +version = "4.9.1" +requires_python = "<4.0,>=3.9" +summary = "Python library for throwaway instances of anything that can run in a Docker container" +groups = ["test"] +dependencies = [ + "docker", + "python-dotenv", + "typing-extensions", + "urllib3", + "wrapt", +] +files = [ + {file = "testcontainers-4.9.1-py3-none-any.whl", hash = "sha256:315fb94b42a383872df530aa45319745278ef0cc18b9cfcdc231a75d14afa5a0"}, + {file = "testcontainers-4.9.1.tar.gz", hash = "sha256:37fe9a222549ddb788463935965b16f91809e9a8d654f437d6a59eac9b77f76f"}, +] + +[[package]] +name = "time-machine" +version = "2.16.0" +requires_python = ">=3.9" +summary = "Travel through time in your tests." +groups = ["default"] +marker = "implementation_name != \"pypy\"" +dependencies = [ + "python-dateutil", +] +files = [ + {file = "time_machine-2.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09531af59fdfb39bfd24d28bd1e837eff5a5d98318509a31b6cfd57d27801e52"}, + {file = "time_machine-2.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:92d0b0f3c49f34dd76eb462f0afdc61ed1cb318c06c46d03e99b44ebb489bdad"}, + {file = "time_machine-2.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c29616e18e2349a8766d5b6817920fc74e39c00fa375d202231e9d525a1b882"}, + {file = "time_machine-2.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1ceb6035a64cb00650e3ab203cf3faffac18576a3f3125c24df468b784077c7"}, + {file = "time_machine-2.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64c205ea37b8c4ba232645335fc3b75bc2d03ce30f0a34649e36cae85652ee96"}, + {file = "time_machine-2.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dfe92412bd11104c4f0fb2da68653e6c45b41f7217319a83a8b66ed4f20148b3"}, + {file = "time_machine-2.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d5fe7a6284e3dce87ae13a25029c53542dd27a28d151f3ef362ec4dd9c3e45fd"}, + {file = "time_machine-2.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0fca3025266d88d1b48be162a43b7c2d91c81cc5b3bee9f01194678ffb9969a"}, + {file = "time_machine-2.16.0-cp310-cp310-win32.whl", hash = "sha256:4149e17018af07a5756a1df84aea71e6e178598c358c860c6bfec42170fa7970"}, + {file = "time_machine-2.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:01bc257e9418980a4922de94775be42a966e1a082fb01a1635917f9afc7b84ca"}, + {file = "time_machine-2.16.0-cp310-cp310-win_arm64.whl", hash = "sha256:6895e3e84119594ab12847c928f619d40ae9cedd0755515dc154a5b5dc6edd9f"}, + {file = "time_machine-2.16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8f936566ef9f09136a3d5db305961ef6d897b76b240c9ff4199144aed6dd4fe5"}, + {file = "time_machine-2.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5886e23ede3478ca2a3e0a641f5d09dd784dfa9e48c96e8e5e31fc4fe77b6dc0"}, + {file = "time_machine-2.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76caf539fa4941e1817b7c482c87c65c52a1903fea761e84525955c6106fafb"}, + {file = "time_machine-2.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:298aa423e07c8b21b991782f01d7749c871c792319c2af3e9755f9ab49033212"}, + {file = "time_machine-2.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391ae9c484736850bb44ef125cbad52fe2d1b69e42c95dc88c43af8ead2cc7"}, + {file = "time_machine-2.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:503e7ff507c2089699d91885fc5b9c8ff16774a7b6aff48b4dcee0c0a0685b61"}, + {file = "time_machine-2.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eee7b0fc4fbab2c6585ea17606c6548be83919c70deea0865409fe9fc2d8cdce"}, + {file = "time_machine-2.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9db5e5b3ccdadaafa5730c2f9db44c38b013234c9ad01f87738907e19bdba268"}, + {file = "time_machine-2.16.0-cp311-cp311-win32.whl", hash = "sha256:2552f0767bc10c9d668f108fef9b487809cdeb772439ce932e74136365c69baf"}, + {file = "time_machine-2.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:12474fcdbc475aa6fe5275fe7224e685c5b9777f5939647f35980e9614ae7558"}, + {file = "time_machine-2.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:ac2df0fa564356384515ed62cb6679f33f1f529435b16b0ec0f88414635dbe39"}, + {file = "time_machine-2.16.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:84788f4d62a8b1bf5e499bb9b0e23ceceea21c415ad6030be6267ce3d639842f"}, + {file = "time_machine-2.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:15ec236b6571730236a193d9d6c11d472432fc6ab54e85eac1c16d98ddcd71bf"}, + {file = "time_machine-2.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cedc989717c8b44a3881ac3d68ab5a95820448796c550de6a2149ed1525157f0"}, + {file = "time_machine-2.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d26d79de1c63a8c6586c75967e09b0ff306aa7e944a1eaddb74595c9b1839ca"}, + {file = "time_machine-2.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:317b68b56a9c3731e0cf8886e0f94230727159e375988b36c60edce0ddbcb44a"}, + {file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:43e1e18279759897be3293a255d53e6b1cb0364b69d9591d0b80c51e461c94b0"}, + {file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e43adb22def972a29d2b147999b56897116085777a0fea182fd93ee45730611e"}, + {file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0c766bea27a0600e36806d628ebc4b47178b12fcdfb6c24dc0a566a9c06bfe7f"}, + {file = "time_machine-2.16.0-cp312-cp312-win32.whl", hash = "sha256:6dae82ab647d107817e013db82223e20a9853fa88543fec853ae326382d03c2e"}, + {file = "time_machine-2.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:265462c77dc9576267c3c7f20707780a171a9fdbac93ac22e608c309efd68c33"}, + {file = "time_machine-2.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:ef768e14768eebe3bb1196c0dece8e14c1c6991605721214a0c3c68cf77eb216"}, + {file = "time_machine-2.16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7751bf745d54e9e8b358c0afa332815da9b8a6194b26d0fd62876ab6c4d5c9c0"}, + {file = "time_machine-2.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1784edf173ca840ba154de6eed000b5727f65ab92972c2f88cec5c4d6349c5f2"}, + {file = "time_machine-2.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f5876a5682ce1f517e55d7ace2383432627889f6f7e338b961f99d684fd9e8d"}, + {file = "time_machine-2.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:806672529a2e255cd901f244c9033767dc1fa53466d0d3e3e49565a1572a64fe"}, + {file = "time_machine-2.16.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:667b150fedb54acdca2a4bea5bf6da837b43e6dd12857301b48191f8803ba93f"}, + {file = "time_machine-2.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:da3ae1028af240c0c46c79adf9c1acffecc6ed1701f2863b8132f5ceae6ae4b5"}, + {file = "time_machine-2.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:520a814ea1b2706c89ab260a54023033d3015abef25c77873b83e3d7c1fafbb2"}, + {file = "time_machine-2.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8243664438bb468408b29c6865958662d75e51f79c91842d2794fa22629eb697"}, + {file = "time_machine-2.16.0-cp313-cp313-win32.whl", hash = "sha256:32d445ce20d25c60ab92153c073942b0bac9815bfbfd152ce3dcc225d15ce988"}, + {file = "time_machine-2.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:f6927dda86425f97ffda36131f297b1a601c64a6ee6838bfa0e6d3149c2f0d9f"}, + {file = "time_machine-2.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:4d3843143c46dddca6491a954bbd0abfd435681512ac343169560e9bab504129"}, + {file = "time_machine-2.16.0.tar.gz", hash = "sha256:4a99acc273d2f98add23a89b94d4dd9e14969c01214c8514bfa78e4e9364c7e2"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +requires_python = ">=3.8" +summary = "A lil' TOML parser" +groups = ["test"] +marker = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +requires_python = ">=3.8" +summary = "Style preserving TOML library" +groups = ["test"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +requires_python = ">=3.8" +summary = "Traitlets Python configuration system" +groups = ["default"] +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + +[[package]] +name = "types-cachetools" +version = "5.5.0.20240820" +requires_python = ">=3.8" +summary = "Typing stubs for cachetools" +groups = ["default"] +files = [ + {file = "types-cachetools-5.5.0.20240820.tar.gz", hash = "sha256:b888ab5c1a48116f7799cd5004b18474cd82b5463acb5ffb2db2fc9c7b053bc0"}, + {file = "types_cachetools-5.5.0.20240820-py3-none-any.whl", hash = "sha256:efb2ed8bf27a4b9d3ed70d33849f536362603a90b8090a328acf0cd42fda82e2"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default", "test", "uvicorn"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2025.1" +requires_python = ">=2" +summary = "Provider of IANA time zone data" +groups = ["default"] +files = [ + {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, + {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, +] + +[[package]] +name = "uritemplate" +version = "4.1.1" +requires_python = ">=3.6" +summary = "Implementation of RFC 6570 URI Templates" +groups = ["default"] +files = [ + {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, + {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["default", "test"] +files = [ + {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, + {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, +] + +[[package]] +name = "uvicorn" +version = "0.34.0" +requires_python = ">=3.9" +summary = "The lightning-fast ASGI server." +groups = ["default", "uvicorn"] +dependencies = [ + "click>=7.0", + "h11>=0.8", + "typing-extensions>=4.0; python_version < \"3.11\"", +] +files = [ + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, +] + +[[package]] +name = "uvicorn" +version = "0.34.0" +extras = ["standard"] +requires_python = ">=3.9" +summary = "The lightning-fast ASGI server." +groups = ["uvicorn"] +dependencies = [ + "colorama>=0.4; sys_platform == \"win32\"", + "httptools>=0.6.3", + "python-dotenv>=0.13", + "pyyaml>=5.1", + "uvicorn==0.34.0", + "uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"", + "watchfiles>=0.13", + "websockets>=10.4", +] +files = [ + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +requires_python = ">=3.8.0" +summary = "Fast implementation of asyncio event loop on top of libuv" +groups = ["uvicorn"] +marker = "(sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"" +files = [ + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, + {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, +] + +[[package]] +name = "vine" +version = "5.1.0" +requires_python = ">=3.6" +summary = "Python promises." +groups = ["default"] +files = [ + {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, + {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, +] + +[[package]] +name = "virtualenv" +version = "20.29.1" +requires_python = ">=3.8" +summary = "Virtual Python Environment builder" +groups = ["test"] +dependencies = [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.12.2", + "importlib-metadata>=6.6; python_version < \"3.8\"", + "platformdirs<5,>=3.9.1", +] +files = [ + {file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"}, + {file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"}, +] + +[[package]] +name = "watchfiles" +version = "1.0.4" +requires_python = ">=3.9" +summary = "Simple, modern and high performance file watching and code reload in python." +groups = ["uvicorn"] +dependencies = [ + "anyio>=3.0.0", +] +files = [ + {file = "watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08"}, + {file = "watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899"}, + {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff"}, + {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f"}, + {file = "watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f"}, + {file = "watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161"}, + {file = "watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19"}, + {file = "watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c"}, + {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1"}, + {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226"}, + {file = "watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105"}, + {file = "watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74"}, + {file = "watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3"}, + {file = "watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2"}, + {file = "watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a"}, + {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff"}, + {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e"}, + {file = "watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94"}, + {file = "watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c"}, + {file = "watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90"}, + {file = "watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9"}, + {file = "watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902"}, + {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1"}, + {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303"}, + {file = "watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80"}, + {file = "watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d"}, + {file = "watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205"}, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +summary = "Measures the displayed width of unicode strings in a terminal" +groups = ["default"] +dependencies = [ + "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", +] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "websockets" +version = "14.2" +requires_python = ">=3.9" +summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +groups = ["uvicorn"] +files = [ + {file = "websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885"}, + {file = "websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397"}, + {file = "websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d"}, + {file = "websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d"}, + {file = "websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2"}, + {file = "websockets-14.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166"}, + {file = "websockets-14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f"}, + {file = "websockets-14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d"}, + {file = "websockets-14.2-cp311-cp311-win32.whl", hash = "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a"}, + {file = "websockets-14.2-cp311-cp311-win_amd64.whl", hash = "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b"}, + {file = "websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c"}, + {file = "websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967"}, + {file = "websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe"}, + {file = "websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205"}, + {file = "websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce"}, + {file = "websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e"}, + {file = "websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad"}, + {file = "websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307"}, + {file = "websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc"}, + {file = "websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f"}, + {file = "websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29"}, + {file = "websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a"}, + {file = "websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3"}, + {file = "websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b"}, + {file = "websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5"}, +] + +[[package]] +name = "wrapt" +version = "1.17.2" +requires_python = ">=3.8" +summary = "Module for decorators, wrappers and monkey patching." +groups = ["default", "test"] +files = [ + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, + {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, + {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, + {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, + {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, + {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, + {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, + {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, + {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, + {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, + {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, + {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, + {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, +] + +[[package]] +name = "yarl" +version = "1.18.3" +requires_python = ">=3.9" +summary = "Yet another URL library" +groups = ["default"] +dependencies = [ + "idna>=2.0", + "multidict>=4.0", + "propcache>=0.2.0", +] +files = [ + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, + {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, + {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, + {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, + {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, + {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, + {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, + {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, + {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, + {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, + {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, +] + +[[package]] +name = "zipp" +version = "3.21.0" +requires_python = ">=3.9" +summary = "Backport of pathlib-compatible object wrapper for zip files" +groups = ["default"] +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[[package]] +name = "zope-event" +version = "5.0" +requires_python = ">=3.7" +summary = "Very basic event publishing system" +groups = ["test"] +dependencies = [ + "setuptools", +] +files = [ + {file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"}, + {file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"}, +] + +[[package]] +name = "zope-interface" +version = "7.2" +requires_python = ">=3.8" +summary = "Interfaces for Python" +groups = ["test"] +dependencies = [ + "setuptools", +] +files = [ + {file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"}, + {file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"}, + {file = "zope.interface-7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550f1c6588ecc368c9ce13c44a49b8d6b6f3ca7588873c679bd8fd88a1b557b6"}, + {file = "zope.interface-7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef9e2f865721553c6f22a9ff97da0f0216c074bd02b25cf0d3af60ea4d6931d"}, + {file = "zope.interface-7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27f926f0dcb058211a3bb3e0e501c69759613b17a553788b2caeb991bed3b61d"}, + {file = "zope.interface-7.2-cp310-cp310-win_amd64.whl", hash = "sha256:144964649eba4c5e4410bb0ee290d338e78f179cdbfd15813de1a664e7649b3b"}, + {file = "zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2"}, + {file = "zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22"}, + {file = "zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7"}, + {file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c"}, + {file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a"}, + {file = "zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1"}, + {file = "zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7"}, + {file = "zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465"}, + {file = "zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89"}, + {file = "zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54"}, + {file = "zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d"}, + {file = "zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5"}, + {file = "zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98"}, + {file = "zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d"}, + {file = "zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c"}, + {file = "zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398"}, + {file = "zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b"}, + {file = "zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd"}, + {file = "zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe"}, +] diff --git a/datajunction-server/pyproject.toml b/datajunction-server/pyproject.toml new file mode 100644 index 000000000..1b45b2491 --- /dev/null +++ b/datajunction-server/pyproject.toml @@ -0,0 +1,147 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["datajunction_server"] +include = ["alembic/**", "alembic.ini"] + +[tool.pdm] +[tool.pdm.build] +includes = ["dj"] + +[[tool.pdm.autoexport]] +filename = "requirements/docker.txt" +groups = ["default", "uvicorn", "transpilation"] +without-hashes = true + +[[tool.pdm.autoexport]] +filename = "requirements/test.txt" +groups = ["default", "test"] +without-hashes = true + +[project] +name = "datajunction-server" +dynamic = ["version"] +description = "DataJunction server library for running to a DataJunction server" +repository = "https://github.com/DataJunction/dj" +keywords = ["semanticlayer", "metrics"] +dependencies = [ + # Database and ORM + "alembic>=1.10.3", + "SQLAlchemy-Utils<1.0.0,>=0.40.0", + "sqlalchemy>=2", + "psycopg>=3.1.16", + + # FastAPI and web framework + "fastapi>=0.110.0", + "sse-starlette>=1.6.0,<=2.0.0", + + # Authentication and security + "passlib>=1.7.4", + "python-jose>=3.3.0", + "cryptography>=41.0.3", + "bcrypt>=4.0.1", + + # Google APIs + "google-api-python-client>=2.95.0", + "google-auth-httplib2>=0.1.0", + "google-auth-oauthlib>=1.0.0", + + # Instrumentation and monitoring + "opentelemetry-instrumentation-fastapi==0.38b0", + "line-profiler>=4.0.3", + + # Task queues + "celery<6.0.0,>=5.2.7", + + # Data serialization and caching + "fastapi-cache2>=0.2.1", + "cachetools>=5.3.1", + "types-cachetools>=5.3.0.6", + "cachelib<1.0.0,>=0.10.2", + "msgpack<2.0.0,>=1.0.5", + "redis<5.0.0,>=4.5.4", + + # Query parsing + "antlr4-python3-runtime==4.13.1", + + # Utilities and formatting + "requests<=2.29.0,>=2.28.2", + "python-dotenv<1.0.0,>=0.19.0", + "rich<14.0.0,>=13.3.3", + "yarl<2.0.0,>=1.8.2", + "jinja2>=3.1.4", + "python-multipart>=0.0.20", + "nbformat>=5.10.4", + + # GraphQL + "strawberry-graphql>=0.204.0", + + # Data validation + "pydantic<2", +] +requires-python = ">=3.10,<4.0" +readme = "README.md" +license = {text = "MIT"} +classifiers = [ + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent" +] + +[project.optional-dependencies] +uvicorn = [ + "uvicorn[standard]>=0.21.1", +] +transpilation = [ + "sqlglot>=18.0.1", +] + +[project.entry-points.'superset.db_engine_specs'] +dj = 'datajunction_server.superset:DJEngineSpec' + +[tool.hatch.version] +path = "datajunction_server/__about__.py" + +[project.urls] +Homepage = "https://datajunction.io" +Repository = "https://github.com/DataJunction/dj" + +[tool.coverage.run] +source = ['datajunction_server/'] +concurrency = ["thread,greenlet"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = [ + "tests", +] + +[tool.pdm.dev-dependencies] +test = [ + "codespell>=2.2.4", + "freezegun>=1.2.2", + "pre-commit>=3.2.2", + "pylint>=3.0.3", + "pytest-asyncio<=0.22", + "pytest-cov>=4.0.0", + "pytest-integration>=0.2.2", + "pytest-mock>=3.10.0", + "pytest>=7.3.0", + "requests-mock>=1.10.0", + "typing-extensions>=4.5.0", + "pytest-xdist>=3.3.0", + "duckdb==0.8.1", + "testcontainers>=3.7.1", + "httpx>=0.27.0", + "greenlet>=3.0.3", + "gevent>=24.2.1", + "sqlparse<1.0.0,>=0.4.3", +] + +[tool.ruff.lint] +ignore = ["F811"] +exclude = ["datajunction_server/sql/parsing/backends/antlr4.py"] diff --git a/datajunction-server/requirements/docker.txt b/datajunction-server/requirements/docker.txt new file mode 100644 index 000000000..4e7d211e0 --- /dev/null +++ b/datajunction-server/requirements/docker.txt @@ -0,0 +1,152 @@ +# This file is @generated by PDM. +# Please do not edit it manually. + +alembic==1.14.1 +amqp==5.3.1 +antlr4-python3-runtime==4.13.1 +anyio==4.8.0 +asgiref==3.8.1 +astroid==3.3.8 +async-timeout==5.0.1; python_full_version <= "3.11.2" +attrs==24.3.0 +bcrypt==4.2.1 +billiard==4.2.1 +cachelib==0.13.0 +cachetools==5.5.1 +celery==5.4.0 +certifi==2024.12.14 +cffi==1.17.1; platform_python_implementation != "PyPy" +cfgv==3.4.0 +charset-normalizer==3.4.1 +click==8.1.8 +click-didyoumean==0.3.1 +click-plugins==1.1.1 +click-repl==0.3.0 +codespell==2.4.0 +colorama==0.4.6; sys_platform == "win32" or platform_system == "Windows" +coverage[toml]==7.6.10 +cryptography==44.0.0 +deprecated==1.2.15 +dill==0.3.9 +distlib==0.3.9 +docker==7.1.0 +duckdb==0.8.1 +ecdsa==0.19.0 +exceptiongroup==1.2.2; python_version < "3.11" +execnet==2.1.1 +fastapi==0.115.7 +fastapi-cache2==0.2.2 +fastjsonschema==2.21.1 +filelock==3.17.0 +freezegun==1.5.1 +gevent==24.11.1 +google-api-core==2.24.1rc1 +google-api-python-client==2.159.0 +google-auth==2.38.0 +google-auth-httplib2==0.2.0 +google-auth-oauthlib==1.2.1 +googleapis-common-protos==1.66.0 +graphql-core==3.2.5 +greenlet==3.1.1 +h11==0.14.0 +httpcore==1.0.7 +httplib2==0.22.0 +httptools==0.6.4 +httpx==0.28.1 +identify==2.6.6 +idna==3.10 +importlib-metadata==8.5.0 +iniconfig==2.0.0 +isort==5.13.2 +jinja2==3.1.5 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +jupyter-core==5.7.2 +kombu==5.4.2 +line-profiler==4.2.0 +mako==1.3.8 +markdown-it-py==3.0.0 +markupsafe==3.0.2 +mccabe==0.7.0 +mdurl==0.1.2 +msgpack==1.1.0 +multidict==6.1.0 +nbformat==5.10.4 +nodeenv==1.9.1 +oauthlib==3.2.2 +opentelemetry-api==1.29.0 +opentelemetry-instrumentation==0.38b0 +opentelemetry-instrumentation-asgi==0.38b0 +opentelemetry-instrumentation-fastapi==0.38b0 +opentelemetry-semantic-conventions==0.38b0 +opentelemetry-util-http==0.38b0 +packaging==24.2 +passlib==1.7.4 +pendulum==3.0.0 +platformdirs==4.3.6 +pluggy==1.5.0 +pre-commit==4.1.0 +prompt-toolkit==3.0.50 +propcache==0.2.1 +proto-plus==1.26.0rc1 +protobuf==5.29.3 +psycopg==3.2.4 +pyasn1==0.6.1 +pyasn1-modules==0.4.1 +pycparser==2.22; platform_python_implementation != "PyPy" +pydantic==1.10.21 +pygments==2.19.1 +pylint==3.3.3 +pyparsing==3.2.1; python_version > "3.0" +pytest==8.3.4 +pytest-asyncio==0.25.2 +pytest-cov==6.0.0 +pytest-integration==0.2.3 +pytest-mock==3.14.0 +pytest-xdist==3.6.1 +python-dateutil==2.9.0.post0 +python-dotenv==0.21.1 +python-jose==3.3.0 +python-multipart==0.0.20 +pywin32==308; sys_platform == "win32" +pyyaml==6.0.2 +redis==4.6.0 +referencing==0.36.1 +requests==2.29.0 +requests-mock==1.12.1 +requests-oauthlib==2.0.0 +rich==13.9.4 +rpds-py==0.22.3 +rsa==4.9 +setuptools==75.8.0 +six==1.17.0 +sniffio==1.3.1 +sqlalchemy==2.0.37 +sqlalchemy-utils==0.41.2 +sqlglot==26.3.6 +sqlparse==0.5.3 +sse-starlette==2.2.1 +starlette==0.45.2 +strawberry-graphql==0.258.0 +testcontainers==4.9.1 +time-machine==2.16.0; implementation_name != "pypy" +tomli==2.2.1; python_version < "3.11" +tomlkit==0.13.2 +traitlets==5.14.3 +types-cachetools==5.5.0.20240820 +typing-extensions==4.12.2 +tzdata==2025.1 +uritemplate==4.1.1 +urllib3==1.26.20 +uvicorn[standard]==0.34.0 +uvloop==0.21.0; (sys_platform != "cygwin" and sys_platform != "win32") and platform_python_implementation != "PyPy" +vine==5.1.0 +virtualenv==20.29.1 +watchfiles==1.0.4 +wcwidth==0.2.13 +websockets==14.2 +wrapt==1.17.2 +yarl==1.18.3 +zipp==3.21.0 +zope-event==5.0 +zope-interface==7.2 diff --git a/datajunction-server/requirements/test.txt b/datajunction-server/requirements/test.txt new file mode 100644 index 000000000..ff96b6f3b --- /dev/null +++ b/datajunction-server/requirements/test.txt @@ -0,0 +1,147 @@ +# This file is @generated by PDM. +# Please do not edit it manually. + +alembic==1.14.1 +amqp==5.3.1 +antlr4-python3-runtime==4.13.1 +anyio==4.8.0 +asgiref==3.8.1 +astroid==3.3.8 +async-timeout==5.0.1; python_full_version <= "3.11.2" +attrs==25.1.0 +bcrypt==4.2.1 +billiard==4.2.1 +cachelib==0.13.0 +cachetools==5.5.1 +celery==5.4.0 +certifi==2024.12.14 +cffi==1.17.1; platform_python_implementation != "PyPy" +cfgv==3.4.0 +charset-normalizer==3.4.1 +click==8.1.8 +click-didyoumean==0.3.1 +click-plugins==1.1.1 +click-repl==0.3.0 +codespell==2.4.0 +colorama==0.4.6; sys_platform == "win32" or platform_system == "Windows" +coverage[toml]==7.6.10 +cryptography==44.0.0 +deprecated==1.2.17 +dill==0.3.9 +distlib==0.3.9 +docker==7.1.0 +duckdb==0.8.1 +ecdsa==0.19.0 +exceptiongroup==1.2.2; python_version < "3.11" +execnet==2.1.1 +fastapi==0.115.7 +fastapi-cache2==0.2.2 +fastjsonschema==2.21.1 +filelock==3.17.0 +freezegun==1.5.1 +gevent==24.11.1 +google-api-core==2.24.1rc1 +google-api-python-client==2.159.0 +google-auth==2.38.0 +google-auth-httplib2==0.2.0 +google-auth-oauthlib==1.2.1 +googleapis-common-protos==1.66.0 +graphql-core==3.2.5 +greenlet==3.1.1 +h11==0.14.0 +httpcore==1.0.7 +httplib2==0.22.0 +httpx==0.28.1 +identify==2.6.6 +idna==3.10 +importlib-metadata==8.5.0 +iniconfig==2.0.0 +isort==5.13.2 +jinja2==3.1.5 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +jupyter-core==5.7.2 +kombu==5.4.2 +line-profiler==4.2.0 +mako==1.3.8 +markdown-it-py==3.0.0 +markupsafe==3.0.2 +mccabe==0.7.0 +mdurl==0.1.2 +msgpack==1.1.0 +multidict==6.1.0 +nbformat==5.10.4 +nodeenv==1.9.1 +oauthlib==3.2.2 +opentelemetry-api==1.29.0 +opentelemetry-instrumentation==0.38b0 +opentelemetry-instrumentation-asgi==0.38b0 +opentelemetry-instrumentation-fastapi==0.38b0 +opentelemetry-semantic-conventions==0.38b0 +opentelemetry-util-http==0.38b0 +packaging==24.2 +passlib==1.7.4 +pendulum==3.0.0 +platformdirs==4.3.6 +pluggy==1.5.0 +pre-commit==4.1.0 +prompt-toolkit==3.0.50 +propcache==0.2.1 +proto-plus==1.26.0rc1 +protobuf==5.29.3 +psycopg==3.2.4 +pyasn1==0.6.1 +pyasn1-modules==0.4.1 +pycparser==2.22; platform_python_implementation != "PyPy" +pydantic==1.10.21 +pygments==2.19.1 +pylint==3.3.3 +pyparsing==3.2.1; python_version > "3.0" +pytest==8.3.4 +pytest-asyncio==0.21.2 +pytest-cov==6.0.0 +pytest-integration==0.2.3 +pytest-mock==3.14.0 +pytest-xdist==3.6.1 +python-dateutil==2.9.0.post0 +python-dotenv==0.21.1 +python-jose==3.3.0 +python-multipart==0.0.20 +pywin32==308; sys_platform == "win32" +pyyaml==6.0.2 +redis==4.6.0 +referencing==0.36.2 +requests==2.29.0 +requests-mock==1.12.1 +requests-oauthlib==2.0.0 +rich==13.9.4 +rpds-py==0.22.3 +rsa==4.9 +setuptools==75.8.0 +six==1.17.0 +sniffio==1.3.1 +sqlalchemy==2.0.37 +sqlalchemy-utils==0.41.2 +sqlparse==0.5.3 +sse-starlette==2.0.0 +starlette==0.45.3 +strawberry-graphql==0.258.0 +testcontainers==4.9.1 +time-machine==2.16.0; implementation_name != "pypy" +tomli==2.2.1; python_version < "3.11" +tomlkit==0.13.2 +traitlets==5.14.3 +types-cachetools==5.5.0.20240820 +typing-extensions==4.12.2 +tzdata==2025.1 +uritemplate==4.1.1 +urllib3==1.26.20 +uvicorn==0.34.0 +vine==5.1.0 +virtualenv==20.29.1 +wcwidth==0.2.13 +wrapt==1.17.2 +yarl==1.18.3 +zipp==3.21.0 +zope-event==5.0 +zope-interface==7.2 diff --git a/datajunction-server/scripts/docs-snippets.js b/datajunction-server/scripts/docs-snippets.js new file mode 100644 index 000000000..483cc2b61 --- /dev/null +++ b/datajunction-server/scripts/docs-snippets.js @@ -0,0 +1,153 @@ +const { DJClient } = require("datajunction") + +const dj = new DJClient("http://localhost:8000") + +Promise.resolve().then( + () => { + return dj.namespaces.create("default").then(data => console.log(data)) + } +).then( + () => { + return dj.catalogs.create({"name": "warehouse"}).then(data => console.log(data)) + } +).then( + () => { + return dj.engines.create({ + name: "duckdb", + version: "0.7.1", + dialect: "spark", + }).then(data => console.log(data)) + } +).then( + () => { + return dj.catalogs.addEngine("warehouse", "duckdb", "0.7.1").then(data => console.log(data)) + } +).then( + () => { + return dj.sources.create({ + name: "default.repair_orders", + description: "Repair orders", + mode: "published", + catalog: "warehouse", + schema_: "roads", + table: "repair_orders", + columns: [ + {name: "repair_order_id", type: "int"}, + {name: "municipality_id", type: "string"}, + {name: "hard_hat_id", type: "int"}, + {name: "order_date", type: "timestamp"}, + {name: "required_date", type: "timestamp"}, + {name: "dispatched_date", type: "timestamp"}, + {name: "dispatcher_id", type: "int"} + ] + }).then(data => console.log(data)) + } +).then( + () => { + return dj.transforms.create( + { + name: "default.repair_orders_w_dispatchers", + mode: "published", + description: "Repair orders that have a dispatcher", + query: ` + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + dispatcher_id + FROM default.repair_orders + WHERE dispatcher_id IS NOT NULL + ` + } + ).then(data => console.log(data)) + } +).then( + () => { + return dj.sources.create( + { + name: "default.dispatchers", + mode: "published", + description: "Contact list for dispatchers", + catalog: "warehouse", + schema_: "roads", + table: "dispatchers", + columns: [ + {name: "dispatcher_id", type: "int"}, + {name: "company_name", type: "string"}, + {name: "phone", type: "string"} + ] + } + ).then(data => console.log(data)) + } +).then( + () => { + return dj.dimensions.create( + { + name: "default.all_dispatchers", + mode: "published", + description: "All dispatchers", + query: ` + SELECT + dispatcher_id, + company_name, + phone + FROM default.dispatchers + `, + primary_key: ["dispatcher_id"] + } + ).then(data => console.log(data)) + } +).then( + () => { + return dj.dimensions.link("default.repair_orders", "dispatcher_id", "default.all_dispatchers", "dispatcher_id").then(data => console.log(data)) + } +).then( + () => { + return dj.metrics.create( + { + name: "default.num_repair_orders", + description: "Number of repair orders", + mode: "published", + query: ` + SELECT + count(repair_order_id) as num_repair_orders + FROM default.repair_orders + ` + } + ).then(data => console.log(data)) + } +).then( + () => { + return dj.cubes.create( + { + name: "default.repairs_cube", + mode: "published", + display_name: "Repairs for each company", + description: "Cube of the number of repair orders grouped by dispatcher companies", + metrics: [ + "default.num_repair_orders" + ], + dimensions: [ + "default.all_dispatchers.company_name" + ], + filters: ["default.all_dispatchers.company_name IS NOT NULL"] + } + ).then(data => console.log(data)) + } +).then( + () => { + return dj.sql.get( + metrics=["default.num_repair_orders"], + dimensions=["default.all_dispatchers.company_name"], + filters=["default.all_dispatchers.company_name IS NOT NULL"] + ).then(data => console.log(data)) + } +).then( + () => { + return dj.data.get( + metrics=["default.num_repair_orders"], + dimensions=["default.all_dispatchers.company_name"], + filters=["default.all_dispatchers.company_name IS NOT NULL"] + ).then(data => console.log(data)) + } +) diff --git a/datajunction-server/scripts/docs-snippets.sh b/datajunction-server/scripts/docs-snippets.sh new file mode 100755 index 000000000..71c5c2863 --- /dev/null +++ b/datajunction-server/scripts/docs-snippets.sh @@ -0,0 +1,149 @@ +# Creating a namespace +printf "\n" +curl -X POST http://localhost:8000/namespaces/default/ + +# Creating a catalog +printf "\n" +curl -X POST http://localhost:8000/catalogs/ \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "warehouse" +}' + +# Creating an engine +printf "\n" +curl -X 'POST' \ + 'http://localhost:8000/engines/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "duckdb", + "version": "0.7.1", + "dialect": "spark" +}' + +# Attaching an engine to a catalog +printf "\n" +curl -X 'POST' \ + 'http://localhost:8000/catalogs/warehouse/engines/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '[{ + "name": "duckdb", + "version": "0.7.1" +}]' + +# Creating a source node +printf "\n" +curl -X POST http://localhost:8000/nodes/source/ \ +-H 'Content-Type: application/json' \ +-d '{ + "name": "default.repair_orders", + "description": "Repair orders", + "mode": "published", + "catalog": "warehouse", + "schema_": "roads", + "table": "repair_orders", + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "municipality_id", "type": "string"}, + {"name": "hard_hat_id", "type": "int"}, + {"name": "order_date", "type": "timestamp"}, + {"name": "required_date", "type": "timestamp"}, + {"name": "dispatched_date", "type": "timestamp"}, + {"name": "dispatcher_id", "type": "int"} + ] +}' + +# Creating a transform node +printf "\n" +curl -X POST http://localhost:8000/nodes/transform/ \ +-H 'Content-Type: application/json' \ +-d '{ + "name": "default.repair_orders_w_dispatchers", + "description": "Repair orders that have a dispatcher", + "mode": "published", + "query": "SELECT repair_order_id, municipality_id, hard_hat_id, dispatcher_id FROM default.repair_orders WHERE dispatcher_id IS NOT NULL" +}' + +# Creating a dimension node (part 1, creating a source first) +printf "\n" +curl -X 'POST' \ + 'http://localhost:8000/nodes/source/' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "columns": [ + {"name": "dispatcher_id", "type": "int"}, + {"name": "company_name", "type": "string"}, + {"name": "phone", "type": "string"} + ], + "description": "Contact list for dispatchers", + "mode": "published", + "name": "default.dispatchers", + "catalog": "warehouse", + "schema_": "roads", + "table": "dispatchers" + }' + +# Creating a dimension node (part 2, creating the dimension node) +printf "\n" +curl -X POST http://localhost:8000/nodes/dimension/ \ +-H 'Content-Type: application/json' \ +-d '{ + "name": "default.all_dispatchers", + "description": "All dispatchers", + "mode": "published", + "query": "SELECT dispatcher_id, company_name, phone FROM default.dispatchers", + "primary_key": ["dispatcher_id"] +}' + +# Creating a dimension node (part 3, linking a dimension to a column) +printf "\n" +curl -X 'POST' \ + 'http://localhost:8000/nodes/default.repair_orders/columns/dispatcher_id/?dimension=default.all_dispatchers&dimension_column=dispatcher_id' \ + -H 'accept: application/json' + +# Creating a metric node +printf "\n" +curl -X POST http://localhost:8000/nodes/metric/ \ +-H 'Content-Type: application/json' \ +-d '{ + "name": "default.num_repair_orders", + "description": "Number of repair orders", + "mode": "published", + "query": "SELECT count(repair_order_id) as num_repair_orders FROM default.repair_orders" +}' + +# Creating a cube +printf "\n" +curl -X POST http://localhost:8000/nodes/cube/ \ +-H 'Content-Type: application/json' \ +-d '{ + "name": "default.repairs_cube", + "mode": "published", + "display_name": "Repairs for each company", + "description": "Cube of the number of repair orders grouped by dispatcher companies", + "metrics": [ + "default.num_repair_orders" + ], + "dimensions": [ + "default.all_dispatchers.company_name" + ], + "filters": ["default.all_dispatchers.company_name IS NOT NULL"], +}' + +# Get SQL for metrics +printf "\n" +curl -X 'GET' \ + 'http://localhost:8000/sql/?metrics=default.num_repair_orders&dimensions=default.all_dispatchers.company_name&filters=default.all_dispatchers.company_name%20IS%20NOT%20NULL' \ + -H 'accept: application/json' + +# Get data for metrics +printf "\n" +curl -X 'GET' \ + 'http://localhost:8000/data/?metrics=default.num_repair_orders&dimensions=default.all_dispatchers.company_name&filters=default.all_dispatchers.company_name%20IS%20NOT%20NULL' \ + -H 'accept: application/json' + +printf "\n" diff --git a/datajunction-server/scripts/generate-openapi.py b/datajunction-server/scripts/generate-openapi.py new file mode 100755 index 000000000..71857035a --- /dev/null +++ b/datajunction-server/scripts/generate-openapi.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import argparse +import json + +from datajunction_server.api.main import app + + +def save_openapi_spec(f: str): + spec = app.openapi() + with open(f, "w") as outfile: + outfile.write(json.dumps(spec, indent=4)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Generate a file containing the OpenAPI spec for a DJ server", + ) + parser.add_argument( + "-o", + "--output-file", + dest="filename", + required=True, + metavar="FILE", + ) + args = vars(parser.parse_args()) + save_openapi_spec(f=args["filename"]) diff --git a/datajunction-server/tests/__init__.py b/datajunction-server/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/tests/api/__init__.py b/datajunction-server/tests/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/tests/api/access_test.py b/datajunction-server/tests/api/access_test.py new file mode 100644 index 000000000..6d7749c05 --- /dev/null +++ b/datajunction-server/tests/api/access_test.py @@ -0,0 +1,88 @@ +""" +Tests for the data API. +""" + +import pytest +from httpx import AsyncClient + +from datajunction_server.api.main import app +from datajunction_server.internal.access.authorization import validate_access +from datajunction_server.models import access + + +class TestDataAccessControl: + """ + Test the data access control. + """ + + @pytest.mark.asyncio + async def test_get_metric_data_unauthorized( + self, + module__client_with_examples: AsyncClient, + ) -> None: + """ + Test retrieving data for a metric + """ + + def validate_access_override(): + def _validate_access(access_control: access.AccessControl): + access_control.deny_all() + + return _validate_access + + app.dependency_overrides[validate_access] = validate_access_override + response = await module__client_with_examples.get("/data/basic.num_comments/") + data = response.json() + assert "Authorization of User `dj` for this request failed" in data["message"] + assert "read:node/basic.num_comments" in data["message"] + assert "read:node/basic.source.comments" in data["message"] + assert response.status_code == 403 + app.dependency_overrides.clear() + + @pytest.mark.asyncio + async def test_sql_with_filters_orderby_no_access( + self, + module__client_with_examples: AsyncClient, + ): + """ + Test ``GET /sql/{node_name}/`` with various filters and dimensions using a + version of the DJ roads database with namespaces. + """ + + def validate_access_override(): + def _validate_access(access_control: access.AccessControl): + access_control.deny_all() + + return _validate_access + + app.dependency_overrides[validate_access] = validate_access_override + + node_name = "foo.bar.num_repair_orders" + dimensions = [ + "foo.bar.hard_hat.city", + "foo.bar.hard_hat.last_name", + "foo.bar.dispatcher.company_name", + "foo.bar.municipality_dim.local_region", + ] + filters = [ + "foo.bar.repair_orders.dispatcher_id=1", + "foo.bar.hard_hat.state != 'AZ'", + "foo.bar.dispatcher.phone = '4082021022'", + "foo.bar.repair_orders.order_date >= '2020-01-01'", + ] + orderby = ["foo.bar.hard_hat.last_name"] + response = await module__client_with_examples.get( + f"/sql/{node_name}/", + params={"dimensions": dimensions, "filters": filters, "orderby": orderby}, + ) + data = response.json() + assert sorted(list(data["message"])) == sorted( + list( + "Authorization of User `dj` for this request failed." + "\nThe following requests were denied:\nread:node/foo.bar.dispatcher, " + "read:node/foo.bar.repair_orders, read:node/foo.bar.municipality_dim, " + "read:node/foo.bar.num_repair_orders, read:node/foo.bar.hard_hat.", + ), + ) + assert data["errors"][0]["code"] == 500 + app.dependency_overrides.clear() diff --git a/datajunction-server/tests/api/attributes_test.py b/datajunction-server/tests/api/attributes_test.py new file mode 100644 index 000000000..f1ceff951 --- /dev/null +++ b/datajunction-server/tests/api/attributes_test.py @@ -0,0 +1,112 @@ +""" +Tests for the attributes API. +""" + +from unittest.mock import ANY + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_adding_new_attribute( + module__client: AsyncClient, +) -> None: + """ + Test adding an attribute. + """ + response = await module__client.post( + "/attributes/", + json={ + "namespace": "custom", + "name": "internal", + "description": "Column for internal use only", + "allowed_node_types": ["source"], + }, + ) + data = response.json() + assert response.status_code == 201 + assert data == { + "id": ANY, + "namespace": "custom", + "name": "internal", + "description": "Column for internal use only", + "uniqueness_scope": [], + "allowed_node_types": ["source"], + } + + response = await module__client.post( + "/attributes/", + json={ + "namespace": "custom", + "name": "internal", + "description": "Column for internal use only", + "allowed_node_types": ["source"], + }, + ) + assert response.status_code == 409 + data = response.json() + assert data == { + "message": "Attribute type `internal` already exists!", + "errors": [], + "warnings": [], + } + + response = await module__client.post( + "/attributes/", + json={ + "namespace": "system", + "name": "logging", + "description": "Column for logging use only", + "allowed_node_types": ["source"], + }, + ) + assert response.status_code == 422 + data = response.json() + assert data == { + "message": "Cannot use `system` as the attribute type namespace as it is reserved.", + "errors": [], + "warnings": [], + } + + +@pytest.mark.asyncio +async def test_list_system_attributes( + module__client: AsyncClient, +) -> None: + """ + Test listing attributes. These should contain the default attributes. + """ + response = await module__client.get("/attributes/") + assert response.status_code == 200 + data = response.json() + data = { + type_["name"]: {k: type_[k] for k in (type_.keys() - {"id"})} for type_ in data + } + data_for_system = {k: v for k, v in data.items() if v["namespace"] == "system"} + assert data_for_system == { + "primary_key": { + "namespace": "system", + "uniqueness_scope": [], + "allowed_node_types": ["source", "transform", "dimension"], + "name": "primary_key", + "description": "Points to a column which is part of the primary key of the node", + }, + "dimension": { + "namespace": "system", + "uniqueness_scope": [], + "allowed_node_types": ["source", "transform"], + "name": "dimension", + "description": "Points to a dimension attribute column", + }, + "hidden": { + "namespace": "system", + "uniqueness_scope": [], + "allowed_node_types": ["dimension"], + "name": "hidden", + "description": ( + "Points to a dimension column that's not useful " + "for end users and should be hidden" + ), + }, + } diff --git a/datajunction-server/tests/api/catalog_test.py b/datajunction-server/tests/api/catalog_test.py new file mode 100644 index 000000000..2a171f41c --- /dev/null +++ b/datajunction-server/tests/api/catalog_test.py @@ -0,0 +1,412 @@ +""" +Tests for the catalog API. +""" + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_catalog_adding_a_new_catalog( + module__client: AsyncClient, +) -> None: + """ + Test adding a catalog + """ + response = await module__client.post( + "/catalogs/", + json={ + "name": "dev-1", + }, + ) + data = response.json() + assert response.status_code == 201 + assert data == {"name": "dev-1", "engines": []} + + +@pytest.mark.asyncio +async def test_catalog_list( + module__client: AsyncClient, +) -> None: + """ + Test listing catalogs + """ + response = await module__client.post( + "/engines/", + json={ + "name": "spark", + "version": "3.3.1", + "dialect": "spark", + }, + ) + assert response.status_code == 201 + + response = await module__client.post( + "/catalogs/", + json={ + "name": "cat-dev", + "engines": [ + { + "name": "spark", + "version": "3.3.1", + "dialect": "spark", + }, + ], + }, + ) + assert response.status_code == 201 + + response = await module__client.post( + "/catalogs/", + json={ + "name": "cat-test", + }, + ) + assert response.status_code == 201 + + response = await module__client.post( + "/catalogs/", + json={ + "name": "cat-prod", + }, + ) + assert response.status_code == 201 + + response = await module__client.get("/catalogs/") + assert response.status_code == 200 + filtered_response = [ + cat for cat in response.json() if cat["name"].startswith("cat-") + ] + assert sorted(filtered_response, key=lambda v: v["name"]) == sorted( + [ + { + "name": "cat-dev", + "engines": [ + { + "name": "spark", + "version": "3.3.1", + "uri": None, + "dialect": "spark", + }, + ], + }, + {"name": "cat-test", "engines": []}, + {"name": "cat-prod", "engines": []}, + ], + key=lambda v: v["name"], # type: ignore + ) + + +@pytest.mark.asyncio +async def test_catalog_get_catalog( + module__client: AsyncClient, +) -> None: + """ + Test getting a catalog + """ + response = await module__client.post( + "/engines/", + json={ + "name": "one-spark", + "version": "3.3.1", + "dialect": "spark", + }, + ) + assert response.status_code == 201 + + response = await module__client.post( + "/catalogs/", + json={ + "name": "one-dev", + "engines": [ + { + "name": "one-spark", + "version": "3.3.1", + "dialect": "spark", + }, + ], + }, + ) + assert response.status_code == 201 + + response = await module__client.get( + "/catalogs/one-dev", + ) + assert response.status_code == 200 + data = response.json() + assert data == { + "name": "one-dev", + "engines": [ + {"name": "one-spark", "uri": None, "version": "3.3.1", "dialect": "spark"}, + ], + } + + +@pytest.mark.asyncio +async def test_catalog_adding_a_new_catalog_with_engines( + module__client: AsyncClient, +) -> None: + """ + Test adding a catalog with engines + """ + response = await module__client.post( + "/engines/", + json={ + "name": "two-spark", + "uri": None, + "version": "3.3.1", + "dialect": "spark", + }, + ) + data = response.json() + assert response.status_code == 201 + + response = await module__client.post( + "/catalogs/", + json={ + "name": "two-dev", + "engines": [ + { + "name": "two-spark", + "version": "3.3.1", + "dialect": "spark", + }, + ], + }, + ) + data = response.json() + assert response.status_code == 201 + assert data == { + "name": "two-dev", + "engines": [ + { + "name": "two-spark", + "uri": None, + "version": "3.3.1", + "dialect": "spark", + }, + ], + } + + +@pytest.mark.asyncio +async def test_catalog_adding_a_new_catalog_then_attaching_engines( + module__client: AsyncClient, +) -> None: + """ + Test adding a catalog then attaching a catalog + """ + response = await module__client.post( + "/engines/", + json={ + "name": "spark-3", + "uri": None, + "version": "3.3.1", + "dialect": "spark", + }, + ) + data = response.json() + assert response.status_code == 201 + + response = await module__client.post( + "/catalogs/", + json={ + "name": "dev-3", + }, + ) + assert response.status_code == 201 + + await module__client.post( + "/catalogs/dev-3/engines/", + json=[ + { + "name": "spark-3", + "version": "3.3.1", + "dialect": "spark", + }, + ], + ) + + response = await module__client.get("/catalogs/dev-3/") + data = response.json() + assert data == { + "name": "dev-3", + "engines": [ + { + "name": "spark-3", + "uri": None, + "version": "3.3.1", + "dialect": "spark", + }, + ], + } + + +@pytest.mark.asyncio +async def test_catalog_adding_without_duplicating( + module__client: AsyncClient, +) -> None: + """ + Test adding a catalog and having existing catalogs not re-added + """ + response = await module__client.post( + "/engines/", + json={ + "name": "spark-4", + "uri": None, + "version": "2.4.4", + "dialect": "spark", + }, + ) + data = response.json() + assert response.status_code == 201 + + response = await module__client.post( + "/engines/", + json={ + "name": "spark-4", + "version": "3.3.0", + "dialect": "spark", + }, + ) + data = response.json() + assert response.status_code == 201 + + response = await module__client.post( + "/engines/", + json={ + "name": "spark-4", + "version": "3.3.1", + "dialect": "spark", + }, + ) + data = response.json() + assert response.status_code == 201 + + response = await module__client.post( + "/catalogs/", + json={ + "name": "dev-4", + "dialect": "spark", + }, + ) + assert response.status_code == 201 + + response = await module__client.post( + "/catalogs/dev-4/engines/", + json=[ + { + "name": "spark-4", + "version": "2.4.4", + "dialect": "spark", + }, + { + "name": "spark-4", + "version": "3.3.0", + "dialect": "spark", + }, + { + "name": "spark-4", + "version": "3.3.1", + "dialect": "spark", + }, + ], + ) + assert response.status_code == 201 + + response = await module__client.post( + "/catalogs/dev-4/engines/", + json=[ + { + "name": "spark-4", + "version": "2.4.4", + "dialect": "spark", + }, + { + "name": "spark-4", + "version": "3.3.0", + "dialect": "spark", + }, + { + "name": "spark-4", + "version": "3.3.1", + "dialect": "spark", + }, + ], + ) + assert response.status_code == 201 + data = response.json() + assert data == { + "name": "dev-4", + "engines": [ + { + "name": "spark-4", + "uri": None, + "version": "2.4.4", + "dialect": "spark", + }, + { + "name": "spark-4", + "uri": None, + "version": "3.3.0", + "dialect": "spark", + }, + { + "name": "spark-4", + "uri": None, + "version": "3.3.1", + "dialect": "spark", + }, + ], + } + + +@pytest.mark.asyncio +async def test_catalog_raise_on_adding_a_new_catalog_with_nonexistent_engines( + module__client: AsyncClient, +) -> None: + """ + Test raising an error when adding a catalog with engines that do not exist + """ + response = await module__client.post( + "/catalogs/", + json={ + "name": "dev", + "engines": [ + { + "name": "spark", + "version": "4.0.0", + "dialect": "spark", + }, + ], + }, + ) + data = response.json() + assert response.status_code == 404 + assert data == {"detail": "Engine not found: `spark` version `4.0.0`"} + + +@pytest.mark.asyncio +async def test_catalog_raise_on_catalog_already_exists( + module__client: AsyncClient, +) -> None: + """ + Test raise on catalog already exists + """ + response = await module__client.post( + "/catalogs/", + json={ + "name": "dev", + }, + ) + assert response.status_code == 201 + + response = await module__client.post( + "/catalogs/", + json={ + "name": "dev", + }, + ) + data = response.json() + assert response.status_code == 409 + assert data == {"detail": "Catalog already exists: `dev`"} diff --git a/datajunction-server/tests/api/client_test.py b/datajunction-server/tests/api/client_test.py new file mode 100644 index 000000000..ec0c97747 --- /dev/null +++ b/datajunction-server/tests/api/client_test.py @@ -0,0 +1,396 @@ +""" +Tests for client code generator. +""" + +import os +from pathlib import Path + +import pytest +from httpx import AsyncClient + +TEST_DIR = os.path.dirname(os.path.abspath(__file__)) + + +@pytest.fixture +def load_expected_file(): + """ + Loads expected fixture file + """ + + def _load(filename: str): + expected_path = TEST_DIR / Path("files/client_test") + with open(expected_path / filename, encoding="utf-8") as fe: + return fe.read().strip() + + return _load + + +def trim_trailing_whitespace(string: str) -> str: + """Trim trailing whitespace on each line""" + return "\n".join([chunk.rstrip() for chunk in string.split("\n")]).strip() + + +@pytest.mark.asyncio +async def test_generated_python_client_code_new_source( + module__client_with_roads: AsyncClient, + load_expected_file, +): + """ + Test generating Python client code for creating a new source + """ + expected = load_expected_file("register_table.txt") + response = await module__client_with_roads.get( + "/datajunction-clients/python/new_node/default.repair_order_details" + "?include_client_setup=false", + ) + assert trim_trailing_whitespace(response.json()) == expected + response = await module__client_with_roads.get( + "/datajunction-clients/python/new_node/default.repair_order_details" + "?include_client_setup=false&replace_namespace=%7Bnamespace%7D", + ) + assert trim_trailing_whitespace(response.json()) == expected + + +@pytest.mark.asyncio +async def test_generated_python_client_code_new_transform( + module__client_with_roads: AsyncClient, + load_expected_file, +): + """ + Test generating Python client code for creating a new transform + """ + response = await module__client_with_roads.get( + "/datajunction-clients/python/new_node/default.regional_level_agg" + "?include_client_setup=false", + ) + assert ( + trim_trailing_whitespace(response.json()).strip() + == load_expected_file("create_transform.regional_level_agg.txt").strip() + ) + response = await module__client_with_roads.get( + "/datajunction-clients/python/new_node/default.regional_level_agg" + "?include_client_setup=false&replace_namespace=%7Bnamespace%7D", + ) + assert ( + trim_trailing_whitespace(response.json()).strip() + == load_expected_file( + "create_transform.regional_level_agg.namespace.txt", + ).strip() + ) + + +@pytest.mark.asyncio +async def test_generated_python_client_code_new_dimension( + module__client_with_roads: AsyncClient, + load_expected_file, +): + """ + Test generating Python client code for creating a new dimension + """ + response = await module__client_with_roads.get( + "/datajunction-clients/python/new_node/default.repair_order" + "?include_client_setup=false", + ) + assert ( + trim_trailing_whitespace(response.json()).strip() + == load_expected_file("create_dimension.repair_order.txt").strip() + ) + + response = await module__client_with_roads.get( + "/datajunction-clients/python/new_node/default.repair_order" + "?include_client_setup=false&replace_namespace=%7Bnamespace%7D", + ) + assert ( + trim_trailing_whitespace(response.json()).strip() + == load_expected_file("create_dimension.repair_order.namespace.txt").strip() + ) + + +@pytest.mark.asyncio +async def test_generated_python_client_code_new_metric( + module__client_with_roads: AsyncClient, + load_expected_file, +): + """ + Test generating Python client code for creating a new metric + """ + response = await module__client_with_roads.get( + "/datajunction-clients/python/new_node/default.num_repair_orders" + "?include_client_setup=false", + ) + assert ( + trim_trailing_whitespace(response.json()).strip() + == load_expected_file("create_metric.num_repair_orders.txt").strip() + ) + + response = await module__client_with_roads.get( + "/datajunction-clients/python/new_node/default.num_repair_orders" + "?include_client_setup=false&replace_namespace=%7Bnamespace%7D", + ) + assert ( + trim_trailing_whitespace(response.json()).strip() + == load_expected_file("create_metric.num_repair_orders.namespace.txt").strip() + ) + + +@pytest.mark.asyncio +async def test_generated_python_client_code_new_cube( + module__client_with_roads: AsyncClient, + load_expected_file, +): + """ + Test generating Python client code for creating a new cube + """ + await module__client_with_roads.post( + "/nodes/cube/", + json={ + "metrics": ["default.num_repair_orders", "default.total_repair_cost"], + "dimensions": [ + "default.hard_hat.country", + "default.hard_hat.city", + ], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.repairs_cube", + }, + ) + response = await module__client_with_roads.get( + "/datajunction-clients/python/new_node/default.repairs_cube" + "?include_client_setup=false", + ) + assert ( + trim_trailing_whitespace(response.json()).strip() + == load_expected_file("create_cube.repairs_cube.txt").strip() + ) + response = await module__client_with_roads.get( + "/datajunction-clients/python/new_node/default.repairs_cube" + "?include_client_setup=false&replace_namespace=%7Bnamespace%7D", + ) + assert ( + trim_trailing_whitespace(response.json()).strip() + == load_expected_file( + "create_cube.repairs_cube.namespace.txt", + ).strip() + ) + + +@pytest.mark.asyncio +async def test_generated_python_client_code_link_dimension( + module__client_with_roads: AsyncClient, + load_expected_file, +): + """ + Test generating Python client code for creating a new dimension + """ + response = await module__client_with_roads.get( + "/datajunction-clients/python/dimension_links/default.repair_orders/" + "?include_client_setup=false", + ) + assert ( + response.json().strip() + == load_expected_file("dimension_links.repair_orders.txt").strip() + ) + # When replace_namespace is set, verify that the namespaces are replaced in the + # dimension links' join SQL + response = await module__client_with_roads.get( + "/datajunction-clients/python/dimension_links/default.repair_orders/" + "?include_client_setup=false&replace_namespace=%7Bnamespace%7D", + ) + assert ( + response.json().strip() + == load_expected_file( + "dimension_links.repair_orders.namespace.txt", + ).strip() + ) + + +@pytest.mark.asyncio +async def test_include_client_setup( + module__client_with_roads: AsyncClient, + load_expected_file, +): + """ + Generate create new node python client code with client setup included. + """ + response = await module__client_with_roads.get( + "/datajunction-clients/python/new_node/default.num_repair_orders" + "?include_client_setup=true", + ) + assert ( + trim_trailing_whitespace(response.json()).strip() + == load_expected_file("include_client_setup.txt").strip() + ) + + +@pytest.mark.asyncio +async def test_export_namespace_as_notebook( + module__client_with_roads: AsyncClient, + load_expected_file, +): + """ + Verify exporting all nodes in a namespace as a notebook. + """ + response = await module__client_with_roads.post( + "/nodes/default.repair_order_details/columns/repair_type_id/attributes/", + json=[ + { + "namespace": "system", + "name": "dimension", + }, + ], + ) + response = await module__client_with_roads.get( + "/datajunction-clients/python/notebook?namespace=default", + ) + assert ( + response.headers["content-disposition"] == 'attachment; filename="export.ipynb"' + ) + notebook = response.json() + assert len(notebook["cells"]) >= 50 + # Intro cell + assert notebook["cells"][0]["cell_type"] == "markdown" + assert ( + "## DJ Namespace Export\n\nExported `default`" in notebook["cells"][0]["source"] + ) + + # Client setup cell + assert notebook["cells"][1]["cell_type"] == "code" + assert "from datajunction import" in notebook["cells"][1]["source"] + + # Documenting what nodes are being upserted cell + assert notebook["cells"][2]["cell_type"] == "markdown" + assert "### Upserting Nodes:" in notebook["cells"][2]["source"] + + # Namespace mapping configuration cell + assert notebook["cells"][3]["cell_type"] == "code" + assert ( + notebook["cells"][3]["source"] + == """# A mapping from current namespaces to new namespaces +# Note: Editing the mapping will result in the nodes under that namespace getting +# copied to the new namespace + +NAMESPACE_MAPPING = { + "default": "default", +}""" + ) + + # Registering table + assert notebook["cells"][4]["cell_type"] == "code" + assert ( + notebook["cells"][4]["source"] + == load_expected_file("register_table.txt").strip() + ) + + # Linking dimensions for table + assert ( + notebook["cells"][5]["source"] + == "Linking dimensions for source node `default.repair_order_details`:" + ) + assert ( + notebook["cells"][6]["source"] + == load_expected_file( + "notebook.link_dimension.txt", + ).strip() + ) + # Check column attributes + assert ( + notebook["cells"][7]["source"] + == load_expected_file( + "notebook.set_attribute.txt", + ).strip() + ) + + +@pytest.mark.asyncio +async def test_export_cube_as_notebook( + module__client_with_roads: AsyncClient, + load_expected_file, +): + """ + Verify exporting all nodes relevant for a cube as a notebook. + """ + await module__client_with_roads.post( + "/nodes/cube/", + json={ + "metrics": ["default.num_repair_orders", "default.total_repair_cost"], + "dimensions": [ + "default.hard_hat.country", + "default.hard_hat.city", + ], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.roads_cube", + }, + ) + response = await module__client_with_roads.get( + "/datajunction-clients/python/notebook?cube=default.roads_cube", + ) + assert ( + response.headers["content-disposition"] == 'attachment; filename="export.ipynb"' + ) + notebook = response.json() + assert len(notebook["cells"]) == 10 + + # Intro cell + assert notebook["cells"][0]["cell_type"] == "markdown" + assert ( + "## DJ Cube Export\n\nExported `default.roads_cube` v1.0" + in notebook["cells"][0]["source"] + ) + + # Documenting which nodes are getting exported + assert ( + notebook["cells"][2]["source"] + == """### Upserting Nodes: +* default.repair_orders_fact +* default.total_repair_cost +* default.num_repair_orders +* default.roads_cube""" + ) + + # Export first transform + assert trim_trailing_whitespace( + notebook["cells"][4]["source"], + ) == load_expected_file( + "notebook.create_transform.txt", + ) + + # Include sources and dimensions + response = await module__client_with_roads.get( + "/datajunction-clients/python/notebook?cube=default.roads_cube" + "&include_sources=true&include_dimensions=true", + ) + notebook = response.json() + assert len(notebook["cells"]) == 21 + assert ( + notebook["cells"][2]["source"] + == """### Upserting Nodes: +* default.repair_order_details +* default.repair_orders +* default.repair_orders_fact +* default.hard_hats +* default.total_repair_cost +* default.num_repair_orders +* default.hard_hat +* default.roads_cube""" + ) + assert ( + trim_trailing_whitespace(notebook["cells"][20]["source"]) + == load_expected_file( + "notebook.create_cube.txt", + ).strip() + ) + + +@pytest.mark.asyncio +async def test_export_notebook_failures(module__client_with_roads: AsyncClient): + """ + Verify that trying to set both cube and namespace when exporting to a notebook will fail + """ + response = await module__client_with_roads.get( + "/datajunction-clients/python/notebook?namespace=default&cube=default.roads_cube", + ) + assert ( + response.json()["message"] + == "Can only specify export of either a namespace or a cube." + ) diff --git a/datajunction-server/tests/api/collections_test.py b/datajunction-server/tests/api/collections_test.py new file mode 100644 index 000000000..82acfd066 --- /dev/null +++ b/datajunction-server/tests/api/collections_test.py @@ -0,0 +1,207 @@ +""" +Tests for the collections API. +""" + +import pytest +from httpx import AsyncClient + + +class TestCollections: + """ + Test ``POST /collections/``. + """ + + @pytest.mark.asyncio + async def test_collections_creating( + self, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test creating a new collection + """ + response = await module__client_with_account_revenue.post( + "/collections/", + json={ + "name": "Accounting", + "description": "This is a collection that contains accounting related nodes", + }, + ) + data = response.json() + assert response.status_code == 201 + + # Ensure the collection shows up in collections list + response = await module__client_with_account_revenue.get( + "/collections/", + ) + assert response.status_code == 200 + + data = response.json() + assert data == [ + { + "id": 1, + "name": "Accounting", + "description": "This is a collection that contains accounting related nodes", + }, + ] + + # Ensure that creating the same collection again returns a 409 + response = await module__client_with_account_revenue.post( + "/collections/", + json={ + "name": "Accounting", + "description": "This is a collection that contains accounting related nodes", + }, + ) + data = response.json() + assert response.status_code == 409 + assert "Collection already exists" in str(data) + + @pytest.mark.asyncio + async def test_collections_adding( + self, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test adding a node to a collection + """ + # Create a collection + response = await module__client_with_account_revenue.post( + "/collections/", + json={ + "name": "Revenue Project", + "description": "A collection for nodes related to a revenue project", + }, + ) + assert response.status_code == 201 + + # Add a node to a collection + response = await module__client_with_account_revenue.post( + "/collections/Revenue%20Project/nodes/", + json=["default.payment_type", "default.revenue"], + ) + assert response.status_code == 204 + + @pytest.mark.asyncio + async def test_collections__nodes_not_found_when_adding( + self, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test adding a node to a collection when the nodes can't be found + """ + # Create a collection + response = await module__client_with_account_revenue.post( + "/collections/", + json={ + "name": "My Collection", + "description": "This is a collection that contains accounting related nodes", + }, + ) + assert response.status_code == 201 + + # Add a node to a collection + response = await module__client_with_account_revenue.post( + "/collections/My%20Collection/nodes/", + json=["foo.bar", "baz.qux"], + ) + assert response.status_code == 404 + + @pytest.mark.asyncio + async def test_collections_removing_nodes( + self, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test removing a node from a collection + """ + # Create a collection + response = await module__client_with_account_revenue.post( + "/collections/", + json={ + "name": "add_then_remove", + "description": "A collection to test adding and removing a node", + }, + ) + data = response.json() + assert response.status_code == 201 + + # Add a node to a collection + response = await module__client_with_account_revenue.post( + "/collections/add_then_remove/nodes/", + json=["default.payment_type"], + ) + assert response.status_code == 204 + + # Remove the node from the collection + response = await module__client_with_account_revenue.post( + "/collections/add_then_remove/remove/", + json=["default.payment_type", "default.revenue"], + ) + assert response.status_code == 204 + + # Confirm node no longer found in collection + response = await module__client_with_account_revenue.get( + "/collections/add_then_remove", + ) + data = response.json() + for node in data["nodes"]: + assert node["name"] != "default.payment_type" + + @pytest.mark.asyncio + async def test_collections_no_errors_when_removing( + self, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test removing a node from a collection when the node can't be found + """ + # Create a collection + response = await module__client_with_account_revenue.post( + "/collections/", + json={ + "name": "Removing Nodes", + "description": "This is a collection to test for no errors when removing nodes", + }, + ) + assert response.status_code == 201 + + # Remove the node from the collection + response = await module__client_with_account_revenue.post( + "/collections/Removing%20Nodes/remove/", + json=["foo.bar", "baz.qux"], + ) + assert response.status_code == 204 + + @pytest.mark.asyncio + async def test_collections_deleting( + self, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test removing a collection + """ + # Create a collection + response = await module__client_with_account_revenue.post( + "/collections/", + json={ + "name": "DeleteMe", + "description": "This collection will be deleted", + }, + ) + assert response.status_code == 201 + + # Remove the collection + response = await module__client_with_account_revenue.delete( + "/collections/DeleteMe", + ) + assert response.status_code == 204 + + # Ensure the collection does not show up in collections list + response = await module__client_with_account_revenue.get( + "/collections/", + ) + assert response.status_code == 200 + + data = response.json() + for collection in data: + assert collection["name"] != "DeleteMe" diff --git a/datajunction-server/tests/api/cubes_test.py b/datajunction-server/tests/api/cubes_test.py new file mode 100644 index 000000000..428f776c0 --- /dev/null +++ b/datajunction-server/tests/api/cubes_test.py @@ -0,0 +1,3150 @@ +""" +Tests for the cubes API. +""" + +from typing import Dict, Iterator +from unittest import mock + +import pytest +import pytest_asyncio +from httpx import AsyncClient + +from datajunction_server.internal.nodes import derive_sql_column +from datajunction_server.models.cube import CubeElementMetadata +from datajunction_server.models.node import ColumnOutput +from datajunction_server.models.query import ColumnMetadata +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.sql.parsing.backends.antlr4 import parse +from tests.sql.utils import compare_query_strings + + +async def make_a_test_cube( + client: AsyncClient, + cube_name: str, + with_materialization: bool = True, + metrics_or_measures: str = "metrics", +): + """ + Make a new cube with a temporal partition. + """ + # Make an isolated cube + metrics_list = [ + "default.discounted_orders_rate", + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + "default.total_repair_order_discounts", + ] + await client.post( + "/nodes/cube/", + json={ + "metrics": metrics_list + ["default.double_total_repair_cost"], + "dimensions": [ + "default.hard_hat.country", + "default.hard_hat.postal_code", + "default.hard_hat.city", + "default.hard_hat.hire_date", + "default.hard_hat.state", + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + ], + "filters": ["default.hard_hat.state='AZ'"], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": f"{cube_name}", + }, + ) + if with_materialization: + # add materialization to the cube + await client.post( + f"/nodes/{cube_name}/columns/default.hard_hat.hire_date/partition", + json={ + "type_": "temporal", + "granularity": "day", + "format": "yyyyMMdd", + }, + ) + await client.post( + f"/nodes/{cube_name}/materialization/", + json={ + "job": "druid_metrics_cube" + if metrics_or_measures == "metrics" + else "druid_measures_cube", + "strategy": "incremental_time", + "config": { + "spark": {}, + }, + "schedule": "", + }, + ) + + +@pytest.mark.asyncio +async def test_read_cube(module__client_with_account_revenue: AsyncClient) -> None: + """ + Test ``GET /cubes/{name}``. + """ + # Create a cube + response = await module__client_with_account_revenue.post( + "/nodes/cube/", + json={ + "metrics": ["default.number_of_account_types"], + "dimensions": ["default.account_type.account_type_name"], + "filters": [], + "description": "A cube of number of accounts grouped by account type", + "mode": "published", + "name": "default.number_of_accounts_by_account_type", + }, + ) + assert response.status_code == 201 + data = response.json() + assert data["version"] == "v1.0" + assert data["type"] == "cube" + assert data["name"] == "default.number_of_accounts_by_account_type" + assert data["display_name"] == "Number Of Accounts By Account Type" + assert data["mode"] == "published" + assert data["tags"] == [] + + # Read the cube + response = await module__client_with_account_revenue.get( + "/cubes/default.number_of_accounts_by_account_type", + ) + assert response.status_code == 200 + data = response.json() + assert data["type"] == "cube" + assert data["name"] == "default.number_of_accounts_by_account_type" + assert data["display_name"] == "Number Of Accounts By Account Type" + assert data["version"] == "v1.0" + assert data["description"] == "A cube of number of accounts grouped by account type" + + +@pytest.mark.asyncio +async def test_create_invalid_cube(module__client_with_account_revenue: AsyncClient): + """ + Check that creating a cube with a query fails appropriately + """ + response = await module__client_with_account_revenue.post( + "/nodes/cube/", + json={ + "description": "A cube of number of accounts grouped by account type", + "mode": "published", + "query": "SELECT 1", + "cube_elements": [ + "default.number_of_account_types", + "default.account_type", + ], + "name": "default.cubes_shouldnt_have_queries", + }, + ) + assert response.status_code == 422 + data = response.json() + assert data["detail"] == [ + { + "loc": ["body", "metrics"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "dimensions"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + + # Check that creating a cube with no cube elements fails appropriately + response = await module__client_with_account_revenue.post( + "/nodes/cube/", + json={ + "metrics": ["default.account_type"], + "dimensions": ["default.account_type.account_type_name"], + "description": "A cube of number of accounts grouped by account type", + "mode": "published", + "name": "default.cubes_must_have_elements", + }, + ) + assert response.status_code == 422 + data = response.json() + assert data == { + "message": "Node default.account_type of type dimension cannot be added to a cube. " + "Did you mean to add a dimension attribute?", + "errors": [ + { + "code": 204, + "context": "", + "debug": None, + "message": "Node default.account_type of type dimension cannot be added to a " + "cube. Did you mean to add a dimension attribute?", + }, + ], + "warnings": [], + } + + # Check that creating a cube with incompatible nodes fails appropriately + response = await module__client_with_account_revenue.post( + "/nodes/cube/", + json={ + "metrics": ["default.number_of_account_types"], + "dimensions": ["default.payment_type.payment_type_name"], + "description": "", + "mode": "published", + "name": "default.cubes_cant_use_source_nodes", + }, + ) + assert response.status_code == 422 + data = response.json() + assert data == { + "message": "The dimension attribute `default.payment_type.payment_type_name` " + "is not available on every metric and thus cannot be included.", + "errors": [ + { + "code": 601, + "context": "", + "debug": None, + "message": "The dimension attribute `default.payment_type.payment_type_name` " + "is not available on every metric and thus cannot be included.", + }, + ], + "warnings": [], + } + + # Check that creating a cube with no metric nodes fails appropriately + response = await module__client_with_account_revenue.post( + "/nodes/cube/", + json={ + "metrics": [], + "dimensions": ["default.account_type.account_type_name"], + "description": "", + "mode": "published", + "name": "default.cubes_must_have_metrics", + }, + ) + assert response.status_code == 422 + data = response.json() + assert data == { + "message": "At least one metric is required", + "errors": [], + "warnings": [], + } + + # Check that creating a cube with no dimension nodes fails appropriately + response = await module__client_with_account_revenue.post( + "/nodes/cube/", + json={ + "metrics": ["default.number_of_account_types"], + "dimensions": [], + "description": "A cube of number of accounts grouped by account type", + "mode": "published", + "name": "default.cubes_must_have_dimensions", + }, + ) + assert response.status_code == 422 + data = response.json() + assert data == { + "message": "At least one dimension is required", + "errors": [], + "warnings": [], + } + + +@pytest.mark.asyncio +async def test_raise_on_cube_with_multiple_catalogs( + module__client_with_both_basics, +) -> None: + """ + Test raising when creating a cube with multiple catalogs + """ + # Create a cube + response = await module__client_with_both_basics.post( + "/nodes/cube/", + json={ + "metrics": ["basic.num_users", "different.basic.num_users"], + "dimensions": ["basic.dimension.users.country"], + "description": "multicatalog cube's raise an error", + "mode": "published", + "name": "default.multicatalog", + }, + ) + assert response.status_code >= 400 + data = response.json() + assert "Metrics and dimensions cannot be from multiple catalogs" in data["message"] + + +@pytest_asyncio.fixture(scope="module") +async def client_with_repairs_cube( + module__client_with_roads: AsyncClient, +): + """ + Adds a repairs cube with a new double total repair cost metric to the test client + """ + metrics_list = [ + "default.discounted_orders_rate", + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + "default.total_repair_order_discounts", + ] + + # Metric that doubles the total repair cost to test the sum(x) + sum(y) scenario + await module__client_with_roads.post( + "/nodes/metric/", + json={ + "description": "Double total repair cost", + "query": ( + "SELECT sum(price) + sum(price) as default_DOT_double_total_repair_cost " + "FROM default.repair_order_details" + ), + "mode": "published", + "name": "default.double_total_repair_cost", + }, + ) + # Should succeed + response = await module__client_with_roads.post( + "/nodes/cube/", + json={ + "metrics": metrics_list + ["default.double_total_repair_cost"], + "dimensions": [ + "default.hard_hat.country", + "default.hard_hat.postal_code", + "default.hard_hat.city", + "default.hard_hat.state", + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + "default.hard_hat_to_delete.hire_date", + ], + "filters": ["default.hard_hat.state='AZ'"], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.repairs_cube", + }, + ) + assert response.status_code == 201 + assert response.json()["version"] == "v1.0" + return module__client_with_roads + + +@pytest.fixture +def repair_orders_cube_measures() -> Dict: + """ + Fixture for repair orders cube metrics to measures mapping. + """ + return { + "default.double_total_repair_cost": { + "combiner": "sum(default_DOT_repair_order_details_DOT_price) + " + "sum(default_DOT_repair_order_details_DOT_price) AS ", + "measures": [ + { + "agg": "sum", + "field_name": "default_DOT_repair_order_details_DOT_price", + "name": "default.repair_order_details.price", + "type": "float", + }, + { + "agg": "sum", + "field_name": "default_DOT_repair_order_details_DOT_price", + "name": "default.repair_order_details.price", + "type": "float", + }, + ], + "metric": "default.double_total_repair_cost", + }, + } + + +@pytest.fixture +def repairs_cube_elements(): + """ + Fixture of repairs cube elements + """ + return [ + { + "display_name": "Discounted Orders Rate", + "name": "default_DOT_discounted_orders_rate", + "node_name": "default.discounted_orders_rate", + "partition": None, + "type": "metric", + }, + { + "display_name": "Company Name", + "name": "company_name", + "node_name": "default.dispatcher", + "partition": None, + "type": "dimension", + }, + { + "display_name": "Local Region", + "name": "local_region", + "node_name": "default.municipality_dim", + "partition": None, + "type": "dimension", + }, + { + "display_name": "Hire Date", + "name": "hire_date", + "node_name": "default.hard_hat", + "partition": None, + "type": "dimension", + }, + { + "display_name": "City", + "name": "city", + "node_name": "default.hard_hat", + "partition": None, + "type": "dimension", + }, + { + "display_name": "State", + "name": "state", + "node_name": "default.hard_hat", + "partition": None, + "type": "dimension", + }, + { + "display_name": "Postal Code", + "name": "postal_code", + "node_name": "default.hard_hat", + "partition": None, + "type": "dimension", + }, + { + "display_name": "Country", + "name": "country", + "node_name": "default.hard_hat", + "partition": None, + "type": "dimension", + }, + { + "display_name": "Num Repair Orders", + "name": "default_DOT_num_repair_orders", + "node_name": "default.num_repair_orders", + "partition": None, + "type": "metric", + }, + { + "display_name": "Avg Repair Price", + "name": "default_DOT_avg_repair_price", + "node_name": "default.avg_repair_price", + "partition": None, + "type": "metric", + }, + { + "display_name": "Total Repair Cost", + "name": "default_DOT_total_repair_cost", + "node_name": "default.total_repair_cost", + "partition": None, + "type": "metric", + }, + { + "display_name": "Total Repair Order Discounts", + "name": "default_DOT_total_repair_order_discounts", + "node_name": "default.total_repair_order_discounts", + "partition": None, + "type": "metric", + }, + { + "display_name": "Double Total Repair Cost", + "name": "default_DOT_double_total_repair_cost", + "node_name": "default.double_total_repair_cost", + "partition": None, + "type": "metric", + }, + ] + + +@pytest.mark.asyncio +async def test_invalid_cube(module__client_with_roads: AsyncClient): + """ + Test that creating a cube without valid dimensions fails + """ + metrics_list = [ + "default.discounted_orders_rate", + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + "default.total_repair_order_discounts", + ] + # Should fail because dimension attribute isn't available + response = await module__client_with_roads.post( + "/nodes/cube/", + json={ + "metrics": metrics_list, + "dimensions": [ + "default.contractor.company_name", + ], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.repairs_cube", + }, + ) + assert response.json()["message"] == ( + "The dimension attribute `default.contractor.company_name` " + "is not available on every metric and thus cannot be included." + ) + + +@pytest.mark.asyncio +async def test_create_cube_failures( + module__client_with_roads: AsyncClient, +): + """ + Test create cube failure cases + """ + # Creating a cube with a metric that doesn't exist should fail + response = await module__client_with_roads.post( + "/nodes/cube/", + json={ + "metrics": ["default.metric_that_doesnt_exist"], + "dimensions": [ + "default.hard_hat.country", + "default.hard_hat.postal_code", + ], + "description": "Cube with metric that doesn't exist", + "mode": "published", + "name": "default.bad_cube", + }, + ) + assert response.status_code == 404 + assert response.json() == { + "message": "The following metric nodes were not found: default.metric_that_doesnt_exist", + "errors": [ + { + "code": 203, + "context": "", + "debug": None, + "message": "The following metric nodes were not found: " + "default.metric_that_doesnt_exist", + }, + ], + "warnings": [], + } + + +@pytest.mark.asyncio +async def test_create_cube_similar_dimensions( + module__client_with_roads: AsyncClient, +): + """ + Tests cube creation for dimension attributes with the same name + but from different dimension nodes. + """ + + metrics_list = [ + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + ] + + await module__client_with_roads.post("/nodes/default.repair_order_fact/") + # Should succeed + response = await module__client_with_roads.post( + "/nodes/cube/", + json={ + "metrics": metrics_list, + "dimensions": [ + "default.hard_hat.country", + "default.hard_hat.postal_code", + "default.hard_hat_to_delete.postal_code", + ], + "filters": ["default.hard_hat.state='AZ'"], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.repairs_cube2", + }, + ) + assert response.status_code == 201 + assert response.json()["version"] == "v1.0" + + +@pytest.mark.asyncio +async def test_create_cube( + client_with_repairs_cube: AsyncClient, +): + """ + Tests cube creation and the generated cube SQL + """ + response = await client_with_repairs_cube.get("/nodes/default.repairs_cube/") + results = response.json() + + assert results["name"] == "default.repairs_cube" + assert results["display_name"] == "Repairs Cube" + assert results["description"] == "Cube of various metrics related to repairs" + + response = await client_with_repairs_cube.get("/cubes/default.repairs_cube") + cube = response.json() + + # Make sure it matches the original metrics order + metrics = [ + "default.discounted_orders_rate", + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + "default.total_repair_order_discounts", + "default.double_total_repair_cost", + ] + assert cube["cube_node_metrics"] == metrics + assert [ + elem["node_name"] for elem in cube["cube_elements"] if elem["type"] == "metric" + ] == metrics + dimensions = [ + "default.hard_hat.country", + "default.hard_hat.postal_code", + "default.hard_hat.city", + "default.hard_hat.state", + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + "default.hard_hat_to_delete.hire_date", + ] + assert cube["cube_node_dimensions"] == dimensions + + metrics_query = "&".join([f"metrics={metric}" for metric in metrics]) + dimensions_query = "&".join([f"dimensions={dim}" for dim in dimensions]) + response = await client_with_repairs_cube.get( + f"/sql?{metrics_query}&{dimensions_query}&filters=default.hard_hat.state='AZ'", + ) + metrics_sql_results = response.json() + response = await client_with_repairs_cube.get( + "/sql/default.repairs_cube?filters=default.hard_hat.state='AZ'", + ) + cube_sql_results = response.json() + expected_query = """ +WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id +), default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE + default_DOT_hard_hats.state = 'AZ' +), default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers +), default_DOT_municipality_dim AS ( + SELECT + m.municipality_id AS municipality_id, + m.contact_name, + m.contact_title, + m.local_region, + m.state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM roads.municipality AS m + LEFT JOIN roads.municipality_municipality_type AS mmt + ON m.municipality_id = mmt.municipality_id + LEFT JOIN roads.municipality_type AS mt + ON mmt.municipality_type_id = mt.municipality_type_desc +), default_DOT_hard_hat_to_delete AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats +), default_DOT_repair_order_details AS ( + SELECT + default_DOT_repair_order_details.repair_order_id, + default_DOT_repair_order_details.repair_type_id, + default_DOT_repair_order_details.price, + default_DOT_repair_order_details.quantity, + default_DOT_repair_order_details.discount + FROM roads.repair_order_details AS default_DOT_repair_order_details +), default_DOT_repair_order AS ( + SELECT + default_DOT_repair_orders.repair_order_id, + default_DOT_repair_orders.municipality_id, + default_DOT_repair_orders.hard_hat_id, + default_DOT_repair_orders.order_date, + default_DOT_repair_orders.required_date, + default_DOT_repair_orders.dispatched_date, + default_DOT_repair_orders.dispatcher_id + FROM roads.repair_orders AS default_DOT_repair_orders +), default_DOT_repair_orders_fact_metrics AS ( + SELECT + default_DOT_hard_hat.country default_DOT_hard_hat_DOT_country, + default_DOT_hard_hat.postal_code default_DOT_hard_hat_DOT_postal_code, + default_DOT_hard_hat.city default_DOT_hard_hat_DOT_city, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region default_DOT_municipality_dim_DOT_local_region, + default_DOT_hard_hat_to_delete.hire_date default_DOT_hard_hat_to_delete_DOT_hire_date, + CAST(sum(if(default_DOT_repair_orders_fact.discount > 0.0, 1, 0)) AS DOUBLE) / count(*) + AS default_DOT_discounted_orders_rate, + count(default_DOT_repair_orders_fact.repair_order_id) default_DOT_num_repair_orders, + avg(default_DOT_repair_orders_fact.price) default_DOT_avg_repair_price, + sum(default_DOT_repair_orders_fact.total_repair_cost) default_DOT_total_repair_cost, + sum(default_DOT_repair_orders_fact.price * default_DOT_repair_orders_fact.discount) + default_DOT_total_repair_order_discounts + FROM default_DOT_repair_orders_fact + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher + ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_municipality_dim + ON default_DOT_repair_orders_fact.municipality_id = default_DOT_municipality_dim.municipality_id + LEFT JOIN default_DOT_hard_hat_to_delete + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat_to_delete.hard_hat_id + GROUP BY + default_DOT_hard_hat.country, + default_DOT_hard_hat.postal_code, + default_DOT_hard_hat.city, + default_DOT_hard_hat.state, + default_DOT_dispatcher.company_name, + default_DOT_municipality_dim.local_region, + default_DOT_hard_hat_to_delete.hire_date +), default_DOT_repair_order_details_metrics AS ( + SELECT + default_DOT_hard_hat.country default_DOT_hard_hat_DOT_country, + default_DOT_hard_hat.postal_code default_DOT_hard_hat_DOT_postal_code, + default_DOT_hard_hat.city default_DOT_hard_hat_DOT_city, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region default_DOT_municipality_dim_DOT_local_region, + default_DOT_hard_hat_to_delete.hire_date default_DOT_hard_hat_to_delete_DOT_hire_date, + sum(default_DOT_repair_order_details.price) + sum(default_DOT_repair_order_details.price) + AS default_DOT_double_total_repair_cost + FROM default_DOT_repair_order_details + INNER JOIN default_DOT_repair_order + ON default_DOT_repair_order_details.repair_order_id = default_DOT_repair_order.repair_order_id + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_order.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher + ON default_DOT_repair_order.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_municipality_dim + ON default_DOT_repair_order.municipality_id = default_DOT_municipality_dim.municipality_id + LEFT JOIN default_DOT_hard_hat_to_delete + ON default_DOT_repair_order.hard_hat_id = default_DOT_hard_hat_to_delete.hard_hat_id + GROUP BY + default_DOT_hard_hat.country, + default_DOT_hard_hat.postal_code, + default_DOT_hard_hat.city, + default_DOT_hard_hat.state, + default_DOT_dispatcher.company_name, + default_DOT_municipality_dim.local_region, + default_DOT_hard_hat_to_delete.hire_date +) +SELECT + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_country, + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_postal_code, + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_city, + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_state, + default_DOT_repair_orders_fact_metrics.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_metrics.default_DOT_municipality_dim_DOT_local_region, + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_to_delete_DOT_hire_date, + default_DOT_repair_orders_fact_metrics.default_DOT_discounted_orders_rate, + default_DOT_repair_orders_fact_metrics.default_DOT_num_repair_orders, + default_DOT_repair_orders_fact_metrics.default_DOT_avg_repair_price, + default_DOT_repair_orders_fact_metrics.default_DOT_total_repair_cost, + default_DOT_repair_orders_fact_metrics.default_DOT_total_repair_order_discounts, + default_DOT_repair_order_details_metrics.default_DOT_double_total_repair_cost +FROM default_DOT_repair_orders_fact_metrics +FULL JOIN default_DOT_repair_order_details_metrics + ON default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_country = + default_DOT_repair_order_details_metrics.default_DOT_hard_hat_DOT_country + AND default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_postal_code = + default_DOT_repair_order_details_metrics.default_DOT_hard_hat_DOT_postal_code + AND default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_city = + default_DOT_repair_order_details_metrics.default_DOT_hard_hat_DOT_city + AND default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_state = + default_DOT_repair_order_details_metrics.default_DOT_hard_hat_DOT_state + AND default_DOT_repair_orders_fact_metrics.default_DOT_dispatcher_DOT_company_name = + default_DOT_repair_order_details_metrics.default_DOT_dispatcher_DOT_company_name + AND default_DOT_repair_orders_fact_metrics.default_DOT_municipality_dim_DOT_local_region = + default_DOT_repair_order_details_metrics.default_DOT_municipality_dim_DOT_local_region + AND default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_to_delete_DOT_hire_date = + default_DOT_repair_order_details_metrics.default_DOT_hard_hat_to_delete_DOT_hire_date""" + assert str(parse(metrics_sql_results["sql"])) == str(parse(expected_query)) + assert str(parse(cube_sql_results["sql"])) == str(parse(expected_query)) + + +@pytest.mark.asyncio +async def test_cube_materialization_sql_and_measures( + client_with_repairs_cube: AsyncClient, + repair_orders_cube_measures, + repairs_cube_elements, +): + """ + Verifies a cube's materialization SQL + measures + """ + await make_a_test_cube( + client_with_repairs_cube, + "default.repairs_cube_3", + metrics_or_measures="measures", + ) + + response = await client_with_repairs_cube.get("/cubes/default.repairs_cube_3/") + data = response.json() + assert data["cube_elements"] == repairs_cube_elements + expected_materialization_query = """ + WITH + default_DOT_repair_order_details AS ( + SELECT default_DOT_repair_order_details.repair_order_id, + default_DOT_repair_order_details.repair_type_id, + default_DOT_repair_order_details.price, + default_DOT_repair_order_details.quantity, + default_DOT_repair_order_details.discount + FROM roads.repair_order_details AS default_DOT_repair_order_details + ), + default_DOT_repair_order AS ( + SELECT default_DOT_repair_orders.repair_order_id, + default_DOT_repair_orders.municipality_id, + default_DOT_repair_orders.hard_hat_id, + default_DOT_repair_orders.order_date, + default_DOT_repair_orders.required_date, + default_DOT_repair_orders.dispatched_date, + default_DOT_repair_orders.dispatcher_id + FROM roads.repair_orders AS default_DOT_repair_orders + ), + default_DOT_hard_hat AS ( + SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + ), + default_DOT_dispatcher AS ( + SELECT default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_municipality_dim AS ( + SELECT m.municipality_id AS municipality_id, + m.contact_name, + m.contact_title, + m.local_region, + m.state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM roads.municipality AS m LEFT JOIN roads.municipality_municipality_type AS mmt ON m.municipality_id = mmt.municipality_id + LEFT JOIN roads.municipality_type AS mt ON mmt.municipality_type_id = mt.municipality_type_desc + ) + SELECT default_DOT_repair_order_details.price default_DOT_repair_order_details_DOT_price, + default_DOT_hard_hat.country default_DOT_hard_hat_DOT_country, + default_DOT_hard_hat.postal_code default_DOT_hard_hat_DOT_postal_code, + default_DOT_hard_hat.city default_DOT_hard_hat_DOT_city, + default_DOT_hard_hat.hire_date default_DOT_hard_hat_DOT_hire_date, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region default_DOT_municipality_dim_DOT_local_region + FROM default_DOT_repair_order_details INNER JOIN default_DOT_repair_order ON default_DOT_repair_order_details.repair_order_id = default_DOT_repair_order.repair_order_id + INNER JOIN default_DOT_hard_hat ON default_DOT_repair_order.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher ON default_DOT_repair_order.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_municipality_dim ON default_DOT_repair_order.municipality_id = default_DOT_municipality_dim.municipality_id + """ + assert str(parse(data["materializations"][0]["config"]["query"])) == str( + parse(expected_materialization_query), + ) + assert data["materializations"][0]["job"] == "DruidMeasuresCubeMaterializationJob" + assert ( + data["materializations"][0]["config"]["measures"] == repair_orders_cube_measures + ) + + +@pytest.mark.asyncio +async def test_druid_cube_agg_materialization( + client_with_repairs_cube: AsyncClient, + module__query_service_client: Iterator[QueryServiceClient], +): + """ + Verifies scheduling a materialized aggregate cube + """ + await make_a_test_cube( + client_with_repairs_cube, + "default.repairs_cube_4", + ) + response = await client_with_repairs_cube.post( + "/nodes/default.repairs_cube_4/materialization/", + json={ + "job": "druid_metrics_cube", + "strategy": "incremental_time", + "config": { + "spark": {}, + }, + "schedule": "@daily", + }, + ) + assert response.json() == { + "message": "The same materialization config with name " + "`druid_metrics_cube__incremental_time__default.hard_hat.hire_date` " + "already exists for node `default.repairs_cube_4` so no update was performed.", + "info": { + "output_tables": ["common.a", "common.b"], + "urls": ["http://fake.url/job"], + }, + } + called_kwargs_all = [ + call_[0][0] + for call_ in module__query_service_client.materialize.call_args_list # type: ignore + ] + called_kwargs_for_cube_4 = [ + call_ + for call_ in called_kwargs_all + if call_.node_name == "default.repairs_cube_4" + ] + called_kwargs = called_kwargs_for_cube_4[0] + assert ( + called_kwargs.name + == "druid_metrics_cube__incremental_time__default.hard_hat.hire_date" + ) + assert called_kwargs.node_name == "default.repairs_cube_4" + assert called_kwargs.node_type == "cube" + assert called_kwargs.schedule == "@daily" + assert called_kwargs.spark_conf == {} + assert len(called_kwargs.columns) > 0 + dimensions_sorted = sorted( + called_kwargs.druid_spec["dataSchema"]["parser"]["parseSpec"]["dimensionsSpec"][ + "dimensions" + ], + ) + called_kwargs.druid_spec["dataSchema"]["parser"]["parseSpec"]["dimensionsSpec"][ + "dimensions" + ] = dimensions_sorted + called_kwargs.druid_spec["dataSchema"]["metricsSpec"] = sorted( + called_kwargs.druid_spec["dataSchema"]["metricsSpec"], + key=lambda x: x["fieldName"], + ) + assert sorted(called_kwargs.columns, key=lambda x: x.name) == sorted( + [ + ColumnMetadata( + name="default_DOT_avg_repair_price", + type="double", + column="default_DOT_avg_repair_price", + node="default.avg_repair_price", + semantic_entity="default.avg_repair_price.default_DOT_avg_repair_price", + semantic_type="metric", + ), + ColumnMetadata( + name="default_DOT_discounted_orders_rate", + type="double", + column="default_DOT_discounted_orders_rate", + node="default.discounted_orders_rate", + semantic_entity="default.discounted_orders_rate.default_DOT_discounted_orders_rate", + semantic_type="metric", + ), + ColumnMetadata( + name="default_DOT_dispatcher_DOT_company_name", + type="string", + column="company_name", + node="default.dispatcher", + semantic_entity="default.dispatcher.company_name", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_double_total_repair_cost", + type="double", + column="default_DOT_double_total_repair_cost", + node="default.double_total_repair_cost", + semantic_entity="default.double_total_repair_cost.default_DOT_double_total_repair_cost", + semantic_type="metric", + ), + ColumnMetadata( + name="default_DOT_hard_hat_DOT_city", + type="string", + column="city", + node="default.hard_hat", + semantic_entity="default.hard_hat.city", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_hard_hat_DOT_country", + type="string", + column="country", + node="default.hard_hat", + semantic_entity="default.hard_hat.country", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_hard_hat_DOT_hire_date", + type="timestamp", + column="hire_date", + node="default.hard_hat", + semantic_entity="default.hard_hat.hire_date", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_hard_hat_DOT_postal_code", + type="string", + column="postal_code", + node="default.hard_hat", + semantic_entity="default.hard_hat.postal_code", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_hard_hat_DOT_state", + type="string", + column="state", + node="default.hard_hat", + semantic_entity="default.hard_hat.state", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_municipality_dim_DOT_local_region", + type="string", + column="local_region", + node="default.municipality_dim", + semantic_entity="default.municipality_dim.local_region", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_num_repair_orders", + type="bigint", + column="default_DOT_num_repair_orders", + node="default.num_repair_orders", + semantic_entity="default.num_repair_orders.default_DOT_num_repair_orders", + semantic_type="metric", + ), + ColumnMetadata( + name="default_DOT_total_repair_cost", + type="double", + column="default_DOT_total_repair_cost", + node="default.total_repair_cost", + semantic_entity="default.total_repair_cost.default_DOT_total_repair_cost", + semantic_type="metric", + ), + ColumnMetadata( + name="default_DOT_total_repair_order_discounts", + type="double", + column="default_DOT_total_repair_order_discounts", + node="default.total_repair_order_discounts", + semantic_entity="default.total_repair_order_discounts.default_DOT_total_repair_order_discounts", + semantic_type="metric", + ), + ], + key=lambda x: x.name, + ) + assert called_kwargs.druid_spec == { + "dataSchema": { + "dataSource": "default_DOT_repairs_cube_4", + "parser": { + "parseSpec": { + "format": "parquet", + "dimensionsSpec": { + "dimensions": [ + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_hard_hat_DOT_city", + "default_DOT_hard_hat_DOT_country", + "default_DOT_hard_hat_DOT_hire_date", + "default_DOT_hard_hat_DOT_postal_code", + "default_DOT_hard_hat_DOT_state", + "default_DOT_municipality_dim_DOT_local_region", + ], + }, + "timestampSpec": { + "column": "default_DOT_hard_hat_DOT_hire_date", + "format": "yyyyMMdd", + }, + }, + }, + "metricsSpec": [ + { + "fieldName": "default_DOT_avg_repair_price", + "name": "default_DOT_avg_repair_price", + "type": "doubleSum", + }, + { + "fieldName": "default_DOT_discounted_orders_rate", + "name": "default_DOT_discounted_orders_rate", + "type": "doubleSum", + }, + { + "fieldName": "default_DOT_double_total_repair_cost", + "name": "default_DOT_double_total_repair_cost", + "type": "doubleSum", + }, + { + "fieldName": "default_DOT_num_repair_orders", + "name": "default_DOT_num_repair_orders", + "type": "longSum", + }, + { + "fieldName": "default_DOT_total_repair_cost", + "name": "default_DOT_total_repair_cost", + "type": "doubleSum", + }, + { + "fieldName": "default_DOT_total_repair_order_discounts", + "name": "default_DOT_total_repair_order_discounts", + "type": "doubleSum", + }, + ], + "granularitySpec": { + "type": "uniform", + "segmentGranularity": "DAY", + "intervals": [], + }, + }, + "tuningConfig": { + "partitionsSpec": {"targetPartitionSize": 5000000, "type": "hashed"}, + "useCombiner": True, + "type": "hadoop", + }, + } + response = await client_with_repairs_cube.get("/nodes/default.repairs_cube_4/") + materializations = response.json()["materializations"] + assert len(materializations) == 1 + druid_materialization = [ + materialization + for materialization in materializations + if materialization["job"] == "DruidMetricsCubeMaterializationJob" + ][0] + assert set(druid_materialization["config"]["dimensions"]) == { + "default_DOT_hard_hat_DOT_country", + "default_DOT_hard_hat_DOT_postal_code", + "default_DOT_hard_hat_DOT_city", + "default_DOT_hard_hat_DOT_hire_date", + "default_DOT_hard_hat_DOT_state", + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_municipality_dim_DOT_local_region", + } + assert druid_materialization["config"]["metrics"] == [ + { + "name": "default_DOT_discounted_orders_rate", + "type": "double", + "column": "default_DOT_discounted_orders_rate", + "node": "default.discounted_orders_rate", + "semantic_entity": "default.discounted_orders_rate.default_DOT_discounted_orders_rate", + "semantic_type": "metric", + }, + { + "name": "default_DOT_num_repair_orders", + "type": "bigint", + "column": "default_DOT_num_repair_orders", + "node": "default.num_repair_orders", + "semantic_entity": "default.num_repair_orders.default_DOT_num_repair_orders", + "semantic_type": "metric", + }, + { + "name": "default_DOT_avg_repair_price", + "type": "double", + "column": "default_DOT_avg_repair_price", + "node": "default.avg_repair_price", + "semantic_entity": "default.avg_repair_price.default_DOT_avg_repair_price", + "semantic_type": "metric", + }, + { + "name": "default_DOT_total_repair_cost", + "type": "double", + "column": "default_DOT_total_repair_cost", + "node": "default.total_repair_cost", + "semantic_entity": "default.total_repair_cost.default_DOT_total_repair_cost", + "semantic_type": "metric", + }, + { + "name": "default_DOT_total_repair_order_discounts", + "type": "double", + "column": "default_DOT_total_repair_order_discounts", + "node": "default.total_repair_order_discounts", + "semantic_entity": "default.total_repair_order_discounts." + "default_DOT_total_repair_order_discounts", + "semantic_type": "metric", + }, + { + "name": "default_DOT_double_total_repair_cost", + "type": "double", + "column": "default_DOT_double_total_repair_cost", + "node": "default.double_total_repair_cost", + "semantic_entity": "default.double_total_repair_cost." + "default_DOT_double_total_repair_cost", + "semantic_type": "metric", + }, + ] + assert druid_materialization["schedule"] == "@daily" + + +@pytest.mark.asyncio +async def test_materialized_cube_sql( + client_with_repairs_cube: AsyncClient, +): + """ + Test generating SQL for a materialized cube with two cases: + (1) the materialized table's catalog is compatible with the desired engine. + (2) the materialized table's catalog is not compatible with the desired engine. + """ + await make_a_test_cube( + client_with_repairs_cube, + "default.mini_repairs_cube", + with_materialization=True, + ) + await client_with_repairs_cube.post( + "/data/default.mini_repairs_cube/availability/", + json={ + "catalog": "draft", + "schema_": "roads", + "table": "mini_repairs_cube", + "valid_through_ts": 1010129120, + }, + ) + # Ask for SQL with metrics, dimensions, filters, order by, and limit + response = await client_with_repairs_cube.get( + "/sql/", + params={ + "metrics": ["default.avg_repair_price", "default.num_repair_orders"], + "dimensions": ["default.hard_hat.state", "default.dispatcher.company_name"], + "limit": 100, + }, + ) + results = response.json() + assert "default_DOT_hard_hat AS (" in results["sql"] + + response = await client_with_repairs_cube.post( + "/data/default.mini_repairs_cube/availability/", + json={ + "catalog": "default", + "schema_": "roads", + "table": "mini_repairs_cube", + "valid_through_ts": 1010129120, + }, + ) + assert response.status_code == 200 + + response = await client_with_repairs_cube.get( + "/sql/", + params={ + "metrics": ["default.avg_repair_price", "default.num_repair_orders"], + "dimensions": ["default.hard_hat.state", "default.dispatcher.company_name"], + "limit": 100, + }, + ) + response = await client_with_repairs_cube.get( + "/sql/", + params={ + "metrics": ["default.avg_repair_price", "default.num_repair_orders"], + "dimensions": ["default.hard_hat.state", "default.dispatcher.company_name"], + "limit": 100, + }, + ) + results = response.json() + expected_sql = """ + SELECT + SUM(default_DOT_avg_repair_price), + SUM(default_DOT_num_repair_orders), + default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher_DOT_company_name + FROM mini_repairs_cube + GROUP BY default_DOT_hard_hat_DOT_state, default_DOT_dispatcher_DOT_company_name + LIMIT 100""" + assert str(parse(results["sql"])) == str(parse(expected_sql)) + + +@pytest.mark.asyncio +async def test_remove_dimension_link_invalidate_cube( + client_with_repairs_cube: AsyncClient, +): + """ + Verify that removing a dimension link can invalidate a cube. + """ + # Delete an irrelevant dimension link + response = await client_with_repairs_cube.request( + "DELETE", + "/nodes/default.repair_order/link/", + json={ + "dimension_node": "default.hard_hat_to_delete", + "role": None, + }, + ) + assert response.json() == { + "message": "Dimension link default.hard_hat_to_delete to node default.repair_order has " + "been removed.", + } + # The cube remains valid + response = await client_with_repairs_cube.get("/nodes/default.repairs_cube") + data = response.json() + assert data["status"] == "valid" + + # Add an unaffected cube + await client_with_repairs_cube.post( + "/nodes/cube/", + json={ + "metrics": ["default.num_repair_orders"], + "dimensions": [ + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + ], + "filters": ["default.dispatcher.company_name='Pothole Pete'"], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.repairs_cube_unaffected", + }, + ) + + # Delete the link between default.repair_orders_fact and default.hard_hat_to_delete + response = await client_with_repairs_cube.request( + "DELETE", + "/nodes/default.repair_orders_fact/link", + json={ + "dimension_node": "default.hard_hat_to_delete", + }, + ) + assert response.status_code == 201 + + # The cube that has default.hard_hat_to_delete dimensions should be invalid + response = await client_with_repairs_cube.get("/nodes/default.repairs_cube") + assert response.json()["status"] == "invalid" + + # The cube without default.hard_hat_to_delete dimensions should remain valid + response = await client_with_repairs_cube.get( + "/nodes/default.repairs_cube_unaffected", + ) + assert response.json()["status"] == "valid" + + +@pytest.mark.asyncio +async def test_changing_node_upstream_from_cube( + client_with_repairs_cube: AsyncClient, +): + """ + Verify changing nodes upstream from a cube + """ + # Prepare the objects + await client_with_repairs_cube.post( + "/nodes/source/", + json={ + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "municipality_id", "type": "string"}, + {"name": "hard_hat_id", "type": "int"}, + {"name": "order_date", "type": "timestamp"}, + {"name": "required_date", "type": "timestamp"}, + {"name": "dispatched_date", "type": "timestamp"}, + {"name": "dispatcher_id", "type": "int"}, + ], + "description": "All repair orders", + "mode": "published", + "name": "default.repair_orders__one", + "catalog": "default", + "schema_": "roads", + "table": "repair_orders__one", + }, + ) + await client_with_repairs_cube.post( + "/nodes/dimension/", + json={ + "description": "Repair order dimension", + "query": """ + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM default.repair_orders__one + """, + "mode": "published", + "name": "default.repair_order_dim__one", + "primary_key": ["repair_order_id"], + }, + ) + await client_with_repairs_cube.post( + "/nodes/metric/", + json={ + "description": "Repair order date count", + "query": """ + SELECT + COUNT(DISTINCT order_date) AS order_date_count + FROM default.repair_order_dim__one + """, + "mode": "published", + "name": "default.order_date_count__one", + }, + ) + response = await client_with_repairs_cube.post( + "/nodes/cube/", + json={ + "metrics": ["default.order_date_count__one"], + "dimensions": [ + "default.repair_order_dim__one.hard_hat_id", + "default.repair_order_dim__one.municipality_id", + ], + "filters": ["default.repair_order_dim__one.hard_hat_id > 10"], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.repairs_cube_1", + }, + ) + assert response.status_code == 201 + assert response.json()["version"] == "v1.0" + + # Verify effects on cube after deactivating a node upstream from the cube + await client_with_repairs_cube.request( + "DELETE", + "/nodes/default.repair_orders__one/", + ) + response = await client_with_repairs_cube.get("/nodes/default.repairs_cube_1/") + data = response.json() + assert data["status"] == "invalid" + + # Verify effects on cube after restoring a node upstream from the cube + await client_with_repairs_cube.post("/nodes/default.repair_orders__one/restore/") + response = await client_with_repairs_cube.get("/nodes/default.repairs_cube_1/") + data = response.json() + assert data["status"] == "valid" + + response = await client_with_repairs_cube.get("/cubes/default.repairs_cube_1/") + data = response.json() + expected_elements = [ + { + "name": "default_DOT_order_date_count__one", + "display_name": "Order Date Count One", + "node_name": "default.order_date_count__one", + "type": "metric", + "partition": None, + }, + { + "name": "municipality_id", + "display_name": "Municipality Id", + "node_name": "default.repair_order_dim__one", + "type": "dimension", + "partition": None, + }, + { + "name": "hard_hat_id", + "display_name": "Hard Hat Id", + "node_name": "default.repair_order_dim__one", + "type": "dimension", + "partition": None, + }, + ] + assert data["cube_elements"] == expected_elements + + # Verify effects on cube after updating a node upstream from the cube + await client_with_repairs_cube.patch( + "/nodes/default.order_date_count__one/", + json={ + "query": """SELECT COUNT(DISTINCT order_date) + 42 AS order_date_count " + "FROM default.repair_order_dim__one""", + }, + ) + response = await client_with_repairs_cube.get("/nodes/default.repairs_cube_1/") + data = response.json() + assert data["status"] == "valid" + + response = await client_with_repairs_cube.get("/cubes/default.repairs_cube_1/") + data = response.json() + assert data["cube_elements"] == expected_elements + + +def assert_updated_repairs_cube(data): + """ + Asserts that the updated repairs cube has the right cube elements and default materialization + """ + assert sorted(data["cube_elements"], key=lambda x: x["name"]) == [ + { + "display_name": "City", + "name": "city", + "node_name": "default.hard_hat", + "type": "dimension", + "partition": None, + }, + { + "display_name": "Discounted Orders Rate", + "name": "default_DOT_discounted_orders_rate", + "node_name": "default.discounted_orders_rate", + "type": "metric", + "partition": None, + }, + { + "display_name": "Hire Date", + "name": "hire_date", + "node_name": "default.hard_hat", + "partition": None, + "type": "dimension", + }, + ] + + +@pytest.mark.asyncio +async def test_updating_cube( + client_with_repairs_cube: AsyncClient, +): + """ + Verify updating a cube + """ + await make_a_test_cube( + client_with_repairs_cube, + "default.repairs_cube_6", + ) + # Check a minor update to the cube + response = await client_with_repairs_cube.patch( + "/nodes/default.repairs_cube_6", + json={ + "description": "This cube has a new description", + }, + ) + data = response.json() + assert data["version"] == "v1.1" + assert data["description"] == "This cube has a new description" + + # Check a major update to the cube + response = await client_with_repairs_cube.patch( + "/nodes/default.repairs_cube_6", + json={ + "metrics": ["default.discounted_orders_rate"], + "dimensions": ["default.hard_hat.city", "default.hard_hat.hire_date"], + }, + ) + result = response.json() + assert result["version"] == "v2.0" + assert sorted(result["columns"], key=lambda x: x["name"]) == sorted( + [ + { + "name": "default.discounted_orders_rate", + "display_name": "Discounted Orders Rate", + "type": "double", + "attributes": [], + "dimension": None, + "partition": None, + }, + { + "name": "default.hard_hat.city", + "display_name": "City", + "type": "string", + "attributes": [], + "dimension": None, + "partition": None, + }, + { + "name": "default.hard_hat.hire_date", + "display_name": "Hire Date", + "type": "timestamp", + "attributes": [], + "dimension": None, + "partition": { + "type_": "temporal", + "format": "yyyyMMdd", + "granularity": "day", + "expression": None, + }, + }, + ], + key=lambda x: x["name"], # type: ignore + ) + + response = await client_with_repairs_cube.get("/cubes/default.repairs_cube_6/") + data = response.json() + assert_updated_repairs_cube(data) + + response = await client_with_repairs_cube.get( + "/history?node=default.repairs_cube_6", + ) + assert [ + event for event in response.json() if event["activity_type"] == "update" + ] == [ + { + "activity_type": "update", + "created_at": mock.ANY, + "details": {"version": "v2.0"}, + "entity_name": "default.repairs_cube_6", + "entity_type": "node", + "id": mock.ANY, + "node": "default.repairs_cube_6", + "post": {}, + "pre": {}, + "user": mock.ANY, + }, + { + "activity_type": "update", + "created_at": mock.ANY, + "details": {"version": "v1.1"}, + "entity_name": "default.repairs_cube_6", + "entity_type": "node", + "id": mock.ANY, + "node": "default.repairs_cube_6", + "post": {}, + "pre": {}, + "user": mock.ANY, + }, + ] + + +@pytest.mark.asyncio +async def test_updating_cube_with_existing_materialization( + client_with_repairs_cube: AsyncClient, + module__query_service_client: QueryServiceClient, +): + """ + Verify updating a cube with existing materialization + """ + # Make an isolated cube and add materialization to the cube + await make_a_test_cube( + client_with_repairs_cube, + "default.repairs_cube_2", + metrics_or_measures="measures", + ) + # Make sure that the cube already has an additional materialization configured + response = await client_with_repairs_cube.get("/cubes/default.repairs_cube_2/") + data = response.json() + assert len(data["materializations"]) == 1 + + # Update the existing materialization config + response = await client_with_repairs_cube.post( + "/nodes/default.repairs_cube_2/materialization", + json={ + "job": "druid_measures_cube", + "strategy": "incremental_time", + "config": {"spark": {"spark.executor.memory": "6g"}}, + "schedule": "@daily", + }, + ) + data = response.json() + assert data == { + "message": "Successfully updated materialization config named " + "`druid_measures_cube__incremental_time__default.hard_hat.hire_date` for node " + "`default.repairs_cube_2`", + "urls": [["http://fake.url/job"]], + } + + # Check that the configured materialization was updated + response = await client_with_repairs_cube.get("/cubes/default.repairs_cube_2/") + data = response.json() + assert data["materializations"][0]["config"]["spark"] == { + "spark.executor.memory": "6g", + } + + # Update the cube, but keep the temporal partition column. This should succeed + response = await client_with_repairs_cube.patch( + "/nodes/default.repairs_cube_2", + json={ + "metrics": ["default.discounted_orders_rate"], + "dimensions": ["default.hard_hat.city", "default.hard_hat.hire_date"], + }, + ) + result = response.json() + assert result["version"] == "v2.0" + + # Check that the query service was called to materialize + assert len(module__query_service_client.materialize.call_args_list) >= 1 # type: ignore + last_call_args = ( + module__query_service_client.materialize.call_args_list[-1].args[0].dict() # type: ignore + ) + assert ( + last_call_args["name"] + == "druid_measures_cube__incremental_time__default.hard_hat.hire_date" + ) + assert last_call_args["node_name"] == "default.repairs_cube_2" + assert last_call_args["node_version"] == "v2.0" + assert last_call_args["node_type"] == "cube" + assert last_call_args["schedule"] == "@daily" + assert last_call_args["druid_spec"]["dataSchema"]["parser"]["parseSpec"][ + "timestampSpec" + ] == { + "column": "default_DOT_hard_hat_DOT_hire_date", + "format": "yyyyMMdd", + } + + # Check that the cube was updated + response = await client_with_repairs_cube.get("/cubes/default.repairs_cube_2/") + data = response.json() + assert_updated_repairs_cube(data) + + # Check that the existing materialization was updated + assert data["materializations"][0]["backfills"] == [] + assert sorted( + data["materializations"][0]["config"]["columns"], + key=lambda x: x["name"], + ) == sorted( + [ + { + "column": "city", + "name": "default_DOT_hard_hat_DOT_city", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.city", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "hire_date", + "name": "default_DOT_hard_hat_DOT_hire_date", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.hire_date", + "semantic_type": "dimension", + "type": "timestamp", + }, + { + "column": "discount", + "name": "default_DOT_repair_orders_fact_DOT_discount", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.discount", + "semantic_type": "measure", + "type": "float", + }, + ], + key=lambda x: x["name"], + ) + assert data["materializations"][0]["config"]["dimensions"] == [ + "default_DOT_hard_hat_DOT_city", + "default_DOT_hard_hat_DOT_hire_date", + ] + assert data["materializations"][0]["config"]["druid"] is None + assert data["materializations"][0]["config"]["measures"] == { + "default.discounted_orders_rate": { + "combiner": "CAST(sum(if(default_DOT_repair_orders_fact_DOT_discount " + "> 0.0, 1, 0)) AS DOUBLE) / " + "count(*) AS ", + "measures": [ + { + "agg": "sum", + "field_name": "default_DOT_repair_orders_fact_DOT_discount", + "name": "default.repair_orders_fact.discount", + "type": "float", + }, + ], + "metric": "default.discounted_orders_rate", + }, + } + assert data["materializations"][0]["config"]["prefix"] == "" + assert data["materializations"][0]["config"]["suffix"] == "" + assert data["materializations"][0]["config"]["spark"] == { + "spark.executor.memory": "6g", + } + assert set(data["materializations"][0]["config"]["upstream_tables"]) == { + "default.roads.repair_order_details", + "default.roads.repair_orders", + "default.roads.hard_hats", + } + assert data["materializations"][0]["strategy"] == "incremental_time" + assert data["materializations"][0]["job"] == "DruidMeasuresCubeMaterializationJob" + assert ( + data["materializations"][0]["name"] + == "druid_measures_cube__incremental_time__default.hard_hat.hire_date" + ) + assert data["materializations"][0]["schedule"] == "@daily" + + response = await client_with_repairs_cube.get( + "/history?node=default.repairs_cube_2", + ) + assert [ + event for event in response.json() if event["activity_type"] == "update" + ] == [ + { + "activity_type": "update", + "created_at": mock.ANY, + "details": {}, + "entity_name": "druid_measures_cube__incremental_time__default.hard_hat.hire_date", + "entity_type": "materialization", + "id": mock.ANY, + "node": "default.repairs_cube_2", + "post": {}, + "pre": {}, + "user": "dj", + }, + { + "activity_type": "update", + "created_at": mock.ANY, + "details": {"version": "v2.0"}, + "entity_name": "default.repairs_cube_2", + "entity_type": "node", + "id": mock.ANY, + "node": "default.repairs_cube_2", + "post": {}, + "pre": {}, + "user": "dj", + }, + { + "activity_type": "update", + "created_at": mock.ANY, + "details": { + "materialization": "druid_measures_cube__incremental_time__" + "default.hard_hat.hire_date", + "node": "default.repairs_cube_2", + }, + "entity_name": "druid_measures_cube__incremental_time__default.hard_hat.hire_date", + "entity_type": "materialization", + "id": mock.ANY, + "node": "default.repairs_cube_2", + "post": {}, + "pre": {}, + "user": "dj", + }, + ] + + # Update the cube, but remove the temporal partition column. This should fail when + # trying to update the cube's materialization config + response = await client_with_repairs_cube.patch( + "/nodes/default.repairs_cube_2", + json={ + "metrics": ["default.discounted_orders_rate"], + "dimensions": ["default.hard_hat.city"], + }, + ) + result = response.json() + assert result["message"] == ( + "The cube materialization cannot be configured if there is no temporal partition " + "specified on the cube. Please make sure at least one cube element has a temporal " + "partition defined" + ) + + +@pytest.mark.asyncio +async def test_get_materialized_cube_dimension_sql( + client_with_repairs_cube: AsyncClient, +): + """ + Test building SQL to get unique dimension values for a materialized cube + """ + await make_a_test_cube( + client_with_repairs_cube, + "default.repairs_cube_7", + ) + await client_with_repairs_cube.post( + "/data/default.repairs_cube_7/availability/", + json={ + "catalog": "default", + "schema_": "roads", + "table": "repairs_cube", + "valid_through_ts": 1010129120, + }, + ) + + # Asking for an unavailable dimension should fail + response = await client_with_repairs_cube.get( + "/cubes/default.repairs_cube_7/dimensions/sql", + params={ + "dimensions": ["default.hard_hat.city", "default.contractor.company_name"], + }, + ) + assert response.json()["message"] == ( + "The following dimensions {'default.contractor.company_name'} are " + "not available in the cube default.repairs_cube_7." + ) + + # Ask for single dimension + response = await client_with_repairs_cube.get( + "/cubes/default.repairs_cube_7/dimensions/sql", + params={ + "dimensions": ["default.hard_hat.city"], + }, + ) + results = response.json() + assert results["columns"] == [ + { + "column": None, + "name": "default_DOT_hard_hat_DOT_city", + "node": None, + "semantic_entity": "default.hard_hat.city", + "semantic_type": "dimension", + "type": "string", + }, + ] + assert compare_query_strings( + results["sql"], + "SELECT default_DOT_hard_hat_DOT_city FROM repairs_cube " + "GROUP BY default_DOT_hard_hat_DOT_city", + ) + + # Ask for single dimension with counts + response = await client_with_repairs_cube.get( + "/cubes/default.repairs_cube_7/dimensions/sql", + params={ + "dimensions": ["default.hard_hat.city"], + "include_counts": True, + }, + ) + results = response.json() + assert results["columns"] == [ + { + "column": None, + "name": "default_DOT_hard_hat_DOT_city", + "node": None, + "semantic_entity": "default.hard_hat.city", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": None, + "name": "count", + "node": None, + "semantic_entity": None, + "semantic_type": None, + "type": "int", + }, + ] + assert compare_query_strings( + results["sql"], + "SELECT default_DOT_hard_hat_DOT_city, COUNT(*) FROM repairs_cube " + "GROUP BY default_DOT_hard_hat_DOT_city " + "ORDER BY 2 DESC", + ) + + # Ask for multiple dimensions with counts + response = await client_with_repairs_cube.get( + "/cubes/default.repairs_cube_7/dimensions/sql", + params={ + "dimensions": ["default.hard_hat.city", "default.dispatcher.company_name"], + "include_counts": True, + }, + ) + results = response.json() + assert results["columns"] == [ + { + "column": None, + "name": "default_DOT_hard_hat_DOT_city", + "node": None, + "semantic_entity": "default.hard_hat.city", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": None, + "name": "default_DOT_dispatcher_DOT_company_name", + "node": None, + "semantic_entity": "default.dispatcher.company_name", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": None, + "name": "count", + "node": None, + "semantic_entity": None, + "semantic_type": None, + "type": "int", + }, + ] + assert compare_query_strings( + results["sql"], + "SELECT default_DOT_hard_hat_DOT_city, default_DOT_dispatcher_DOT_company_name, COUNT(*) " + "FROM repairs_cube " + "GROUP BY default_DOT_hard_hat_DOT_city, default_DOT_dispatcher_DOT_company_name " + "ORDER BY 3 DESC", + ) + + # Ask for multiple dimensions with filters and limit + response = await client_with_repairs_cube.get( + "/cubes/default.repairs_cube_7/dimensions/sql", + params={ + "dimensions": ["default.hard_hat.city", "default.dispatcher.company_name"], + "filters": "default.dispatcher.company_name = 'Pothole Pete'", + "limit": 4, + "include_counts": True, + }, + ) + results = response.json() + assert compare_query_strings( + results["sql"], + "SELECT default_DOT_hard_hat_DOT_city, default_DOT_dispatcher_DOT_company_name, COUNT(*) " + "FROM repairs_cube " + "WHERE default_DOT_dispatcher_DOT_company_name = 'Pothole Pete' " + "GROUP BY default_DOT_hard_hat_DOT_city, default_DOT_dispatcher_DOT_company_name " + "ORDER BY 3 DESC LIMIT 4", + ) + + +@pytest.mark.asyncio +async def test_get_unmaterialized_cube_dimensions_values( + client_with_repairs_cube: AsyncClient, +): + """ + Test building SQL + getting data for dimension values for an unmaterialized cube + """ + await make_a_test_cube( + client_with_repairs_cube, + "default.repairs_cube_8", + with_materialization=False, + ) + # Get SQL for single dimension + response = await client_with_repairs_cube.get( + "/cubes/default.repairs_cube_8/dimensions/sql", + params={ + "dimensions": ["default.hard_hat.city"], + }, + ) + results = response.json() + assert "SELECT default_DOT_hard_hat_DOT_city" in results["sql"] + assert "GROUP BY default_DOT_hard_hat_DOT_city" in results["sql"] + + # Get data for single dimension + response = await client_with_repairs_cube.get( + "/cubes/default.repairs_cube_8/dimensions/data", + params={ + "dimensions": ["default.hard_hat.city"], + }, + ) + results = response.json() + assert results == { + "cardinality": 9, + "dimensions": ["default.hard_hat.city"], + "values": [ + {"count": None, "value": ["Jersey City"]}, + {"count": None, "value": ["Billerica"]}, + {"count": None, "value": ["Southgate"]}, + {"count": None, "value": ["Phoenix"]}, + {"count": None, "value": ["Southampton"]}, + {"count": None, "value": ["Powder Springs"]}, + {"count": None, "value": ["Middletown"]}, + {"count": None, "value": ["Muskogee"]}, + {"count": None, "value": ["Niagara Falls"]}, + ], + } + + # Ask for single dimension with counts + response = await client_with_repairs_cube.get( + "/cubes/default.repairs_cube_8/dimensions/sql", + params={ + "dimensions": ["default.hard_hat.city"], + "include_counts": True, + }, + ) + results = response.json() + assert "SELECT default_DOT_hard_hat_DOT_city,\n\tCOUNT(*)" in results["sql"] + assert "GROUP BY default_DOT_hard_hat_DOT_city" in results["sql"] + assert "ORDER BY 2 DESC" in results["sql"] + + # Get data for single dimension with counts + response = await client_with_repairs_cube.get( + "/cubes/default.repairs_cube_8/dimensions/data", + params={ + "dimensions": ["default.hard_hat.city"], + "include_counts": True, + }, + ) + assert response.json() == { + "cardinality": 9, + "dimensions": ["default.hard_hat.city"], + "values": [ + {"count": 5, "value": ["Southgate"]}, + {"count": 4, "value": ["Jersey City"]}, + {"count": 4, "value": ["Southampton"]}, + {"count": 3, "value": ["Billerica"]}, + {"count": 3, "value": ["Powder Springs"]}, + {"count": 2, "value": ["Phoenix"]}, + {"count": 2, "value": ["Middletown"]}, + {"count": 1, "value": ["Muskogee"]}, + {"count": 1, "value": ["Niagara Falls"]}, + ], + } + + # Get data for multiple dimensions with counts + response = await client_with_repairs_cube.get( + "/cubes/default.repairs_cube_8/dimensions/data", + params={ + "dimensions": ["default.hard_hat.city", "default.dispatcher.company_name"], + "include_counts": True, + }, + ) + assert response.json() == { + "cardinality": 17, + "dimensions": ["default.hard_hat.city", "default.dispatcher.company_name"], + "values": [ + {"count": 3, "value": ["Jersey City", "Pothole Pete"]}, + {"count": 2, "value": ["Southgate", "Asphalts R Us"]}, + {"count": 2, "value": ["Billerica", "Asphalts R Us"]}, + {"count": 2, "value": ["Southgate", "Federal Roads Group"]}, + {"count": 2, "value": ["Southampton", "Pothole Pete"]}, + {"count": 2, "value": ["Powder Springs", "Asphalts R Us"]}, + {"count": 2, "value": ["Middletown", "Federal Roads Group"]}, + {"count": 1, "value": ["Jersey City", "Federal Roads Group"]}, + {"count": 1, "value": ["Billerica", "Pothole Pete"]}, + {"count": 1, "value": ["Phoenix", "Asphalts R Us"]}, + {"count": 1, "value": ["Southampton", "Asphalts R Us"]}, + {"count": 1, "value": ["Southampton", "Federal Roads Group"]}, + {"count": 1, "value": ["Phoenix", "Federal Roads Group"]}, + {"count": 1, "value": ["Muskogee", "Federal Roads Group"]}, + {"count": 1, "value": ["Powder Springs", "Pothole Pete"]}, + {"count": 1, "value": ["Niagara Falls", "Federal Roads Group"]}, + {"count": 1, "value": ["Southgate", "Pothole Pete"]}, + ], + } + + # Get data for multiple dimensions with filters + response = await client_with_repairs_cube.get( + "/cubes/default.repairs_cube_8/dimensions/data", + params={ + "dimensions": ["default.hard_hat.city", "default.dispatcher.company_name"], + "filters": "default.dispatcher.company_name = 'Pothole Pete'", + "include_counts": True, + }, + ) + assert response.json() == { + "cardinality": 5, + "dimensions": ["default.hard_hat.city", "default.dispatcher.company_name"], + "values": [ + {"count": 3, "value": ["Jersey City", "Pothole Pete"]}, + {"count": 2, "value": ["Southampton", "Pothole Pete"]}, + {"count": 1, "value": ["Billerica", "Pothole Pete"]}, + {"count": 1, "value": ["Southgate", "Pothole Pete"]}, + {"count": 1, "value": ["Powder Springs", "Pothole Pete"]}, + ], + } + + # Get data for multiple dimensions with filters and limit + response = await client_with_repairs_cube.get( + "/cubes/default.repairs_cube_8/dimensions/data", + params={ + "dimensions": ["default.hard_hat.city", "default.dispatcher.company_name"], + "filters": "default.dispatcher.company_name = 'Pothole Pete'", + "limit": 4, + "include_counts": True, + }, + ) + assert response.json() == { + "cardinality": 4, + "dimensions": ["default.hard_hat.city", "default.dispatcher.company_name"], + "values": [ + {"count": 3, "value": ["Jersey City", "Pothole Pete"]}, + {"count": 2, "value": ["Southampton", "Pothole Pete"]}, + {"count": 1, "value": ["Billerica", "Pothole Pete"]}, + {"count": 1, "value": ["Southgate", "Pothole Pete"]}, + ], + } + + +@pytest.mark.asyncio +async def test_derive_sql_column(): + """ + Test that SQL column name are properly derived from cube elements + """ + sql_column = await derive_sql_column( + CubeElementMetadata( + name="foo_DOT_bar_DOT_baz_DOT_revenue", + display_name="Revenue", + node_name="foo.bar.baz", + type="metric", + ), + ) + expected_sql_column = ColumnOutput( + name="foo_DOT_bar_DOT_baz_DOT_revenue", + display_name="Revenue", + type="metric", + ) + assert sql_column.name == expected_sql_column.name + assert sql_column.display_name == expected_sql_column.display_name + assert sql_column.type == expected_sql_column.type + sql_column = await derive_sql_column( + CubeElementMetadata( + name="owner", + display_name="Owner", + node_name="foo.bar.baz", + type="dimension", + ), + ) + expected_sql_column = ColumnOutput( + name="foo_DOT_bar_DOT_baz_DOT_owner", + display_name="Owner", + type="dimension", + ) + assert sql_column.name == expected_sql_column.name + assert sql_column.display_name == expected_sql_column.display_name + assert sql_column.type == expected_sql_column.type + + +@pytest.mark.asyncio +async def test_cube_materialization_metadata( + client_with_repairs_cube: AsyncClient, +): + """ + Test building cube materialization metadata + """ + await make_a_test_cube( + client_with_repairs_cube, + "default.example_repairs_cube", + with_materialization=False, + ) + response = await client_with_repairs_cube.get( + "/cubes/default.example_repairs_cube/materialization", + ) + results = response.json() + assert results["message"] == ( + "The cube must have a single temporal partition column set" + " in order for it to be materialized." + ) + + await client_with_repairs_cube.post( + "/nodes/default.example_repairs_cube/columns/default.hard_hat.hire_date/partition", + json={ + "type_": "temporal", + "granularity": "day", + "format": "yyyyMMdd", + }, + ) + response = await client_with_repairs_cube.get( + "/cubes/default.example_repairs_cube/materialization", + ) + results = response.json() + assert results["cube"] == { + "name": "default.example_repairs_cube", + "version": "v1.0", + } + assert results["job"] == "DRUID_CUBE" + assert results["lookback_window"] == "1 DAY" + assert results["schedule"] == "0 0 * * *" + assert results["strategy"] == "incremental_time" + assert results["dimensions"] == [ + "default.hard_hat.country", + "default.hard_hat.postal_code", + "default.hard_hat.city", + "default.hard_hat.hire_date", + "default.hard_hat.state", + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + ] + assert results["metrics"] == [ + { + "derived_expression": "SELECT CAST(sum(discount_sum_62846f49) AS DOUBLE) / " + "SUM(count_3389dae3) AS default_DOT_discounted_orders_rate FROM " + "default.repair_orders_fact", + "metric": { + "name": "default.discounted_orders_rate", + "version": "v1.0", + }, + "required_measures": [ + { + "measure_name": "discount_sum_62846f49", + "node": { + "name": "default.repair_orders_fact", + "version": "v1.0", + }, + }, + { + "measure_name": "count_3389dae3", + "node": { + "name": "default.repair_orders_fact", + "version": "v1.0", + }, + }, + ], + }, + { + "derived_expression": "SELECT SUM(repair_order_id_count_0b7dfba0) FROM " + "default.repair_orders_fact", + "metric": { + "name": "default.num_repair_orders", + "version": "v1.0", + }, + "required_measures": [ + { + "measure_name": "repair_order_id_count_0b7dfba0", + "node": { + "name": "default.repair_orders_fact", + "version": "v1.0", + }, + }, + ], + }, + { + "derived_expression": "SELECT SUM(price_sum_78a5eb43) / SUM(price_count_78a5eb43) FROM " + "default.repair_orders_fact", + "metric": { + "name": "default.avg_repair_price", + "version": "v1.0", + }, + "required_measures": [ + { + "measure_name": "price_count_78a5eb43", + "node": { + "name": "default.repair_orders_fact", + "version": "v1.0", + }, + }, + { + "measure_name": "price_sum_78a5eb43", + "node": { + "name": "default.repair_orders_fact", + "version": "v1.0", + }, + }, + ], + }, + { + "derived_expression": "SELECT sum(total_repair_cost_sum_9bdaf803) FROM " + "default.repair_orders_fact", + "metric": { + "name": "default.total_repair_cost", + "version": "v1.0", + }, + "required_measures": [ + { + "measure_name": "total_repair_cost_sum_9bdaf803", + "node": { + "name": "default.repair_orders_fact", + "version": "v1.0", + }, + }, + ], + }, + { + "derived_expression": "SELECT sum(price_discount_sum_017d55a8) FROM " + "default.repair_orders_fact", + "metric": { + "name": "default.total_repair_order_discounts", + "version": "v1.0", + }, + "required_measures": [ + { + "measure_name": "price_discount_sum_017d55a8", + "node": { + "name": "default.repair_orders_fact", + "version": "v1.0", + }, + }, + ], + }, + { + "derived_expression": "SELECT sum(price_sum_78a5eb43) + sum(price_sum_78a5eb43) AS " + "default_DOT_double_total_repair_cost FROM " + "default.repair_order_details", + "metric": { + "name": "default.double_total_repair_cost", + "version": "v1.0", + }, + "required_measures": [ + { + "measure_name": "price_sum_78a5eb43", + "node": { + "name": "default.repair_order_details", + "version": "v1.0", + }, + }, + ], + }, + ] + assert results["measures_materializations"] == [ + { + "columns": [ + { + "column": "country", + "name": "default_DOT_hard_hat_DOT_country", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.country", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "postal_code", + "name": "default_DOT_hard_hat_DOT_postal_code", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.postal_code", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "city", + "name": "default_DOT_hard_hat_DOT_city", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.city", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "hire_date", + "name": "default_DOT_hard_hat_DOT_hire_date", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.hire_date", + "semantic_type": "dimension", + "type": "timestamp", + }, + { + "column": "state", + "name": "default_DOT_hard_hat_DOT_state", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.state", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "company_name", + "name": "default_DOT_dispatcher_DOT_company_name", + "node": "default.dispatcher", + "semantic_entity": "default.dispatcher.company_name", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "local_region", + "name": "default_DOT_municipality_dim_DOT_local_region", + "node": "default.municipality_dim", + "semantic_entity": "default.municipality_dim.local_region", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "discount_sum_62846f49", + "name": "discount_sum_62846f49", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.discount_sum_62846f49", + "semantic_type": "measure", + "type": "bigint", + }, + { + "column": "count_3389dae3", + "name": "count_3389dae3", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.count_3389dae3", + "semantic_type": "measure", + "type": "bigint", + }, + { + "column": "repair_order_id_count_0b7dfba0", + "name": "repair_order_id_count_0b7dfba0", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.repair_order_id_count_0b7dfba0", + "semantic_type": "measure", + "type": "bigint", + }, + { + "column": "price_count_78a5eb43", + "name": "price_count_78a5eb43", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price_count_78a5eb43", + "semantic_type": "measure", + "type": "bigint", + }, + { + "column": "price_sum_78a5eb43", + "name": "price_sum_78a5eb43", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price_sum_78a5eb43", + "semantic_type": "measure", + "type": "double", + }, + { + "column": "total_repair_cost_sum_9bdaf803", + "name": "total_repair_cost_sum_9bdaf803", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.total_repair_cost_sum_9bdaf803", + "semantic_type": "measure", + "type": "double", + }, + { + "column": "price_discount_sum_017d55a8", + "name": "price_discount_sum_017d55a8", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price_discount_sum_017d55a8", + "semantic_type": "measure", + "type": "double", + }, + ], + "dimensions": [ + "default_DOT_hard_hat_DOT_country", + "default_DOT_hard_hat_DOT_postal_code", + "default_DOT_hard_hat_DOT_city", + "default_DOT_hard_hat_DOT_hire_date", + "default_DOT_hard_hat_DOT_state", + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_municipality_dim_DOT_local_region", + ], + "grain": [ + "default_DOT_hard_hat_DOT_country", + "default_DOT_hard_hat_DOT_postal_code", + "default_DOT_hard_hat_DOT_city", + "default_DOT_hard_hat_DOT_hire_date", + "default_DOT_hard_hat_DOT_state", + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_municipality_dim_DOT_local_region", + ], + "granularity": "day", + "measures": [ + { + "aggregation": "SUM", + "expression": "if(discount > 0.0, 1, 0)", + "name": "discount_sum_62846f49", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "COUNT", + "expression": "*", + "name": "count_3389dae3", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "COUNT", + "expression": "repair_order_id", + "name": "repair_order_id_count_0b7dfba0", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "COUNT", + "expression": "price", + "name": "price_count_78a5eb43", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "SUM", + "expression": "price", + "name": "price_sum_78a5eb43", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "SUM", + "expression": "total_repair_cost", + "name": "total_repair_cost_sum_9bdaf803", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "SUM", + "expression": "price * discount", + "name": "price_discount_sum_017d55a8", + "rule": { + "level": None, + "type": "full", + }, + }, + ], + "node": { + "name": "default.repair_orders_fact", + "version": "v1.0", + }, + "output_table_name": "default_repair_orders_fact_v1_0_c9390406463b348e", + "query": mock.ANY, + "spark_conf": None, + "timestamp_column": "default_DOT_hard_hat_DOT_hire_date", + "timestamp_format": "yyyyMMdd", + "upstream_tables": [ + "default.roads.repair_orders", + "default.roads.repair_order_details", + "default.roads.hard_hats", + "default.roads.dispatchers", + "default.roads.municipality", + "default.roads.municipality_municipality_type", + "default.roads.municipality_type", + ], + }, + { + "columns": [ + { + "column": "country", + "name": "default_DOT_hard_hat_DOT_country", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.country", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "postal_code", + "name": "default_DOT_hard_hat_DOT_postal_code", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.postal_code", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "city", + "name": "default_DOT_hard_hat_DOT_city", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.city", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "hire_date", + "name": "default_DOT_hard_hat_DOT_hire_date", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.hire_date", + "semantic_type": "dimension", + "type": "timestamp", + }, + { + "column": "state", + "name": "default_DOT_hard_hat_DOT_state", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.state", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "company_name", + "name": "default_DOT_dispatcher_DOT_company_name", + "node": "default.dispatcher", + "semantic_entity": "default.dispatcher.company_name", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "local_region", + "name": "default_DOT_municipality_dim_DOT_local_region", + "node": "default.municipality_dim", + "semantic_entity": "default.municipality_dim.local_region", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "price_sum_78a5eb43", + "name": "price_sum_78a5eb43", + "node": "default.repair_order_details", + "semantic_entity": "default.repair_order_details.price_sum_78a5eb43", + "semantic_type": "measure", + "type": "double", + }, + ], + "dimensions": [ + "default_DOT_hard_hat_DOT_country", + "default_DOT_hard_hat_DOT_postal_code", + "default_DOT_hard_hat_DOT_city", + "default_DOT_hard_hat_DOT_hire_date", + "default_DOT_hard_hat_DOT_state", + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_municipality_dim_DOT_local_region", + ], + "grain": [ + "default_DOT_hard_hat_DOT_country", + "default_DOT_hard_hat_DOT_postal_code", + "default_DOT_hard_hat_DOT_city", + "default_DOT_hard_hat_DOT_hire_date", + "default_DOT_hard_hat_DOT_state", + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_municipality_dim_DOT_local_region", + ], + "granularity": "day", + "measures": [ + { + "aggregation": "SUM", + "expression": "price", + "name": "price_sum_78a5eb43", + "rule": { + "level": None, + "type": "full", + }, + }, + ], + "node": { + "name": "default.repair_order_details", + "version": "v1.0", + }, + "output_table_name": "default_repair_order_details_v1_0_5bf367d2fc7c255d", + "query": mock.ANY, + "spark_conf": None, + "timestamp_column": "default_DOT_hard_hat_DOT_hire_date", + "timestamp_format": "yyyyMMdd", + "upstream_tables": [ + "default.roads.repair_order_details", + "default.roads.repair_orders", + "default.roads.hard_hats", + "default.roads.dispatchers", + "default.roads.municipality", + "default.roads.municipality_municipality_type", + "default.roads.municipality_type", + ], + }, + ] + measures_sql = """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE default_DOT_hard_hats.hire_date = CAST(DATE_FORMAT(CAST(DJ_LOGICAL_TIMESTAMP() AS TIMESTAMP), 'yyyyMMdd') AS TIMESTAMP) + ), + default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_municipality_dim AS ( + SELECT + m.municipality_id AS municipality_id, + m.contact_name, + m.contact_title, + m.local_region, + m.state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM roads.municipality AS m LEFT JOIN roads.municipality_municipality_type AS mmt ON m.municipality_id = mmt.municipality_id + LEFT JOIN roads.municipality_type AS mt ON mmt.municipality_type_id = mt.municipality_type_desc + ), + default_DOT_repair_orders_fact_built AS ( + SELECT + default_DOT_repair_orders_fact.repair_order_id, + default_DOT_repair_orders_fact.discount, + default_DOT_repair_orders_fact.price, + default_DOT_repair_orders_fact.total_repair_cost, + default_DOT_hard_hat.country default_DOT_hard_hat_DOT_country, + default_DOT_hard_hat.postal_code default_DOT_hard_hat_DOT_postal_code, + default_DOT_hard_hat.city default_DOT_hard_hat_DOT_city, + default_DOT_hard_hat.hire_date default_DOT_hard_hat_DOT_hire_date, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region default_DOT_municipality_dim_DOT_local_region + FROM default_DOT_repair_orders_fact INNER JOIN default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_municipality_dim ON default_DOT_repair_orders_fact.municipality_id = default_DOT_municipality_dim.municipality_id + ) + + SELECT default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_country, + default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_postal_code, + default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_city, + default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_hire_date, + default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_state, + default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_built.default_DOT_municipality_dim_DOT_local_region, + SUM(if(discount > 0.0, 1, 0)) AS discount_sum_62846f49, + COUNT(*) AS count_3389dae3, + COUNT(repair_order_id) AS repair_order_id_count_0b7dfba0, + COUNT(price) AS price_count_78a5eb43, + SUM(price) AS price_sum_78a5eb43, + SUM(total_repair_cost) AS total_repair_cost_sum_9bdaf803, + SUM(price * discount) AS price_discount_sum_017d55a8 + FROM default_DOT_repair_orders_fact_built + GROUP BY default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_country, default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_postal_code, default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_city, default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_hire_date, default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_state, default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_company_name, default_DOT_repair_orders_fact_built.default_DOT_municipality_dim_DOT_local_region + """ + assert str( + parse( + results["measures_materializations"][0]["query"].replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ), + ), + ) == str(parse(measures_sql)) + + assert results["combiners"] == [ + { + "columns": [ + { + "column": "country", + "name": "default_DOT_hard_hat_DOT_country", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.country", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "postal_code", + "name": "default_DOT_hard_hat_DOT_postal_code", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.postal_code", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "city", + "name": "default_DOT_hard_hat_DOT_city", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.city", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "hire_date", + "name": "default_DOT_hard_hat_DOT_hire_date", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.hire_date", + "semantic_type": "dimension", + "type": "timestamp", + }, + { + "column": "state", + "name": "default_DOT_hard_hat_DOT_state", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.state", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "company_name", + "name": "default_DOT_dispatcher_DOT_company_name", + "node": "default.dispatcher", + "semantic_entity": "default.dispatcher.company_name", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "local_region", + "name": "default_DOT_municipality_dim_DOT_local_region", + "node": "default.municipality_dim", + "semantic_entity": "default.municipality_dim.local_region", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "discount_sum_62846f49", + "name": "discount_sum_62846f49", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.discount_sum_62846f49", + "semantic_type": "measure", + "type": "bigint", + }, + { + "column": "count_3389dae3", + "name": "count_3389dae3", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.count_3389dae3", + "semantic_type": "measure", + "type": "bigint", + }, + { + "column": "repair_order_id_count_0b7dfba0", + "name": "repair_order_id_count_0b7dfba0", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.repair_order_id_count_0b7dfba0", + "semantic_type": "measure", + "type": "bigint", + }, + { + "column": "price_count_78a5eb43", + "name": "price_count_78a5eb43", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price_count_78a5eb43", + "semantic_type": "measure", + "type": "bigint", + }, + { + "column": "price_sum_78a5eb43", + "name": "price_sum_78a5eb43", + "node": "default.repair_order_details", + "semantic_entity": "default.repair_order_details.price_sum_78a5eb43", + "semantic_type": "measure", + "type": "double", + }, + { + "column": "total_repair_cost_sum_9bdaf803", + "name": "total_repair_cost_sum_9bdaf803", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.total_repair_cost_sum_9bdaf803", + "semantic_type": "measure", + "type": "double", + }, + { + "column": "price_discount_sum_017d55a8", + "name": "price_discount_sum_017d55a8", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price_discount_sum_017d55a8", + "semantic_type": "measure", + "type": "double", + }, + { + "column": "price_sum_78a5eb43", + "name": "price_sum_78a5eb43", + "node": "default.repair_order_details", + "semantic_entity": "default.repair_order_details.price_sum_78a5eb43", + "semantic_type": "measure", + "type": "double", + }, + ], + "dimensions": [ + "default_DOT_hard_hat_DOT_country", + "default_DOT_hard_hat_DOT_postal_code", + "default_DOT_hard_hat_DOT_city", + "default_DOT_hard_hat_DOT_hire_date", + "default_DOT_hard_hat_DOT_state", + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_municipality_dim_DOT_local_region", + ], + "druid_spec": { + "dataSchema": { + "dataSource": "dj__default_example_repairs_cube_v1_0_41f1198b6e6032dd", + "granularitySpec": { + "intervals": [], + "segmentGranularity": "DAY", + "type": "uniform", + }, + "metricsSpec": [ + { + "fieldName": "discount_sum_62846f49", + "name": "discount_sum_62846f49", + "type": "longSum", + }, + { + "fieldName": "count_3389dae3", + "name": "count_3389dae3", + "type": "longSum", + }, + { + "fieldName": "repair_order_id_count_0b7dfba0", + "name": "repair_order_id_count_0b7dfba0", + "type": "longSum", + }, + { + "fieldName": "price_count_78a5eb43", + "name": "price_count_78a5eb43", + "type": "longSum", + }, + { + "fieldName": "price_sum_78a5eb43", + "name": "price_sum_78a5eb43", + "type": "doubleSum", + }, + { + "fieldName": "total_repair_cost_sum_9bdaf803", + "name": "total_repair_cost_sum_9bdaf803", + "type": "doubleSum", + }, + { + "fieldName": "price_discount_sum_017d55a8", + "name": "price_discount_sum_017d55a8", + "type": "doubleSum", + }, + { + "fieldName": "price_sum_78a5eb43", + "name": "price_sum_78a5eb43", + "type": "doubleSum", + }, + ], + "parser": { + "parseSpec": { + "dimensionsSpec": { + "dimensions": [ + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_hard_hat_DOT_city", + "default_DOT_hard_hat_DOT_country", + "default_DOT_hard_hat_DOT_hire_date", + "default_DOT_hard_hat_DOT_postal_code", + "default_DOT_hard_hat_DOT_state", + "default_DOT_municipality_dim_DOT_local_region", + ], + }, + "format": "parquet", + "timestampSpec": { + "column": "default_DOT_hard_hat_DOT_hire_date", + "format": "yyyyMMdd", + }, + }, + }, + }, + "tuningConfig": { + "partitionsSpec": { + "targetPartitionSize": 5000000, + "type": "hashed", + }, + "type": "hadoop", + "useCombiner": True, + }, + }, + "grain": [ + "default_DOT_hard_hat_DOT_country", + "default_DOT_hard_hat_DOT_postal_code", + "default_DOT_hard_hat_DOT_city", + "default_DOT_hard_hat_DOT_hire_date", + "default_DOT_hard_hat_DOT_state", + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_municipality_dim_DOT_local_region", + ], + "granularity": "day", + "measures": [ + { + "aggregation": "SUM", + "expression": "if(discount > 0.0, 1, 0)", + "name": "discount_sum_62846f49", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "COUNT", + "expression": "*", + "name": "count_3389dae3", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "COUNT", + "expression": "repair_order_id", + "name": "repair_order_id_count_0b7dfba0", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "COUNT", + "expression": "price", + "name": "price_count_78a5eb43", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "SUM", + "expression": "price", + "name": "price_sum_78a5eb43", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "SUM", + "expression": "total_repair_cost", + "name": "total_repair_cost_sum_9bdaf803", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "SUM", + "expression": "price * discount", + "name": "price_discount_sum_017d55a8", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "SUM", + "expression": "price", + "name": "price_sum_78a5eb43", + "rule": { + "level": None, + "type": "full", + }, + }, + ], + "node": { + "name": "default.example_repairs_cube", + "version": "v1.0", + }, + "output_table_name": "default_example_repairs_cube_v1_0_41f1198b6e6032dd", + "query": mock.ANY, + "timestamp_column": "default_DOT_hard_hat_DOT_hire_date", + "timestamp_format": "yyyyMMdd", + "upstream_tables": [], + }, + ] + + expected_combiner = """ + SELECT + COALESCE( + default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_hard_hat_DOT_country, + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_hard_hat_DOT_country + ) default_DOT_hard_hat_DOT_country, + COALESCE( + default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_hard_hat_DOT_postal_code, + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_hard_hat_DOT_postal_code + ) default_DOT_hard_hat_DOT_postal_code, + COALESCE( + default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_hard_hat_DOT_city, + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_hard_hat_DOT_city + ) default_DOT_hard_hat_DOT_city, + COALESCE( + default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_hard_hat_DOT_hire_date, + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_hard_hat_DOT_hire_date + ) default_DOT_hard_hat_DOT_hire_date, + COALESCE( + default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_hard_hat_DOT_state, + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_hard_hat_DOT_state + ) default_DOT_hard_hat_DOT_state, + COALESCE( + default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_dispatcher_DOT_company_name, + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_dispatcher_DOT_company_name + ) default_DOT_dispatcher_DOT_company_name, + COALESCE( + default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_municipality_dim_DOT_local_region, + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_municipality_dim_DOT_local_region + ) default_DOT_municipality_dim_DOT_local_region, + default_repair_orders_fact_v1_0_c9390406463b348e.discount_sum_62846f49, + default_repair_orders_fact_v1_0_c9390406463b348e.count_3389dae3, + default_repair_orders_fact_v1_0_c9390406463b348e.repair_order_id_count_0b7dfba0, + default_repair_orders_fact_v1_0_c9390406463b348e.price_count_78a5eb43, + default_repair_orders_fact_v1_0_c9390406463b348e.price_sum_78a5eb43, + default_repair_orders_fact_v1_0_c9390406463b348e.total_repair_cost_sum_9bdaf803, + default_repair_orders_fact_v1_0_c9390406463b348e.price_discount_sum_017d55a8, + default_repair_order_details_v1_0_5bf367d2fc7c255d.price_sum_78a5eb43 + FROM default_repair_orders_fact_v1_0_c9390406463b348e + FULL OUTER JOIN default_repair_order_details_v1_0_5bf367d2fc7c255d + ON default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_hard_hat_DOT_country = + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_hard_hat_DOT_country + AND default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_hard_hat_DOT_postal_code = + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_hard_hat_DOT_postal_code + AND default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_hard_hat_DOT_city = + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_hard_hat_DOT_city + AND default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_hard_hat_DOT_hire_date = + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_hard_hat_DOT_hire_date + AND default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_hard_hat_DOT_state = + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_hard_hat_DOT_state + AND default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_dispatcher_DOT_company_name = + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_dispatcher_DOT_company_name + AND default_repair_orders_fact_v1_0_c9390406463b348e.default_DOT_municipality_dim_DOT_local_region = + default_repair_order_details_v1_0_5bf367d2fc7c255d.default_DOT_municipality_dim_DOT_local_region + """ + assert str(parse(results["combiners"][0]["query"])) == str(parse(expected_combiner)) diff --git a/datajunction-server/tests/api/data_test.py b/datajunction-server/tests/api/data_test.py new file mode 100644 index 000000000..74bd6b0ae --- /dev/null +++ b/datajunction-server/tests/api/data_test.py @@ -0,0 +1,1746 @@ +""" +Tests for the data API. +""" + +from typing import Dict, List, Optional +from unittest import mock + +import pytest +from httpx import AsyncClient +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload + +from datajunction_server.database import QueryRequest +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.queryrequest import QueryBuildType +from datajunction_server.models.node import AvailabilityStateBase + + +class TestDataForNode: + """ + Test ``POST /data/{node_name}/``. + """ + + @pytest.mark.asyncio + async def test_get_dimension_data_failed( + self, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test trying to get dimensions data while setting dimensions + """ + response = await module__client_with_account_revenue.get( + "/data/default.payment_type/", + params={ + "dimensions": ["something"], + "filters": [], + }, + ) + data = response.json() + assert response.status_code == 422 + assert ( + "something are not available dimensions on default.payment_type" + in data["message"] + ) + + @pytest.mark.asyncio + async def test_get_dimension_data( + self, + module__client_with_account_revenue, + ) -> None: + """ + Test trying to get dimensions data while setting dimensions + """ + response = await module__client_with_account_revenue.get( + "/data/default.payment_type/", + ) + data = response.json() + assert response.status_code == 200 + assert data == { + "engine_name": None, + "engine_version": None, + "errors": [], + "executed_query": None, + "finished": None, + "id": "bd98d6be-e2d2-413e-94c7-96d9411ddee2", + "links": None, + "next": None, + "output_table": None, + "previous": None, + "progress": 0.0, + "results": [ + { + "columns": [ + { + "column": "id", + "name": "default_DOT_payment_type_DOT_id", + "node": "default.payment_type", + "semantic_type": None, + "semantic_entity": "default.payment_type.id", + "type": "int", + }, + { + "column": "payment_type_name", + "name": "default_DOT_payment_type_DOT_payment_type_name", + "node": "default.payment_type", + "semantic_type": None, + "semantic_entity": "default.payment_type.payment_type_name", + "type": "string", + }, + { + "column": "payment_type_classification", + "name": "default_DOT_payment_type_DOT_payment_type_classification", + "node": "default.payment_type", + "semantic_type": None, + "semantic_entity": "default.payment_type.payment_type_classification", + "type": "string", + }, + ], + "row_count": 0, + "rows": [[1, "VISA", "CARD"], [2, "MASTERCARD", "CARD"]], + "sql": mock.ANY, + }, + ], + "scheduled": None, + "started": None, + "state": "FINISHED", + "submitted_query": mock.ANY, + } + + @pytest.mark.asyncio + async def test_get_source_data( + self, + module__client_with_account_revenue, + ) -> None: + """ + Test retrieving data for a source node + """ + response = await module__client_with_account_revenue.get( + "/data/default.revenue/", + ) + data = response.json() + assert response.status_code == 200 + assert data == { + "engine_name": None, + "engine_version": None, + "errors": [], + "executed_query": None, + "finished": None, + "id": "bd98d6be-e2d2-413e-94c7-96d9411ddee2", + "links": None, + "next": None, + "output_table": None, + "previous": None, + "progress": 0.0, + "results": [ + { + "columns": [ + { + "column": "payment_id", + "name": "default_DOT_revenue_DOT_payment_id", + "node": "default.revenue", + "semantic_entity": "default.revenue.payment_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "payment_amount", + "name": "default_DOT_revenue_DOT_payment_amount", + "node": "default.revenue", + "semantic_entity": "default.revenue.payment_amount", + "semantic_type": None, + "type": "float", + }, + { + "column": "payment_type", + "name": "default_DOT_revenue_DOT_payment_type", + "node": "default.revenue", + "semantic_entity": "default.revenue.payment_type", + "semantic_type": None, + "type": "int", + }, + { + "column": "customer_id", + "name": "default_DOT_revenue_DOT_customer_id", + "node": "default.revenue", + "semantic_entity": "default.revenue.customer_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "account_type", + "name": "default_DOT_revenue_DOT_account_type", + "node": "default.revenue", + "semantic_entity": "default.revenue.account_type", + "semantic_type": None, + "type": "string", + }, + ], + "row_count": 0, + "rows": [ + [1, 25.5, 1, 2, "ACTIVE"], + [2, 12.5, 2, 2, "INACTIVE"], + [3, 89.0, 1, 3, "ACTIVE"], + [4, 1293.199951171875, 2, 2, "ACTIVE"], + [5, 23.0, 1, 4, "INACTIVE"], + [6, 398.1300048828125, 2, 3, "ACTIVE"], + [7, 239.6999969482422, 2, 4, "ACTIVE"], + ], + "sql": "SELECT payment_id default_DOT_revenue_DOT_payment_id,\n" + "\tpayment_amount " + "default_DOT_revenue_DOT_payment_amount,\n" + "\tpayment_type default_DOT_revenue_DOT_payment_type,\n" + "\tcustomer_id default_DOT_revenue_DOT_customer_id,\n" + "\taccount_type default_DOT_revenue_DOT_account_type \n" + " FROM accounting.revenue\n", + }, + ], + "scheduled": None, + "started": None, + "state": "FINISHED", + "submitted_query": "SELECT payment_id default_DOT_revenue_DOT_payment_id,\n" + "\tpayment_amount default_DOT_revenue_DOT_payment_amount,\n" + "\tpayment_type default_DOT_revenue_DOT_payment_type,\n" + "\tcustomer_id default_DOT_revenue_DOT_customer_id,\n" + "\taccount_type default_DOT_revenue_DOT_account_type \n" + " FROM accounting.revenue\n", + } + + @pytest.mark.asyncio + async def test_get_transform_data( + self, + module__client_with_roads, + ) -> None: + """ + Test retrieving data for a transform node + """ + response = await module__client_with_roads.get( + "/data/default.repair_orders_fact/?limit=2", + ) + data = response.json() + assert response.status_code == 200 + assert data == { + "engine_name": None, + "engine_version": None, + "errors": [], + "executed_query": None, + "finished": None, + "id": "bd98d6be-e2d2-413e-94c7-96d9411ddee2", + "links": None, + "next": None, + "output_table": None, + "previous": None, + "progress": 0.0, + "results": [ + { + "columns": [ + { + "column": "repair_order_id", + "name": "default_DOT_repair_orders_fact_DOT_repair_order_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.repair_order_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "municipality_id", + "name": "default_DOT_repair_orders_fact_DOT_municipality_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.municipality_id", + "semantic_type": None, + "type": "string", + }, + { + "column": "hard_hat_id", + "name": "default_DOT_repair_orders_fact_DOT_hard_hat_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.hard_hat_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "dispatcher_id", + "name": "default_DOT_repair_orders_fact_DOT_dispatcher_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.dispatcher_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "order_date", + "name": "default_DOT_repair_orders_fact_DOT_order_date", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.order_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatched_date", + "name": "default_DOT_repair_orders_fact_DOT_dispatched_date", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.dispatched_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "required_date", + "name": "default_DOT_repair_orders_fact_DOT_required_date", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.required_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "discount", + "name": "default_DOT_repair_orders_fact_DOT_discount", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.discount", + "semantic_type": None, + "type": "float", + }, + { + "column": "price", + "name": "default_DOT_repair_orders_fact_DOT_price", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price", + "semantic_type": None, + "type": "float", + }, + { + "column": "quantity", + "name": "default_DOT_repair_orders_fact_DOT_quantity", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.quantity", + "semantic_type": None, + "type": "int", + }, + { + "column": "repair_type_id", + "name": "default_DOT_repair_orders_fact_DOT_repair_type_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.repair_type_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "total_repair_cost", + "name": "default_DOT_repair_orders_fact_DOT_total_repair_cost", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.total_repair_cost", + "semantic_type": None, + "type": "float", + }, + { + "column": "time_to_dispatch", + "name": "default_DOT_repair_orders_fact_DOT_time_to_dispatch", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.time_to_dispatch", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatch_delay", + "name": "default_DOT_repair_orders_fact_DOT_dispatch_delay", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.dispatch_delay", + "semantic_type": None, + "type": "timestamp", + }, + ], + "row_count": 0, + "rows": [ + [ + 10001, + "New York", + 1, + 3, + "2007-07-04", + "2007-12-01", + "2009-07-18", + 0.05000000074505806, + 63708.0, + 1, + 1, + 63708.0, + 150, + -595, + ], + [ + 10002, + "New York", + 3, + 1, + "2007-07-05", + "2007-12-01", + "2009-08-28", + 0.05000000074505806, + 67253.0, + 1, + 4, + 67253.0, + 149, + -636, + ], + ], + "sql": mock.ANY, + }, + ], + "scheduled": None, + "started": None, + "state": "FINISHED", + "submitted_query": mock.ANY, + } + + @pytest.mark.asyncio + async def test_get_metric_data( + self, + module__client_with_roads, + ) -> None: + """ + Test retrieving data for a metric + """ + response = await module__client_with_roads.get( + "/data/default.num_repair_orders/", + ) + data = response.json() + assert response.status_code == 200 + assert data == { + "engine_name": None, + "engine_version": None, + "errors": [], + "executed_query": None, + "finished": None, + "id": "bd98d6be-e2d2-413e-94c7-96d9411ddee2", + "links": None, + "next": None, + "output_table": None, + "previous": None, + "progress": 0.0, + "results": [ + { + "columns": [ + { + "column": "default_DOT_num_repair_orders", + "name": "default_DOT_num_repair_orders", + "node": "default.num_repair_orders", + "semantic_entity": ( + "default.num_repair_orders.default_DOT_num_repair_orders" + ), + "semantic_type": "metric", + "type": "bigint", + }, + ], + "row_count": 0, + "rows": [[25]], + "sql": mock.ANY, + }, + ], + "scheduled": None, + "started": None, + "state": "FINISHED", + "submitted_query": mock.ANY, + } + + @pytest.mark.asyncio + async def test_get_multiple_metrics_and_dimensions_data( + self, + module__client_with_roads, + ) -> None: + """ + Test getting multiple metrics and dimensions + """ + response = await module__client_with_roads.get( + "/data?metrics=default.num_repair_orders&metrics=" + "default.avg_repair_price&dimensions=default.dispatcher.company_name&limit=10", + ) + data = response.json() + assert response.status_code == 200 + assert data == { + "engine_name": None, + "engine_version": None, + "errors": [], + "executed_query": None, + "finished": None, + "id": "bd98d6be-e2d2-413e-94c7-96d9411ddee2", + "links": None, + "next": None, + "output_table": None, + "previous": None, + "progress": 0.0, + "results": [ + { + "columns": [ + { + "column": "company_name", + "name": "default_DOT_dispatcher_DOT_company_name", + "node": "default.dispatcher", + "semantic_entity": "default.dispatcher.company_name", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "default_DOT_num_repair_orders", + "name": "default_DOT_num_repair_orders", + "node": "default.num_repair_orders", + "semantic_entity": ( + "default.num_repair_orders.default_DOT_num_repair_orders" + ), + "semantic_type": "metric", + "type": "bigint", + }, + { + "column": "default_DOT_avg_repair_price", + "name": "default_DOT_avg_repair_price", + "node": "default.avg_repair_price", + "semantic_entity": ( + "default.avg_repair_price.default_DOT_avg_repair_price" + ), + "semantic_type": "metric", + "type": "double", + }, + ], + "row_count": 0, + "rows": [ + ["Federal Roads Group", 9, 51913.88888888889], + ["Pothole Pete", 8, 62205.875], + ["Asphalts R Us", 8, 68914.75], + ], + "sql": mock.ANY, + }, + ], + "scheduled": None, + "started": None, + "state": "FINISHED", + "submitted_query": mock.ANY, + } + + @pytest.mark.asyncio + async def test_stream_multiple_metrics_and_dimensions_data( + self, + module__session: AsyncSession, + module__client_with_roads, + ) -> None: + """ + Test streaming query status for + (a) multiple metrics and dimensions and + (b) node data + """ + async with module__client_with_roads.stream( + "GET", + "/stream?metrics=default.num_repair_orders&metrics=" + "default.avg_repair_price&dimensions=default.dispatcher.company_name&limit=10", + headers={ + "Accept": "text/event-stream", + }, + ) as response: + assert response.status_code == 200 + full_text = "".join([text async for text in response.aiter_text()]) + assert "event: message" in full_text + assert "avg(repair_order_details.price)" in full_text + + # Test streaming of node data for a metric + async with module__client_with_roads.stream( + "GET", + "/stream/default.num_repair_orders?dimensions=default.dispatcher.company_name&limit=10", + headers={ + "Accept": "text/event-stream", + }, + ) as response: + assert response.status_code == 200 + full_text = "".join([text async for text in response.aiter_text()]) + assert "event: message" in full_text + assert "count(default_DOT_repair_orders_fact.repair_order_id)" in full_text + + # Test streaming of node data for a transform + async with module__client_with_roads.stream( + "GET", + "/stream/default.repair_orders_fact?" + "dimensions=default.dispatcher.company_name&limit=10", + headers={ + "Accept": "text/event-stream", + }, + ) as response: + assert response.status_code == 200 + full_text = "\n".join([text async for text in response.aiter_lines()]) + assert "event: message" in full_text + assert "SELECT default_DOT_repair_orders_fact.repair_order_id" in full_text + + # Check that the query request for the above transform has an external query id saved + query_request = await QueryRequest.get_query_request( + session=module__session, + query_type=QueryBuildType.NODE, + nodes=["default.repair_orders_fact"], + dimensions=["default.dispatcher.company_name"], + engine_name=None, + engine_version=None, + filters=[], + limit=10, + orderby=[], + ) + assert query_request.query_id == "bd98d6be-e2d2-413e-94c7-96d9411ddee2" # type: ignore + + # Hit the same SSE stream again + async with module__client_with_roads.stream( + "GET", + "/stream/default.repair_orders_fact?" + "dimensions=default.dispatcher.company_name&limit=10", + headers={ + "Accept": "text/event-stream", + }, + ) as response: + assert response.status_code == 200 + full_text = "\n".join([text async for text in response.aiter_lines()]) + assert "event: message" in full_text + assert "SELECT default_DOT_repair_orders_fact.repair_order_id" in full_text + + @pytest.mark.asyncio + async def test_get_data_for_query_id( + self, + module__client_with_roads, + ) -> None: + """ + Test retrieving data for a query ID + """ + # run some query + response = await module__client_with_roads.get( + "/data/default.num_repair_orders/", + ) + data = response.json() + assert response.status_code == 200 + assert data["id"] == "bd98d6be-e2d2-413e-94c7-96d9411ddee2" + + # and try to get the results by the query id only + new_response = await module__client_with_roads.get(f"/data/query/{data['id']}/") + new_data = response.json() + assert new_response.status_code == 200 + assert new_data["results"] == [ + { + "columns": [ + { + "column": "default_DOT_num_repair_orders", + "name": "default_DOT_num_repair_orders", + "node": "default.num_repair_orders", + "semantic_entity": ( + "default.num_repair_orders.default_DOT_num_repair_orders" + ), + "semantic_type": "metric", + "type": "bigint", + }, + ], + "row_count": 0, + "rows": [[25]], + "sql": mock.ANY, + }, + ] + + # and repeat for a bogus query id + yet_another_response = await module__client_with_roads.get( + "/data/query/foo-bar-baz/", + ) + assert yet_another_response.status_code == 404 + assert "Query foo-bar-baz not found." in yet_another_response.text + + +class TestAvailabilityState: + """ + Test ``POST /data/{node_name}/availability/``. + """ + + @pytest.mark.asyncio + async def test_setting_availability_state( + self, + module__session: AsyncSession, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test adding an availability state + """ + response = await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_and_business_only/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "pmts", + "valid_through_ts": 20230125, + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + "custom_metadata": { + "url": "http://some.catalog.com/default.accounting.pmts", + }, + }, + ) + data = response.json() + + assert response.status_code == 200 + assert data == {"message": "Availability state successfully posted"} + + # Check that the history tracker has been updated + response = await module__client_with_account_revenue.get( + "/history?node=default.large_revenue_payments_and_business_only", + ) + data = response.json() + availability_activities = [ + activity for activity in data if activity["entity_type"] == "availability" + ] + assert availability_activities == [ + { + "activity_type": "create", + "created_at": mock.ANY, + "details": {}, + "entity_name": None, + "node": "default.large_revenue_payments_and_business_only", + "entity_type": "availability", + "id": mock.ANY, + "post": { + "catalog": "default", + "categorical_partitions": [], + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + "partitions": [], + "schema_": "accounting", + "table": "pmts", + "temporal_partitions": [], + "valid_through_ts": 20230125, + "custom_metadata": { + "url": "http://some.catalog.com/default.accounting.pmts", + }, + "materialization_id": None, + }, + "pre": {}, + "user": "dj", + }, + ] + + large_revenue_payments_and_business_only = await Node.get_by_name( + module__session, + "default.large_revenue_payments_and_business_only", + ) + node_dict = AvailabilityStateBase.from_orm( + large_revenue_payments_and_business_only.current.availability, # type: ignore + ).dict() + assert node_dict == { + "valid_through_ts": 20230125, + "catalog": "default", + "min_temporal_partition": ["2022", "01", "01"], + "table": "pmts", + "max_temporal_partition": ["2023", "01", "25"], + "partitions": [], + "schema_": "accounting", + "categorical_partitions": [], + "temporal_partitions": [], + "custom_metadata": { + "url": "http://some.catalog.com/default.accounting.pmts", + }, + "materialization_id": None, + } + + @pytest.mark.asyncio + async def test_availability_catalog_mismatch( + self, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test that setting availability works even when the catalogs do not match + """ + response = await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_and_business_only/availability/", + json={ + "catalog": "public", + "schema_": "accounting", + "table": "pmts", + "valid_through_ts": 20230125, + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + }, + ) + data = response.json() + + assert response.status_code == 200 + assert data["message"] == "Availability state successfully posted" + + @pytest.mark.asyncio + async def test_setting_availability_state_multiple_times( + self, + module__session: AsyncSession, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test adding multiple availability states + """ + response = await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_and_business_only_1/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "pmts", + "valid_through_ts": 20230125, + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + }, + ) + data = response.json() + + assert response.status_code == 200 + assert data == {"message": "Availability state successfully posted"} + + response = await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_and_business_only_1/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "pmts", + "valid_through_ts": 20230125, + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + }, + ) + data = response.json() + + assert response.status_code == 200 + assert data == {"message": "Availability state successfully posted"} + + response = await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_and_business_only_1/availability/", + json={ + "catalog": "default", + "schema_": "new_accounting", + "table": "new_payments_table", + "valid_through_ts": 20230125, + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + "categorical_partitions": [], + "temporal_partitions": [], + }, + ) + data = response.json() + + assert response.status_code == 200 + assert data == {"message": "Availability state successfully posted"} + + # Check that the history tracker has been updated + response = await module__client_with_account_revenue.get( + "/history/?node=default.large_revenue_payments_and_business_only_1", + ) + data = response.json() + availability_activities = [ + activity for activity in data if activity["entity_type"] == "availability" + ] + assert availability_activities == [ + { + "activity_type": "create", + "created_at": mock.ANY, + "details": {}, + "entity_name": None, + "node": "default.large_revenue_payments_and_business_only_1", + "entity_type": "availability", + "id": mock.ANY, + "post": { + "catalog": "default", + "categorical_partitions": [], + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + "partitions": [], + "schema_": "new_accounting", + "table": "new_payments_table", + "temporal_partitions": [], + "valid_through_ts": 20230125, + "custom_metadata": {}, + "materialization_id": None, + }, + "pre": { + "catalog": "default", + "categorical_partitions": [], + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + "partitions": [], + "schema_": "accounting", + "table": "pmts", + "temporal_partitions": [], + "valid_through_ts": 20230125, + "custom_metadata": {}, + "materialization_id": None, + }, + "user": "dj", + }, + { + "activity_type": "create", + "created_at": mock.ANY, + "details": {}, + "entity_name": None, + "node": "default.large_revenue_payments_and_business_only_1", + "entity_type": "availability", + "id": mock.ANY, + "post": { + "catalog": "default", + "categorical_partitions": [], + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + "partitions": [], + "schema_": "accounting", + "table": "pmts", + "temporal_partitions": [], + "valid_through_ts": 20230125, + "custom_metadata": {}, + "materialization_id": None, + }, + "pre": { + "catalog": "default", + "categorical_partitions": [], + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + "partitions": [], + "schema_": "accounting", + "table": "pmts", + "temporal_partitions": [], + "valid_through_ts": 20230125, + "custom_metadata": {}, + "materialization_id": None, + }, + "user": "dj", + }, + { + "activity_type": "create", + "created_at": mock.ANY, + "details": {}, + "entity_name": None, + "node": "default.large_revenue_payments_and_business_only_1", + "entity_type": "availability", + "id": mock.ANY, + "post": { + "catalog": "default", + "categorical_partitions": [], + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + "partitions": [], + "schema_": "accounting", + "table": "pmts", + "temporal_partitions": [], + "valid_through_ts": 20230125, + "custom_metadata": {}, + "materialization_id": None, + }, + "pre": {}, + "user": "dj", + }, + ] + + large_revenue_payments_and_business_only = await Node.get_by_name( + module__session, + "default.large_revenue_payments_and_business_only_1", + ) + node_dict = AvailabilityStateBase.from_orm( + large_revenue_payments_and_business_only.current.availability, # type: ignore + ).dict() + assert node_dict == { + "valid_through_ts": 20230125, + "catalog": "default", + "min_temporal_partition": ["2022", "01", "01"], + "table": "new_payments_table", + "max_temporal_partition": ["2023", "01", "25"], + "partitions": [], + "schema_": "new_accounting", + "categorical_partitions": [], + "temporal_partitions": [], + "custom_metadata": {}, + "materialization_id": None, + } + + @pytest.mark.asyncio + async def test_that_update_at_timestamp_is_being_updated( + self, + module__session: AsyncSession, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test that the `updated_at` attribute is being updated + """ + response = await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_and_business_only/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "pmts", + "valid_through_ts": 20230125, + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + }, + ) + assert response.status_code == 200 + large_revenue_payments_and_business_only = await Node.get_by_name( + module__session, + "default.large_revenue_payments_and_business_only", + ) + updated_at_1 = ( + large_revenue_payments_and_business_only.current.availability.updated_at # type: ignore + ) + + response = await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_and_business_only/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "pmts", + "valid_through_ts": 20230125, + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + }, + ) + assert response.status_code == 200 + + large_revenue_payments_and_business_only = await Node.get_by_name( + module__session, + "default.large_revenue_payments_and_business_only", + ) + updated_at_2 = ( + large_revenue_payments_and_business_only.current.availability.updated_at # type: ignore + ) + + assert updated_at_2 > updated_at_1 + + @pytest.mark.asyncio + async def test_raising_when_node_does_not_exist( + self, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test raising when setting availability state on non-existent node + """ + response = await module__client_with_account_revenue.post( + "/data/default.nonexistentnode/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "pmts", + "valid_through_ts": 20230125, + "max_temporal_partition": [20230125], + "min_temporal_partition": [20220101], + }, + ) + data = response.json() + + assert response.status_code == 404 + assert data == { + "message": "A node with name `default.nonexistentnode` does not exist.", + "errors": [], + "warnings": [], + } + + @pytest.mark.asyncio + async def test_merging_in_a_higher_max_partition( + self, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test that the higher max_partition value is used when merging in an availability state + """ + await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_only/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "large_pmts", + "valid_through_ts": 1709827200000, + "temporal_partitions": ["payment_id"], + "max_temporal_partition": [20230101], + "min_temporal_partition": [20220101], + }, + ) + response = await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_only/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "large_pmts", + "valid_through_ts": 1710097200000, + "temporal_partitions": ["payment_id"], + "max_temporal_partition": [ + 20230102, + ], # should be used since it's a higher max_temporal_partition + "min_temporal_partition": [ + 20230102, + ], # should be ignored since it's a higher min_temporal_partition + }, + ) + data = response.json() + + assert response.status_code == 200 + assert data == {"message": "Availability state successfully posted"} + + large_revenue_payments_only = ( + await module__client_with_account_revenue.get( + "/nodes/default.large_revenue_payments_only", + ) + ).json() + assert large_revenue_payments_only["availability"] == { + "valid_through_ts": 1710097200000, + "catalog": "default", + "min_temporal_partition": ["20220101"], + "table": "large_pmts", + "max_temporal_partition": ["20230102"], + "schema_": "accounting", + "partitions": [], + "categorical_partitions": [], + "temporal_partitions": ["payment_id"], + "custom_metadata": {}, + "materialization_id": None, + } + + @pytest.fixture + def post_local_hard_hats_availability(self, module__client_with_roads: AsyncClient): + """ + Fixture for posting availability for local_hard_hats + """ + + async def _post( + node_name: str = "default.local_hard_hats", + min_temporal_partition: Optional[List[str]] = None, + max_temporal_partition: Optional[List[str]] = None, + partitions: List[Dict] = None, + categorical_partitions: List[str] = None, + ): + if categorical_partitions is None: + categorical_partitions = ["country", "postal_code"] + return await module__client_with_roads.post( + f"/data/{node_name}/availability/", + json={ + "catalog": "default", + "schema_": "dimensions", + "table": "local_hard_hats", + "valid_through_ts": 20230101, + "categorical_partitions": categorical_partitions, + "temporal_partitions": ["birth_date"], + "min_temporal_partition": min_temporal_partition, + "max_temporal_partition": max_temporal_partition, + "partitions": partitions, + }, + ) + + return _post + + @pytest.mark.asyncio + async def test_set_temporal_only_availability( + self, + module__client_with_roads: AsyncClient, + post_local_hard_hats_availability, + ): + """ + Test setting availability on a node where it only has temporal partitions and + no categorical partitions. + """ + await post_local_hard_hats_availability( + min_temporal_partition=["20230101"], + max_temporal_partition=["20230105"], + partitions=[], + categorical_partitions=[], + ) + await post_local_hard_hats_availability( + min_temporal_partition=["20230101"], + max_temporal_partition=["20230110"], + partitions=[], + categorical_partitions=[], + ) + + response = await module__client_with_roads.get( + "/nodes/default.local_hard_hats/", + ) + assert response.json()["availability"] == { + "catalog": "default", + "min_temporal_partition": ["20230101"], + "max_temporal_partition": ["20230110"], + "categorical_partitions": [], + "temporal_partitions": ["birth_date"], + "partitions": [], + "schema_": "dimensions", + "table": "local_hard_hats", + "valid_through_ts": 20230101, + "custom_metadata": {}, + "materialization_id": None, + } + + @pytest.mark.asyncio + async def test_set_node_level_availability_wider_time_range( + self, + module__client_with_roads: AsyncClient, + post_local_hard_hats_availability, + ): + """ + The node starts off with partition-level availability with a specific time range. + We add in a node-level availability with a wider time range than at the partition + level. We expect this new availability state to overwrite the partition-level + availability. + """ + # Set initial availability state + await post_local_hard_hats_availability( + partitions=[ + { + "value": ["DE", "ABC123D"], + "min_temporal_partition": ["20230101"], + "max_temporal_partition": ["20230105"], + "valid_through_ts": 20230101, + }, + ], + ) + # Post wider availability + await post_local_hard_hats_availability( + min_temporal_partition=["20230101"], + max_temporal_partition=["20230110"], + partitions=[], + ) + + response = await module__client_with_roads.get( + "/nodes/default.local_hard_hats/", + ) + assert response.json()["availability"] == { + "catalog": "default", + "min_temporal_partition": ["20230101"], + "max_temporal_partition": ["20230110"], + "categorical_partitions": ["country", "postal_code"], + "temporal_partitions": ["birth_date"], + "partitions": [], + "schema_": "dimensions", + "table": "local_hard_hats", + "valid_through_ts": 20230101, + "custom_metadata": {}, + "materialization_id": None, + } + + @pytest.mark.asyncio + async def test_set_node_level_availability_smaller_time_range( + self, + module__client_with_roads: AsyncClient, + post_local_hard_hats_availability, + ): + """ + Set a node level availability with a smaller time range than the existing + one will result in no change to the merged availability state + """ + await post_local_hard_hats_availability( + min_temporal_partition=["20230101"], + max_temporal_partition=["20230110"], + partitions=[], + ) + + await post_local_hard_hats_availability( + min_temporal_partition=["20230103"], + max_temporal_partition=["20230105"], + partitions=[], + ) + + response = await module__client_with_roads.get( + "/nodes/default.local_hard_hats/", + ) + availability = response.json()["availability"] + assert availability["min_temporal_partition"] == ["20230101"] + assert availability["max_temporal_partition"] == ["20230110"] + assert availability["partitions"] == [] + + @pytest.mark.asyncio + async def test_set_partition_level_availability_smaller_time_range( + self, + module__client_with_roads: AsyncClient, + post_local_hard_hats_availability, + ): + """ + Set a partition-level availability with a smaller time range than + the existing node-level time range will result in no change to the + merged availability state + """ + await post_local_hard_hats_availability( + min_temporal_partition=["20230101"], + max_temporal_partition=["20230110"], + partitions=[], + ) + + await post_local_hard_hats_availability( + partitions=[ + { + "value": ["DE", None], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230107"], + "valid_through_ts": 20230101, + }, + ], + ) + + response = await module__client_with_roads.get( + "/nodes/default.local_hard_hats/", + ) + availability = response.json()["availability"] + assert availability["min_temporal_partition"] == ["20230101"] + assert availability["max_temporal_partition"] == ["20230110"] + assert availability["partitions"] == [] + + @pytest.mark.asyncio + async def test_set_partition_level_availability_larger_time_range( + self, + module__client_with_roads: AsyncClient, + post_local_hard_hats_availability, + ): + """ + Set a partition-level availability with a larger time range than + the existing node-level time range will result in the partition with + the larger range being recorded + """ + await post_local_hard_hats_availability( + min_temporal_partition=["20230101"], + max_temporal_partition=["20230110"], + partitions=[], + ) + + await post_local_hard_hats_availability( + partitions=[ + { + "value": ["DE", None], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230115"], + "valid_through_ts": 20230101, + }, + ], + ) + + response = await module__client_with_roads.get( + "/nodes/default.local_hard_hats/", + ) + availability = response.json()["availability"] + assert availability["min_temporal_partition"] == ["20230101"] + assert availability["max_temporal_partition"] == ["20230110"] + assert availability["partitions"] == [ + { + "value": ["DE", None], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230115"], + "valid_through_ts": 20230101, + }, + ] + + @pytest.mark.asyncio + async def test_set_orthogonal_partition_level_availability( + self, + module__client_with_roads: AsyncClient, + post_local_hard_hats_availability, + ): + """ + Test setting an orthogonal partition-level availability. + """ + await post_local_hard_hats_availability( + min_temporal_partition=["20230101"], + max_temporal_partition=["20230110"], + partitions=[ + { + "value": ["DE", None], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230115"], + "valid_through_ts": 20230101, + }, + ], + ) + + await post_local_hard_hats_availability( + partitions=[ + { + "value": ["MY", None], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230115"], + "valid_through_ts": 20230101, + }, + ], + ) + + response = await module__client_with_roads.get( + "/nodes/default.local_hard_hats/", + ) + availability = response.json()["availability"] + assert availability["min_temporal_partition"] == ["20230101"] + assert availability["max_temporal_partition"] == ["20230110"] + assert availability["partitions"] == [ + { + "value": ["DE", None], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230115"], + "valid_through_ts": 20230101, + }, + { + "value": ["MY", None], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230115"], + "valid_through_ts": 20230101, + }, + ] + + @pytest.mark.asyncio + async def test_set_overlap_partition_level_availability( + self, + module__client_with_roads: AsyncClient, + post_local_hard_hats_availability, + ): + """ + Test setting an overlapping partition-level availability. + """ + await post_local_hard_hats_availability( + node_name="default.local_hard_hats_1", + min_temporal_partition=["20230101"], + max_temporal_partition=["20230110"], + partitions=[ + { + "value": ["DE", None], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230115"], + "valid_through_ts": 20230101, + }, + ], + ) + + await post_local_hard_hats_availability( + node_name="default.local_hard_hats_1", + partitions=[ + { + "value": ["DE", None], + "min_temporal_partition": ["20230105"], + "max_temporal_partition": ["20230215"], + "valid_through_ts": 20230101, + }, + ], + ) + + response = await module__client_with_roads.get( + "/nodes/default.local_hard_hats_1/", + ) + availability = response.json()["availability"] + assert availability["min_temporal_partition"] == ["20230101"] + assert availability["max_temporal_partition"] == ["20230110"] + assert availability["partitions"] == [ + { + "value": ["DE", None], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230215"], + "valid_through_ts": 20230101, + }, + ] + + @pytest.mark.asyncio + async def test_set_semioverlap_partition_level_availability( + self, + module__client_with_roads: AsyncClient, + post_local_hard_hats_availability, + ): + """ + Test setting a semi-overlapping partition-level availability. + """ + await post_local_hard_hats_availability( + node_name="default.local_hard_hats_2", + min_temporal_partition=["20230101"], + max_temporal_partition=["20230110"], + partitions=[ + { + "value": ["DE", None], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230115"], + "valid_through_ts": 20230101, + }, + { + "value": ["DE", "abc-def"], + "min_temporal_partition": ["20230202"], + "max_temporal_partition": ["20230215"], + "valid_through_ts": 20230101, + }, + ], + ) + + await post_local_hard_hats_availability( + node_name="default.local_hard_hats_2", + partitions=[ + { + "value": ["DE", None], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230115"], + "valid_through_ts": 20230101, + }, + { + "value": ["DE", "abc-def"], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230215"], + "valid_through_ts": 20230101, + }, + ], + ) + + response = await module__client_with_roads.get( + "/nodes/default.local_hard_hats_2/", + ) + availability = response.json()["availability"] + assert availability["min_temporal_partition"] == ["20230101"] + assert availability["max_temporal_partition"] == ["20230110"] + assert availability["partitions"] == [ + { + "value": ["DE", "abc-def"], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230215"], + "valid_through_ts": 20230101, + }, + { + "value": ["DE", None], + "min_temporal_partition": ["20230102"], + "max_temporal_partition": ["20230115"], + "valid_through_ts": 20230101, + }, + ] + + @pytest.mark.asyncio + async def test_merging_in_a_lower_min_partition( + self, + module__session: AsyncSession, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test that the lower min_partition value is used when merging in an availability state + """ + await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_only_1/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "large_pmts", + "valid_through_ts": 20230101, + "max_temporal_partition": ["2023", "01", "01"], + "min_temporal_partition": ["2022", "01", "01"], + }, + ) + response = await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_only_1/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "large_pmts", + "valid_through_ts": 20230101, + "max_temporal_partition": [ + "2021", + "12", + "31", + ], # should be ignored since it's a lower max_temporal_partition + "min_temporal_partition": [ + "2021", + "12", + "31", + ], # should be used since it's a lower min_partition + }, + ) + data = response.json() + + assert response.status_code == 200 + assert data == {"message": "Availability state successfully posted"} + + statement = ( + select(Node) + .where( + Node.name == "default.large_revenue_payments_only_1", + ) + .options( + joinedload(Node.current).options(joinedload(NodeRevision.availability)), + ) + ) + large_revenue_payments_only = ( + (await module__session.execute(statement)).unique().scalar_one() + ) + node_dict = AvailabilityStateBase.from_orm( + large_revenue_payments_only.current.availability, + ).dict() + assert node_dict == { + "valid_through_ts": 20230101, + "catalog": "default", + "min_temporal_partition": ["2021", "12", "31"], + "categorical_partitions": [], + "temporal_partitions": [], + "table": "large_pmts", + "max_temporal_partition": ["2023", "01", "01"], + "schema_": "accounting", + "partitions": [], + "custom_metadata": {}, + "materialization_id": None, + } + + @pytest.mark.asyncio + async def test_moving_back_valid_through_ts( + self, + module__session: AsyncSession, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test that the valid through timestamp can be moved backwards + """ + await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_only_2/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "large_pmts", + "valid_through_ts": 20230101, + "max_temporal_partition": ["2023", "01", "01"], + "min_temporal_partition": ["2022", "01", "01"], + }, + ) + response = await module__client_with_account_revenue.post( + "/data/default.large_revenue_payments_only_2/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "large_pmts", + "valid_through_ts": 20221231, + "max_temporal_partition": [ + "2023", + "01", + "01", + ], # should be ignored since it's a lower max_temporal_partition + "min_temporal_partition": [ + "2022", + "01", + "01", + ], # should be used since it's a lower min_temporal_partition + }, + ) + data = response.json() + + assert response.status_code == 200 + assert data == {"message": "Availability state successfully posted"} + + statement = ( + select(Node) + .where( + Node.name == "default.large_revenue_payments_only_2", + ) + .options( + joinedload(Node.current).options(joinedload(NodeRevision.availability)), + ) + ) + large_revenue_payments_only = ( + (await module__session.execute(statement)).unique().scalar_one() + ) + node_dict = AvailabilityStateBase.from_orm( + large_revenue_payments_only.current.availability, + ).dict() + assert node_dict == { + "valid_through_ts": 20230101, + "catalog": "default", + "min_temporal_partition": ["2022", "01", "01"], + "table": "large_pmts", + "max_temporal_partition": ["2023", "01", "01"], + "schema_": "accounting", + "partitions": [], + "categorical_partitions": [], + "temporal_partitions": [], + "custom_metadata": {}, + "materialization_id": None, + } + + @pytest.mark.asyncio + async def test_setting_availablity_state_on_a_source_node( + self, + module__session: AsyncSession, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test setting the availability state on a source node + """ + response = await module__client_with_account_revenue.post( + "/data/default.revenue/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "revenue", + "valid_through_ts": 20230101, + "max_temporal_partition": ["2023", "01", "01"], + "min_temporal_partition": ["2022", "01", "01"], + }, + ) + data = response.json() + + assert response.status_code == 200 + assert data == {"message": "Availability state successfully posted"} + + statement = select(Node).where( + Node.name == "default.revenue", + ) + revenue = (await module__session.execute(statement)).scalar_one() + await module__session.refresh(revenue, ["current"]) + await module__session.refresh(revenue.current, ["availability"]) + node_dict = AvailabilityStateBase.from_orm(revenue.current.availability).dict() + assert node_dict == { + "valid_through_ts": 20230101, + "catalog": "default", + "min_temporal_partition": ["2022", "01", "01"], + "table": "revenue", + "max_temporal_partition": ["2023", "01", "01"], + "schema_": "accounting", + "partitions": [], + "categorical_partitions": [], + "temporal_partitions": [], + "custom_metadata": {}, + "materialization_id": None, + } + + @pytest.mark.asyncio + async def test_raise_on_setting_invalid_availability_state_on_a_source_node( + self, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test raising availability state doesn't match existing source node table + """ + response = await module__client_with_account_revenue.post( + "/data/default.revenue/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "large_pmts", + "valid_through_ts": 20230101, + "max_temporal_partition": ["2023", "01", "01"], + "min_temporal_partition": ["2022", "01", "01"], + }, + ) + data = response.json() + + assert response.status_code == 422 + assert data == { + "message": ( + "Cannot set availability state, source nodes require availability states " + "to match the set table: default.accounting.large_pmts does not match " + "default.accounting.revenue " + ), + "errors": [], + "warnings": [], + } + + @pytest.mark.asyncio + async def test_reading_and_saving_custom_metadata( + self, + module__client_with_account_revenue: AsyncClient, + ) -> None: + """ + Tetst reading and saving custom metadata. + """ + # read current value + node1 = ( + await module__client_with_account_revenue.get( + "/nodes/default.large_revenue_payments_only_custom", + ) + ).json() + assert node1["custom_metadata"] == {"foo": "bar"} + assert node1["version"] == "v1.0" + + # save new value + response = await module__client_with_account_revenue.patch( + "/nodes/default.large_revenue_payments_only_custom", + json={ + "custom_metadata": {"bar": "baz"}, + }, + ) + assert response.status_code == 200 + + # read again + node2 = ( + await module__client_with_account_revenue.get( + "/nodes/default.large_revenue_payments_only_custom", + ) + ).json() + assert node2["custom_metadata"] == {"bar": "baz"} + assert node2["version"] == "v1.1" diff --git a/datajunction-server/tests/api/dimension_links_test.py b/datajunction-server/tests/api/dimension_links_test.py new file mode 100644 index 000000000..4829e3769 --- /dev/null +++ b/datajunction-server/tests/api/dimension_links_test.py @@ -0,0 +1,1035 @@ +""" +Dimension linking related tests. + +TODO: convert to module scope later, for now these tests are pretty fast, only ~20 sec. +""" + +import pytest +import pytest_asyncio +from httpx import AsyncClient +from requests import Response + +from datajunction_server.sql.parsing.backends.antlr4 import parse +from tests.conftest import post_and_raise_if_error +from tests.examples import COMPLEX_DIMENSION_LINK, SERVICE_SETUP + + +@pytest_asyncio.fixture +async def dimensions_link_client(client: AsyncClient) -> AsyncClient: + """ + Add dimension link examples to the roads test client. + """ + for endpoint, json in SERVICE_SETUP + COMPLEX_DIMENSION_LINK: + await post_and_raise_if_error( # type: ignore + client=client, + endpoint=endpoint, + json=json, # type: ignore + ) + return client + + +@pytest.mark.asyncio +async def test_link_dimension_with_errors( + dimensions_link_client: AsyncClient, +): + """ + Test linking dimensions with errors + """ + response = await dimensions_link_client.post( + "/nodes/default.elapsed_secs/link", + json={ + "dimension_node": "default.users", + "join_on": ("default.elapsed_secs.x = default.users.y"), + "join_cardinality": "many_to_one", + }, + ) + assert response.json()["message"] == ( + "Cannot link dimension to a node of type metric. Must be a source, " + "dimension, or transform node." + ) + response = await dimensions_link_client.post( + "/nodes/default.events/link", + json={ + "dimension_node": "default.users", + "join_on": ("default.users.user_id = default.users.user_id"), + "join_cardinality": "many_to_one", + }, + ) + assert response.json()["message"] == ( + "The join SQL provided does not reference both the origin node default.events " + "and the dimension node default.users that it's being joined to." + ) + + response = await dimensions_link_client.post( + "/nodes/default.events/link", + json={ + "dimension_node": "default.users", + "join_on": ("default.events.order_year = default.users.year"), + "join_cardinality": "many_to_one", + }, + ) + assert ( + response.json()["message"] + == "Join query default.events.order_year = default.users.year is not valid" + ) + + +@pytest.fixture +def link_events_to_users_without_role( + dimensions_link_client: AsyncClient, +): + """ + Link events with the users dimension without a role + """ + + async def _link_events_to_users_without_role() -> Response: + response = await dimensions_link_client.post( + "/nodes/default.events/link", + json={ + "dimension_node": "default.users", + "join_type": "left", + "join_on": ( + "default.events.user_id = default.users.user_id " + "AND default.events.event_start_date = default.users.snapshot_date" + ), + "join_cardinality": "one_to_one", + }, + ) + return response + + return _link_events_to_users_without_role + + +@pytest.fixture +def link_events_to_users_with_role_direct( + dimensions_link_client: AsyncClient, +): + """ + Link events with the users dimension with the role "user_direct", + indicating a direct mapping between the user's snapshot date with the + event's start date + """ + + async def _link_events_to_users_with_role_direct() -> Response: + response = await dimensions_link_client.post( + "/nodes/default.events/link", + json={ + "dimension_node": "default.users", + "join_type": "left", + "join_on": ( + "default.events.user_id = default.users.user_id " + "AND default.events.event_start_date = default.users.snapshot_date" + ), + "join_cardinality": "one_to_one", + "role": "user_direct", + }, + ) + return response + + return _link_events_to_users_with_role_direct + + +@pytest.fixture +def link_events_to_users_with_role_windowed( + dimensions_link_client: AsyncClient, +): + """ + Link events with the users dimension with the role "user_windowed", + indicating windowed join between events and the user dimension + """ + + async def _link_events_to_users_with_role_windowed() -> Response: + response = await dimensions_link_client.post( + "/nodes/default.events/link", + json={ + "dimension_node": "default.users", + "join_type": "left", + "join_on": "default.events.user_id = default.users.user_id " + "AND default.events.event_start_date BETWEEN default.users.snapshot_date " + "AND CAST(DATE_ADD(CAST(default.users.snapshot_date AS DATE), 10) AS INT)", + "join_cardinality": "one_to_many", + "role": "user_windowed", + }, + ) + return response + + return _link_events_to_users_with_role_windowed + + +@pytest.fixture +def link_users_to_countries_with_role_registration( + dimensions_link_client: AsyncClient, +): + """ + Link users to the countries dimension with role "registration_country". + """ + + async def _link_users_to_countries_with_role_registration() -> Response: + response = await dimensions_link_client.post( + "/nodes/default.users/link", + json={ + "dimension_node": "default.countries", + "join_type": "inner", + "join_on": "default.users.registration_country = default.countries.country_code ", + "join_cardinality": "one_to_one", + "role": "registration_country", + }, + ) + return response + + return _link_users_to_countries_with_role_registration + + +@pytest.fixture +def reference_link_events_user_registration_country( + dimensions_link_client: AsyncClient, +): + """ + Link users to the countries dimension with role "registration_country". + """ + + async def _reference_link_events_user_registration_country() -> Response: + response = await dimensions_link_client.post( + "/nodes/default.events/columns/user_registration_country/link", + params={ + "dimension_node": "default.users", + "dimension_column": "registration_country", + }, + ) + assert response.status_code == 201 + return response + + return _reference_link_events_user_registration_country + + +@pytest.mark.asyncio +async def test_link_complex_dimension_without_role( + dimensions_link_client: AsyncClient, + link_events_to_users_without_role, +): + """ + Test linking complex dimension without role + """ + response = await link_events_to_users_without_role() + assert response.json() == { + "message": "Dimension node default.users has been successfully " + "linked to node default.events.", + } + + response = await dimensions_link_client.get("/nodes/default.events") + assert response.json()["dimension_links"] == [ + { + "dimension": {"name": "default.users"}, + "join_cardinality": "one_to_one", + "join_sql": "default.events.user_id = default.users.user_id " + "AND default.events.event_start_date = default.users.snapshot_date", + "join_type": "left", + "foreign_keys": { + "default.events.event_start_date": "default.users.snapshot_date", + "default.events.user_id": "default.users.user_id", + }, + "role": None, + }, + ] + + # Update dimension link + response = await dimensions_link_client.post( + "/nodes/default.events/link", + json={ + "dimension_node": "default.users", + "join_type": "left", + "join_on": ( + "default.events.user_id = default.users.user_id " + "AND default.events.event_end_date = default.users.snapshot_date" + ), + "join_cardinality": "one_to_many", + }, + ) + assert response.json() == { + "message": "The dimension link between default.events and " + "default.users has been successfully updated.", + } + + response = await dimensions_link_client.get("/nodes/default.events") + assert response.json()["dimension_links"][0]["foreign_keys"] == { + "default.events.event_end_date": "default.users.snapshot_date", + "default.events.user_id": "default.users.user_id", + } + + response = await dimensions_link_client.get("/history?node=default.events") + assert [ + (entry["activity_type"], entry["details"]) + for entry in response.json() + if entry["entity_type"] == "link" + ] == [ + ( + "update", + { + "dimension": "default.users", + "join_cardinality": "one_to_many", + "join_sql": "default.events.user_id = default.users.user_id AND " + "default.events.event_end_date = default.users.snapshot_date", + "role": None, + }, + ), + ( + "create", + { + "dimension": "default.users", + "join_cardinality": "one_to_one", + "join_sql": "default.events.user_id = default.users.user_id AND " + "default.events.event_start_date = default.users.snapshot_date", + "role": None, + }, + ), + ] + + # Switch back to original join definition + await link_events_to_users_without_role() + + response = await dimensions_link_client.get( + "/sql/default.events?dimensions=default.users.user_id" + "&dimensions=default.users.snapshot_date" + "&dimensions=default.users.registration_country", + ) + query = response.json()["sql"] + expected = """WITH default_DOT_events AS ( + SELECT + default_DOT_events_table.user_id, + default_DOT_events_table.event_start_date, + default_DOT_events_table.event_end_date, + default_DOT_events_table.elapsed_secs, + default_DOT_events_table.user_registration_country + FROM examples.events AS default_DOT_events_table + ), + default_DOT_users AS ( + SELECT + default_DOT_users_table.user_id, + default_DOT_users_table.snapshot_date, + default_DOT_users_table.registration_country, + default_DOT_users_table.residence_country, + default_DOT_users_table.account_type + FROM examples.users AS default_DOT_users_table + ) + SELECT + default_DOT_events.user_id default_DOT_users_DOT_user_id, + default_DOT_events.event_start_date default_DOT_users_DOT_snapshot_date, + default_DOT_events.event_end_date default_DOT_events_DOT_event_end_date, + default_DOT_events.elapsed_secs default_DOT_events_DOT_elapsed_secs, + default_DOT_events.user_registration_country default_DOT_events_DOT_user_registration_country, + default_DOT_users.registration_country default_DOT_users_DOT_registration_country + FROM default_DOT_events + LEFT JOIN default_DOT_users + ON default_DOT_events.user_id = default_DOT_users.user_id + AND default_DOT_events.event_start_date = default_DOT_users.snapshot_date + """ + assert str(parse(query)) == str(parse(expected)) + + response = await dimensions_link_client.get("/nodes/default.events/dimensions") + assert [(attr["name"], attr["path"]) for attr in response.json()] == [ + ("default.users.account_type", ["default.events"]), + ("default.users.registration_country", ["default.events"]), + ("default.users.residence_country", ["default.events"]), + ("default.users.snapshot_date", ["default.events"]), + ("default.users.user_id", ["default.events"]), + ] + + +@pytest.mark.asyncio +async def test_link_complex_dimension_with_role( + dimensions_link_client: AsyncClient, + link_events_to_users_with_role_direct, + link_events_to_users_with_role_windowed, + link_users_to_countries_with_role_registration, +): + """ + Testing linking complex dimension with roles. + """ + response = await link_events_to_users_with_role_direct() + assert response.json() == { + "message": "Dimension node default.users has been successfully " + "linked to node default.events.", + } + + response = await dimensions_link_client.get("/nodes/default.events") + assert response.json()["dimension_links"] == [ + { + "dimension": {"name": "default.users"}, + "join_cardinality": "one_to_one", + "join_sql": "default.events.user_id = default.users.user_id " + "AND default.events.event_start_date = default.users.snapshot_date", + "join_type": "left", + "role": "user_direct", + "foreign_keys": { + "default.events.event_start_date": "default.users.snapshot_date", + "default.events.user_id": "default.users.user_id", + }, + }, + ] + + # Add a dimension link with different role + response = await link_events_to_users_with_role_windowed() + assert response.json() == { + "message": "Dimension node default.users has been successfully linked to node " + "default.events.", + } + + # Add a dimension link on users for registration country + response = await link_users_to_countries_with_role_registration() + assert response.json() == { + "message": "Dimension node default.countries has been successfully linked to node " + "default.users.", + } + + response = await dimensions_link_client.get("/nodes/default.events") + assert sorted(response.json()["dimension_links"], key=lambda x: x["role"]) == [ + { + "dimension": {"name": "default.users"}, + "join_cardinality": "one_to_one", + "join_sql": "default.events.user_id = default.users.user_id AND " + "default.events.event_start_date = default.users.snapshot_date", + "join_type": "left", + "role": "user_direct", + "foreign_keys": { + "default.events.event_start_date": "default.users.snapshot_date", + "default.events.user_id": "default.users.user_id", + }, + }, + { + "dimension": {"name": "default.users"}, + "join_cardinality": "one_to_many", + "join_sql": "default.events.user_id = default.users.user_id AND " + "default.events.event_start_date BETWEEN " + "default.users.snapshot_date AND " + "CAST(DATE_ADD(CAST(default.users.snapshot_date AS DATE), 10) AS " + "INT)", + "join_type": "left", + "role": "user_windowed", + "foreign_keys": { + "default.events.event_start_date": None, + "default.events.user_id": "default.users.user_id", + }, + }, + ] + + # Verify that the dimensions on the downstream metric have roles specified + response = await dimensions_link_client.get( + "/nodes/default.elapsed_secs/dimensions", + ) + assert [(attr["name"], attr["path"]) for attr in response.json()] == [ + ( + "default.countries.country_code[user_direct->registration_country]", + ["default.events.user_direct", "default.users.registration_country"], + ), + ( + "default.countries.country_code[user_windowed->registration_country]", + ["default.events.user_windowed", "default.users.registration_country"], + ), + ( + "default.countries.name[user_direct->registration_country]", + ["default.events.user_direct", "default.users.registration_country"], + ), + ( + "default.countries.name[user_windowed->registration_country]", + ["default.events.user_windowed", "default.users.registration_country"], + ), + ( + "default.countries.population[user_direct->registration_country]", + ["default.events.user_direct", "default.users.registration_country"], + ), + ( + "default.countries.population[user_windowed->registration_country]", + ["default.events.user_windowed", "default.users.registration_country"], + ), + ("default.users.account_type[user_direct]", ["default.events.user_direct"]), + ("default.users.account_type[user_windowed]", ["default.events.user_windowed"]), + ( + "default.users.registration_country[user_direct]", + ["default.events.user_direct"], + ), + ( + "default.users.registration_country[user_windowed]", + ["default.events.user_windowed"], + ), + ( + "default.users.residence_country[user_direct]", + ["default.events.user_direct"], + ), + ( + "default.users.residence_country[user_windowed]", + ["default.events.user_windowed"], + ), + ("default.users.snapshot_date[user_direct]", ["default.events.user_direct"]), + ( + "default.users.snapshot_date[user_windowed]", + ["default.events.user_windowed"], + ), + ("default.users.user_id[user_direct]", ["default.events.user_direct"]), + ("default.users.user_id[user_windowed]", ["default.events.user_windowed"]), + ] + + # Get SQL for the downstream metric grouped by the user dimension of role "user_windowed" + response = await dimensions_link_client.get( + "/sql/default.elapsed_secs", + params={ + "dimensions": [ + "default.users.user_id[user_windowed]", + "default.users.snapshot_date[user_windowed]", + "default.users.registration_country[user_windowed]", + ], + "filters": ["default.users.registration_country[user_windowed] = 'NZ'"], + }, + ) + query = response.json()["sql"] + expected = """WITH default_DOT_events AS ( + SELECT + default_DOT_events_table.user_id, + default_DOT_events_table.event_start_date, + default_DOT_events_table.event_end_date, + default_DOT_events_table.elapsed_secs, + default_DOT_events_table.user_registration_country + FROM examples.events AS default_DOT_events_table +), default_DOT_users AS ( + SELECT + default_DOT_users_table.user_id, + default_DOT_users_table.snapshot_date, + default_DOT_users_table.registration_country, + default_DOT_users_table.residence_country, + default_DOT_users_table.account_type + FROM examples.users AS default_DOT_users_table +), +default_DOT_events_metrics AS ( + SELECT + default_DOT_users.user_id default_DOT_users_DOT_user_id_LBRACK_user_windowed_RBRACK, + default_DOT_users.snapshot_date default_DOT_users_DOT_snapshot_date_LBRACK_user_windowed_RBRACK, + default_DOT_users.registration_country default_DOT_users_DOT_registration_country_LBRACK_user_windowed_RBRACK, + SUM(default_DOT_events.elapsed_secs) default_DOT_elapsed_secs + FROM default_DOT_events + LEFT JOIN default_DOT_users + ON default_DOT_events.user_id = default_DOT_users.user_id + AND default_DOT_events.event_start_date = default_DOT_users.snapshot_date + GROUP BY + default_DOT_users.user_id, + default_DOT_users.snapshot_date, + default_DOT_users.registration_country +) +SELECT + default_DOT_events_metrics.default_DOT_users_DOT_user_id_LBRACK_user_windowed_RBRACK, + default_DOT_events_metrics.default_DOT_users_DOT_snapshot_date_LBRACK_user_windowed_RBRACK, + default_DOT_events_metrics.default_DOT_users_DOT_registration_country_LBRACK_user_windowed_RBRACK, + default_DOT_events_metrics.default_DOT_elapsed_secs +FROM default_DOT_events_metrics +""" + assert str(parse(query)) == str(parse(expected)) + + # Get SQL for the downstream metric grouped by the user dimension of role "user" + response = await dimensions_link_client.get( + "/sql/default.elapsed_secs?dimensions=default.users.user_id[user_direct]" + "&dimensions=default.users.snapshot_date[user_direct]" + "&dimensions=default.users.registration_country[user_direct]", + ) + query = response.json()["sql"] + expected = """WITH default_DOT_events AS ( + SELECT + default_DOT_events_table.user_id, + default_DOT_events_table.event_start_date, + default_DOT_events_table.event_end_date, + default_DOT_events_table.elapsed_secs, + default_DOT_events_table.user_registration_country + FROM examples.events AS default_DOT_events_table + ), default_DOT_users AS ( + SELECT + default_DOT_users_table.user_id, + default_DOT_users_table.snapshot_date, + default_DOT_users_table.registration_country, + default_DOT_users_table.residence_country, + default_DOT_users_table.account_type + FROM examples.users AS default_DOT_users_table + ), + default_DOT_events_metrics AS ( + SELECT + default_DOT_users.user_id default_DOT_users_DOT_user_id_LBRACK_user_direct_RBRACK, + default_DOT_users.snapshot_date default_DOT_users_DOT_snapshot_date_LBRACK_user_direct_RBRACK, + default_DOT_users.registration_country default_DOT_users_DOT_registration_country_LBRACK_user_direct_RBRACK, + SUM(default_DOT_events.elapsed_secs) default_DOT_elapsed_secs + FROM default_DOT_events + LEFT JOIN default_DOT_users + ON default_DOT_events.user_id = default_DOT_users.user_id + AND default_DOT_events.event_start_date = default_DOT_users.snapshot_date + GROUP BY + default_DOT_users.user_id, + default_DOT_users.snapshot_date, + default_DOT_users.registration_country + ) + SELECT + default_DOT_events_metrics.default_DOT_users_DOT_user_id_LBRACK_user_direct_RBRACK, + default_DOT_events_metrics.default_DOT_users_DOT_snapshot_date_LBRACK_user_direct_RBRACK, + default_DOT_events_metrics.default_DOT_users_DOT_registration_country_LBRACK_user_direct_RBRACK, + default_DOT_events_metrics.default_DOT_elapsed_secs + FROM default_DOT_events_metrics""" + assert str(parse(query)) == str(parse(expected)) + + # Get SQL for the downstream metric grouped by the user's registration country and + # filtered by the user's residence country + response = await dimensions_link_client.get( + "/sql/default.elapsed_secs?", + params={ + "dimensions": [ + "default.countries.name[user_direct->registration_country]", + "default.users.snapshot_date[user_direct]", + "default.users.registration_country[user_direct]", + ], + "filters": [ + "default.countries.name[user_direct->registration_country] = 'NZ'", + ], + }, + ) + query = response.json()["sql"] + expected = """WITH default_DOT_events AS ( + SELECT + default_DOT_events_table.user_id, + default_DOT_events_table.event_start_date, + default_DOT_events_table.event_end_date, + default_DOT_events_table.elapsed_secs, + default_DOT_events_table.user_registration_country + FROM examples.events AS default_DOT_events_table +), default_DOT_users AS ( + SELECT + default_DOT_users_table.user_id, + default_DOT_users_table.snapshot_date, + default_DOT_users_table.registration_country, + default_DOT_users_table.residence_country, + default_DOT_users_table.account_type + FROM examples.users AS default_DOT_users_table +), default_DOT_countries AS ( + SELECT + default_DOT_countries_table.country_code, + default_DOT_countries_table.name, + default_DOT_countries_table.population + FROM examples.countries AS default_DOT_countries_table +), +default_DOT_events_metrics AS ( + SELECT + default_DOT_users.snapshot_date default_DOT_users_DOT_snapshot_date_LBRACK_user_direct_RBRACK, + default_DOT_users.registration_country default_DOT_users_DOT_registration_country_LBRACK_user_direct_RBRACK, + SUM(default_DOT_events.elapsed_secs) default_DOT_elapsed_secs + FROM default_DOT_events + LEFT JOIN default_DOT_users + ON default_DOT_events.user_id = default_DOT_users.user_id + AND default_DOT_events.event_start_date = default_DOT_users.snapshot_date + INNER JOIN default_DOT_countries + ON default_DOT_users.registration_country = default_DOT_countries.country_code + GROUP BY + default_DOT_users.snapshot_date, + default_DOT_users.registration_country +) +SELECT + default_DOT_events_metrics.default_DOT_users_DOT_snapshot_date_LBRACK_user_direct_RBRACK, + default_DOT_events_metrics.default_DOT_users_DOT_registration_country_LBRACK_user_direct_RBRACK, + default_DOT_events_metrics.default_DOT_elapsed_secs +FROM default_DOT_events_metrics +""" + assert str(parse(query)) == str(parse(expected)) + + +@pytest.mark.asyncio +async def test_remove_dimension_link( + dimensions_link_client: AsyncClient, + link_events_to_users_with_role_direct, + link_events_to_users_without_role, +): + """ + Test removing complex dimension links + """ + response = await link_events_to_users_with_role_direct() + assert response.json() == { + "message": "Dimension node default.users has been successfully linked to node " + "default.events.", + } + response = await dimensions_link_client.get("/nodes/default.events") + # assert response.json()["dimension_links"] == [] + response = await dimensions_link_client.request( + "DELETE", + "/nodes/default.events/link", + json={ + "dimension_node": "default.users", + "role": "user_direct", + }, + ) + assert response.json() == { + "message": "Dimension link default.users (role user_direct) to node " + "default.events has been removed.", + } + + response = await dimensions_link_client.get("/nodes/default.events") + assert response.json()["dimension_links"] == [] + # Deleting again should not work + response = await dimensions_link_client.request( + "DELETE", + "/nodes/default.events/link", + json={ + "dimension_node": "default.users", + "role": "user_direct", + }, + ) + assert response.json() == { + "message": "Dimension link to node default.users with role user_direct not found", + } + + await link_events_to_users_without_role() + response = await dimensions_link_client.request( + "DELETE", + "/nodes/default.events/link", + json={ + "dimension_node": "default.users", + }, + ) + assert response.json() == { + "message": "Dimension link default.users to node " + "default.events has been removed.", + } + + # Deleting again should not work + response = await dimensions_link_client.request( + "DELETE", + "/nodes/default.events/link", + json={ + "dimension_node": "default.users", + }, + ) + assert response.json() == { + "message": "Dimension link to node default.users not found", + } + + +@pytest.mark.asyncio +async def test_measures_sql_with_dimension_roles( + dimensions_link_client: AsyncClient, + link_events_to_users_with_role_direct, + link_events_to_users_with_role_windowed, + link_users_to_countries_with_role_registration, +): + """ + Test measures SQL with dimension roles + """ + await link_events_to_users_with_role_direct() + await link_events_to_users_with_role_windowed() + await link_users_to_countries_with_role_registration() + sql_params = { + "metrics": ["default.elapsed_secs"], + "dimensions": [ + "default.countries.name[user_direct->registration_country]", + "default.users.snapshot_date[user_direct]", + "default.users.registration_country[user_direct]", + ], + "filters": ["default.countries.name[user_direct->registration_country] = 'UG'"], + } + response = await dimensions_link_client.get("/sql/measures/v2", params=sql_params) + query = response.json()[0]["sql"] + expected = """WITH default_DOT_events AS ( + SELECT + default_DOT_events_table.user_id, + default_DOT_events_table.event_start_date, + default_DOT_events_table.event_end_date, + default_DOT_events_table.elapsed_secs, + default_DOT_events_table.user_registration_country + FROM examples.events AS default_DOT_events_table +), default_DOT_users AS ( + SELECT + default_DOT_users_table.user_id, + default_DOT_users_table.snapshot_date, + default_DOT_users_table.registration_country, + default_DOT_users_table.residence_country, + default_DOT_users_table.account_type + FROM examples.users AS default_DOT_users_table +), default_DOT_countries AS ( + SELECT + default_DOT_countries_table.country_code, + default_DOT_countries_table.name, + default_DOT_countries_table.population + FROM examples.countries AS default_DOT_countries_table +) +SELECT + default_DOT_events.elapsed_secs default_DOT_events_DOT_elapsed_secs, + default_DOT_countries.name default_DOT_countries_DOT_name +FROM default_DOT_events +LEFT JOIN default_DOT_users + ON default_DOT_events.user_id = default_DOT_users.user_id + AND default_DOT_events.event_start_date = default_DOT_users.snapshot_date +INNER JOIN default_DOT_countries + ON default_DOT_users.registration_country = default_DOT_countries.country_code""" + assert str(parse(query)) == str(parse(expected)) + # TODO Implement caching for v2 measures SQL endpoint + # query_request = ( + # ( + # await session.execute( + # select(QueryRequest).where( + # QueryRequest.query_type == QueryBuildType.MEASURES, + # ), + # ) + # ) + # .scalars() + # .all() + # ) + # assert len(query_request) == 1 + # assert query_request[0].nodes == ["default.elapsed_secs@v1.0"] + # assert query_request[0].parents == [ + # "default.events@v1.0", + # "default.events_table@v1.0", + # ] + # assert query_request[0].dimensions == [ + # "default.countries.name[user_direct->registration_country]@v1.0", + # "default.users.snapshot_date[user_direct]@v1.0", + # "default.users.registration_country[user_direct]@v1.0", + # ] + # assert query_request[0].filters == [ + # "default.countries.name[user_direct -> registration_country]@v1.0 = 'UG'", + # ] + + +@pytest.mark.asyncio +async def test_reference_dimension_links_errors( + dimensions_link_client: AsyncClient, + reference_link_events_user_registration_country, +): + """ + Test various reference dimension link errors + """ + # Not a dimension node being linked + dimensions_link_client.post("/nodes/{}") + response = await dimensions_link_client.post( + "/nodes/default.events/columns/user_registration_country/link", + params={ + "dimension_node": "default.users_table", + "dimension_column": "user_id", + }, + ) + assert response.status_code == 422 + assert response.json()["message"] == "Node default.events is not of type dimension!" + + # Wrong type to create a reference dimension link + response = await dimensions_link_client.post( + "/nodes/default.events/columns/user_registration_country/link", + params={ + "dimension_node": "default.users", + "dimension_column": "snapshot_date", + }, + ) + assert response.status_code == 422 + assert response.json()["message"] == ( + "The column user_registration_country has type string and is being linked to" + " the dimension default.users via the dimension column snapshot_date, which " + "has type int. These column types are incompatible and the dimension cannot " + "be linked" + ) + + # Delete reference link twice + await reference_link_events_user_registration_country() + response = await dimensions_link_client.delete( + "/nodes/default.events/columns/user_registration_country/link", + ) + assert response.status_code == 200 + assert response.json()["message"] == ( + "The reference dimension link on default.events.user_registration_country" + " has been removed." + ) + response = await dimensions_link_client.delete( + "/nodes/default.events/columns/user_registration_country/link", + ) + assert response.status_code == 200 + assert response.json()["message"] == ( + "There is no reference dimension link on default.events.user_registration_country." + ) + + +@pytest.mark.asyncio +async def test_measures_sql_with_reference_dimension_links( + dimensions_link_client: AsyncClient, + reference_link_events_user_registration_country, + link_events_to_users_without_role, +): + """ + Test measures SQL generation with reference dimension links + """ + await reference_link_events_user_registration_country() + + response = await dimensions_link_client.get( + "/nodes/default.elapsed_secs/dimensions", + ) + dimensions_data = response.json() + assert dimensions_data == [ + { + "filter_only": False, + "name": "default.users.registration_country", + "node_display_name": "Users", + "node_name": "default.users", + "path": [ + "default.events.user_registration_country", + ], + "type": "string", + "properties": [], + }, + ] + + await link_events_to_users_without_role() + response = await dimensions_link_client.get( + "/nodes/default.elapsed_secs/dimensions", + ) + dimensions_data = response.json() + assert [dim["name"] for dim in dimensions_data] == [ + "default.users.account_type", + "default.users.registration_country", + "default.users.registration_country", + "default.users.residence_country", + "default.users.snapshot_date", + "default.users.user_id", + ] + + sql_params = { + "metrics": ["default.elapsed_secs"], + "dimensions": [ + "default.users.registration_country", + ], + } + + response = await dimensions_link_client.get("/sql/measures/v2", params=sql_params) + response_data = response.json() + expected_sql = """WITH +default_DOT_events AS ( + SELECT + default_DOT_events_table.user_id, + default_DOT_events_table.event_start_date, + default_DOT_events_table.event_end_date, + default_DOT_events_table.elapsed_secs, + default_DOT_events_table.user_registration_country + FROM examples.events AS default_DOT_events_table +) +SELECT + default_DOT_events.elapsed_secs default_DOT_events_DOT_elapsed_secs, + default_DOT_events.user_registration_country default_DOT_users_DOT_registration_country +FROM default_DOT_events""" + assert str(parse(response_data[0]["sql"])) == str(parse(expected_sql)) + assert response_data[0]["errors"] == [] + assert response_data[0]["columns"] == [ + { + "name": "default_DOT_events_DOT_elapsed_secs", + "type": "int", + "column": "elapsed_secs", + "node": "default.events", + "semantic_entity": "default.events.elapsed_secs", + "semantic_type": "measure", + }, + { + "name": "default_DOT_users_DOT_registration_country", + "type": "string", + "column": "registration_country", + "node": "default.users", + "semantic_entity": "default.users.registration_country", + "semantic_type": "dimension", + }, + ] + response = await dimensions_link_client.delete( + "/nodes/default.events/columns/user_registration_country/link", + ) + assert response.status_code == 200 + response = await dimensions_link_client.get("/sql/measures/v2", params=sql_params) + response_data = response.json() + expected_sql = """ + WITH default_DOT_events AS ( + SELECT + default_DOT_events_table.user_id, + default_DOT_events_table.event_start_date, + default_DOT_events_table.event_end_date, + default_DOT_events_table.elapsed_secs, + default_DOT_events_table.user_registration_country + FROM examples.events AS default_DOT_events_table + ), + default_DOT_users AS ( + SELECT + default_DOT_users_table.user_id, + default_DOT_users_table.snapshot_date, + default_DOT_users_table.registration_country, + default_DOT_users_table.residence_country, + default_DOT_users_table.account_type + FROM examples.users AS default_DOT_users_table + ) + SELECT + default_DOT_events.elapsed_secs default_DOT_events_DOT_elapsed_secs, + default_DOT_users.registration_country default_DOT_users_DOT_registration_country + FROM default_DOT_events + LEFT JOIN default_DOT_users + ON default_DOT_events.user_id = default_DOT_users.user_id + AND default_DOT_events.event_start_date = default_DOT_users.snapshot_date + """ + assert str(parse(response_data[0]["sql"])) == str(parse(expected_sql)) + assert response_data[0]["errors"] == [] + + +@pytest.mark.asyncio +async def test_dimension_link_cross_join( + dimensions_link_client: AsyncClient, +): + """ + Testing linking complex dimension with CROSS JOIN as the join type. + """ + response = await dimensions_link_client.post( + "/nodes/dimension", + json={ + "description": "Areas", + "query": """ + SELECT tab.area, 1 AS area_rep FROM VALUES ('A'), ('B'), ('C') AS tab(area) + """, + "mode": "published", + "name": "default.areas", + "primary_key": ["area"], + }, + ) + assert response.status_code == 201 + response = await dimensions_link_client.post( + "/nodes/default.events/link", + json={ + "dimension_node": "default.areas", + "join_type": "cross", + "join_on": "", + "join_cardinality": "many_to_one", + }, + ) + assert response.status_code == 201 + response = await dimensions_link_client.get("/nodes/default.events/dimensions") + assert [dim["name"] for dim in response.json()] == [ + "default.areas.area", + "default.areas.area_rep", + ] + + response = await dimensions_link_client.get( + "/sql/default.events?dimensions=default.areas.area&dimensions=default.areas.area_rep", + ) + expected = """WITH + default_DOT_events AS ( + SELECT + default_DOT_events_table.user_id, + default_DOT_events_table.event_start_date, + default_DOT_events_table.event_end_date, + default_DOT_events_table.elapsed_secs, + default_DOT_events_table.user_registration_country + FROM examples.events AS default_DOT_events_table + ), + default_DOT_areas AS ( + SELECT + tab.area, + 1 AS area_rep + FROM VALUES ('A'), + ('B'), + ('C') AS tab(area) + ) + SELECT + default_DOT_events.user_id default_DOT_events_DOT_user_id, + default_DOT_events.event_start_date default_DOT_events_DOT_event_start_date, + default_DOT_events.event_end_date default_DOT_events_DOT_event_end_date, + default_DOT_events.elapsed_secs default_DOT_events_DOT_elapsed_secs, + default_DOT_events.user_registration_country default_DOT_events_DOT_user_registration_country, + default_DOT_areas.area default_DOT_areas_DOT_area, + default_DOT_areas.area_rep default_DOT_areas_DOT_area_rep + FROM default_DOT_events CROSS JOIN default_DOT_areas + """ + assert str(parse(response.json()["sql"])) == str(parse(expected)) diff --git a/datajunction-server/tests/api/dimensions_access_test.py b/datajunction-server/tests/api/dimensions_access_test.py new file mode 100644 index 000000000..e95fcebdf --- /dev/null +++ b/datajunction-server/tests/api/dimensions_access_test.py @@ -0,0 +1,50 @@ +""" +Tests for the dimensions API. +""" + +import pytest +from httpx import AsyncClient + +from datajunction_server.api.main import app + + +@pytest.mark.asyncio +async def test_list_nodes_with_dimension_access_limited( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test ``GET /dimensions/{name}/nodes/``. + """ + from datajunction_server.internal.access.authorization import validate_access + from datajunction_server.models import access + + def validate_access_override(): + def _validate_access(access_control: access.AccessControl): + for request in access_control.requests: + if "repair" in request.access_object.name: + request.approve() + else: + request.deny() + + return _validate_access + + app.dependency_overrides[validate_access] = validate_access_override + + response = await module__client_with_roads.get( + "/dimensions/default.hard_hat/nodes/", + ) + + data = response.json() + roads_repair_nodes = { + "default.repair_orders", + "default.repair_order_details", + "default.regional_repair_efficiency", + "default.num_repair_orders", + "default.avg_repair_price", + "default.repair_orders_fact", + "default.total_repair_cost", + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + } + assert {node["name"] for node in data} == roads_repair_nodes + app.dependency_overrides.clear() diff --git a/datajunction-server/tests/api/dimensions_test.py b/datajunction-server/tests/api/dimensions_test.py new file mode 100644 index 000000000..5c76d9fb1 --- /dev/null +++ b/datajunction-server/tests/api/dimensions_test.py @@ -0,0 +1,184 @@ +""" +Tests for the dimensions API. +""" + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_list_dimension( + module__client_with_roads_and_acc_revenue: AsyncClient, +) -> None: + """ + Test ``GET /dimensions/``. + """ + response = await module__client_with_roads_and_acc_revenue.get("/dimensions/") + data = response.json() + + assert response.status_code == 200 + assert {(dim["name"], dim["indegree"]) for dim in data} == { + (dim["name"], dim["indegree"]) + for dim in [ + {"indegree": 3, "name": "default.dispatcher"}, + {"indegree": 2, "name": "default.repair_order"}, + {"indegree": 2, "name": "default.hard_hat"}, + {"indegree": 2, "name": "default.hard_hat_to_delete"}, + {"indegree": 2, "name": "default.municipality_dim"}, + {"indegree": 1, "name": "default.contractor"}, + {"indegree": 2, "name": "default.us_state"}, + {"indegree": 0, "name": "default.local_hard_hats"}, + {"indegree": 0, "name": "default.local_hard_hats_1"}, + {"indegree": 0, "name": "default.local_hard_hats_2"}, + {"indegree": 0, "name": "default.payment_type"}, + {"indegree": 0, "name": "default.account_type"}, + {"indegree": 0, "name": "default.hard_hat_2"}, + ] + } + + +@pytest.mark.asyncio +async def test_list_nodes_with_dimension( + module__client_with_roads_and_acc_revenue: AsyncClient, +) -> None: + """ + Test ``GET /dimensions/{name}/nodes/``. + """ + response = await module__client_with_roads_and_acc_revenue.get( + "/dimensions/default.hard_hat/nodes/", + ) + data = response.json() + roads_nodes = { + "default.repair_orders", + "default.repair_order_details", + "default.regional_level_agg", + "default.national_level_agg", + "default.regional_repair_efficiency", + "default.num_repair_orders", + "default.avg_repair_price", + "default.repair_orders_fact", + "default.total_repair_cost", + "default.discounted_orders_rate", + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + "default.avg_time_to_dispatch", + } + assert {node["name"] for node in data} == roads_nodes + + response = await module__client_with_roads_and_acc_revenue.get( + "/dimensions/default.repair_order/nodes/", + ) + data = response.json() + assert {node["name"] for node in data} == roads_nodes + + response = await module__client_with_roads_and_acc_revenue.get( + "/dimensions/default.us_state/nodes/", + ) + data = response.json() + assert {node["name"] for node in data} == { + "default.repair_orders", + "default.repair_order_details", + "default.regional_level_agg", + "default.national_level_agg", + "default.regional_repair_efficiency", + "default.num_repair_orders", + "default.avg_repair_price", + "default.repair_orders_fact", + "default.total_repair_cost", + "default.discounted_orders_rate", + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + "default.avg_time_to_dispatch", + "default.contractors", + "default.repair_type", + } + + response = await module__client_with_roads_and_acc_revenue.get( + "/dimensions/default.municipality_dim/nodes/", + ) + data = response.json() + assert {node["name"] for node in data} == roads_nodes + + response = await module__client_with_roads_and_acc_revenue.get( + "/dimensions/default.contractor/nodes/", + ) + data = response.json() + assert {node["name"] for node in data} == { + "default.repair_type", + "default.regional_level_agg", + "default.regional_repair_efficiency", + } + + response = await module__client_with_roads_and_acc_revenue.get( + "/dimensions/default.municipality_dim/nodes/?node_type=metric", + ) + data = response.json() + assert {node["name"] for node in data} == { + "default.regional_repair_efficiency", + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + "default.discounted_orders_rate", + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + "default.avg_time_to_dispatch", + } + + +@pytest.mark.asyncio +async def test_list_nodes_with_common_dimension( + module__client_with_roads_and_acc_revenue: AsyncClient, +) -> None: + """ + Test ``GET /dimensions/common/``. + """ + response = await module__client_with_roads_and_acc_revenue.get( + "/dimensions/common/?dimension=default.hard_hat", + ) + data = response.json() + roads_nodes = { + "default.repair_orders", + "default.repair_order_details", + "default.regional_level_agg", + "default.national_level_agg", + "default.regional_repair_efficiency", + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + "default.repair_orders_fact", + "default.discounted_orders_rate", + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + "default.avg_time_to_dispatch", + } + assert {node["name"] for node in data} == roads_nodes + + response = await module__client_with_roads_and_acc_revenue.get( + "/dimensions/common/?dimension=default.hard_hat&dimension=default.us_state" + "&dimension=default.dispatcher&dimension=default.municipality_dim", + ) + data = response.json() + assert {node["name"] for node in data} == roads_nodes + + response = await module__client_with_roads_and_acc_revenue.get( + "/dimensions/common/?dimension=default.hard_hat&dimension=default.us_state" + "&dimension=default.dispatcher&dimension=default.payment_type", + ) + data = response.json() + assert {node["name"] for node in data} == set() + + response = await module__client_with_roads_and_acc_revenue.get( + "/dimensions/common/?dimension=default.hard_hat&dimension=default.us_state" + "&dimension=default.dispatcher&node_type=metric", + ) + data = response.json() + assert {node["name"] for node in data} == { + "default.regional_repair_efficiency", + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + "default.discounted_orders_rate", + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + "default.avg_time_to_dispatch", + } diff --git a/datajunction-server/tests/api/djql_test.py b/datajunction-server/tests/api/djql_test.py new file mode 100644 index 000000000..1eb90977e --- /dev/null +++ b/datajunction-server/tests/api/djql_test.py @@ -0,0 +1,431 @@ +""" +Tests for the djsql API. +""" + +import pytest +from httpx import AsyncClient + +from tests.sql.utils import assert_query_strings_equal, compare_query_strings + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="Will move djsql to new sql build later") +async def test_get_djsql_data_only_nodes_query( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test djsql with just some non-metric nodes + """ + + query = """ +SELECT default.hard_hat.country, + default.hard_hat.city +FROM default.hard_hat + """ + + response = await module__client_with_roads.get( + "/djsql/data/", + params={"query": query}, + ) + data = response.json()["results"][0]["rows"] + assert data == [ + ["USA", "Jersey City"], + ["USA", "Middletown"], + ["USA", "Billerica"], + ["USA", "Southampton"], + ["USA", "Southgate"], + ["USA", "Powder Springs"], + ["USA", "Niagara Falls"], + ["USA", "Phoenix"], + ["USA", "Muskogee"], + ] + query = response.json()["results"][0]["sql"] + expected_query = """WITH + node_query_0 AS (SELECT default_DOT_hard_hat.hard_hat_id, + default_DOT_hard_hat.last_name, + default_DOT_hard_hat.first_name, + default_DOT_hard_hat.title, + default_DOT_hard_hat.birth_date, + default_DOT_hard_hat.hire_date, + default_DOT_hard_hat.address, + default_DOT_hard_hat.city, + default_DOT_hard_hat.state, + default_DOT_hard_hat.postal_code, + default_DOT_hard_hat.country, + default_DOT_hard_hat.manager, + default_DOT_hard_hat.contractor_id + FROM (SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats) + AS default_DOT_hard_hat + + ) + + SELECT node_query_0.country, + node_query_0.city + FROM node_query_0""" + assert compare_query_strings(query, expected_query) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="Will move djsql to new sql build later") +async def test_get_djsql_data_only_nested_metrics( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test djsql with metric subquery + """ + + query = """ + SELECT + Sum(avg_repair_price), + city + FROM + ( + SELECT + default.avg_repair_price avg_repair_price, + default.hard_hat.country country, + default.hard_hat.city city + FROM + metrics + GROUP BY + default.hard_hat.country, + default.hard_hat.city + LIMIT + 5 + ) + GROUP BY city + """ + + response = await module__client_with_roads.get( + "/djsql/data/", + params={"query": query}, + ) + rows = response.json()["results"][0]["rows"] + assert rows == [ + [54672.75, "Jersey City"], + [76555.33333333333, "Billerica"], + [64190.6, "Southgate"], + [65682.0, "Phoenix"], + [54083.5, "Southampton"], + ] + + query = response.json()["results"][0]["sql"] + expected_query = """WITH + metric_query_0 AS (SELECT default_DOT_repair_orders_fact.default_DOT_avg_repair_price, + default_DOT_repair_orders_fact.default_DOT_hard_hat_DOT_country, + default_DOT_repair_orders_fact.default_DOT_hard_hat_DOT_city + FROM (SELECT default_DOT_hard_hat.country default_DOT_hard_hat_DOT_country, + default_DOT_hard_hat.city default_DOT_hard_hat_DOT_city, + avg(default_DOT_repair_orders_fact.price) default_DOT_avg_repair_price + FROM (SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id) + AS default_DOT_repair_orders_fact LEFT JOIN (SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.country + FROM roads.hard_hats AS default_DOT_hard_hats) + AS default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + GROUP BY default_DOT_hard_hat.country, default_DOT_hard_hat.city) AS default_DOT_repair_orders_fact + + LIMIT 5) + + SELECT Sum(avg_repair_price), + city + FROM (SELECT metric_query_0.default_DOT_avg_repair_price AS avg_repair_price, + metric_query_0.default_DOT_hard_hat_DOT_country AS country, + metric_query_0.default_DOT_hard_hat_DOT_city AS city + FROM metric_query_0) + GROUP BY city""" + assert_query_strings_equal(query, expected_query) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="Will move djsql to new sql build later") +async def test_get_djsql_data_only_multiple_metrics( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test djsql with metric subquery + """ + + query = """ + SELECT + default.avg_repair_price avg_repair_price, + default.total_repair_cost total_cost, + default.hard_hat.country, + default.hard_hat.city + FROM + metrics + GROUP BY + default.hard_hat.country, + default.hard_hat.city + """ + + response = await module__client_with_roads.get( + "/djsql/data/", + params={"query": query}, + ) + data = response.json()["results"][0]["rows"] + assert data == [ + [54672.75, 218691.0, "USA", "Jersey City"], + [76555.33333333333, 229666.0, "USA", "Billerica"], + [64190.6, 320953.0, "USA", "Southgate"], + [65682.0, 131364.0, "USA", "Phoenix"], + [54083.5, 216334.0, "USA", "Southampton"], + [65595.66666666667, 196787.0, "USA", "Powder Springs"], + [39301.5, 78603.0, "USA", "Middletown"], + [70418.0, 70418.0, "USA", "Muskogee"], + [53374.0, 53374.0, "USA", "Niagara Falls"], + ] + query = response.json()["results"][0]["sql"] + expected_query = """WITH + metric_query_0 AS (SELECT default_DOT_repair_orders_fact.default_DOT_avg_repair_price, + default_DOT_repair_orders_fact.default_DOT_total_repair_cost, + default_DOT_repair_orders_fact.default_DOT_hard_hat_DOT_country, + default_DOT_repair_orders_fact.default_DOT_hard_hat_DOT_city + FROM (SELECT default_DOT_hard_hat.country default_DOT_hard_hat_DOT_country, + default_DOT_hard_hat.city default_DOT_hard_hat_DOT_city, + avg(default_DOT_repair_orders_fact.price) default_DOT_avg_repair_price, + sum(default_DOT_repair_orders_fact.total_repair_cost) default_DOT_total_repair_cost + FROM (SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id) + AS default_DOT_repair_orders_fact LEFT JOIN (SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.country + FROM roads.hard_hats AS default_DOT_hard_hats) + AS default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + GROUP BY default_DOT_hard_hat.country, default_DOT_hard_hat.city) AS default_DOT_repair_orders_fact) + + SELECT metric_query_0.default_DOT_avg_repair_price AS avg_repair_price, + metric_query_0.default_DOT_total_repair_cost AS total_cost, + metric_query_0.default_DOT_hard_hat_DOT_country, + metric_query_0.default_DOT_hard_hat_DOT_city + FROM metric_query_0""" + assert compare_query_strings(query, expected_query) + + +@pytest.mark.asyncio +async def test_get_djsql_metric_table_exception( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test djsql with metric subquery from non `metrics` + """ + + query = """ + SELECT + default.avg_repair_price avg_repair_price, + default.hard_hat.country, + default.hard_hat.city + FROM + oops + GROUP BY + default.hard_hat.country, + default.hard_hat.city + + """ + + response = await module__client_with_roads.get( + "/djsql/data/", + params={"query": query}, + ) + assert ( + response.json()["message"] + == "Any SELECT referencing a Metric must source from a single unaliased Table named `metrics`." + ) + + +@pytest.mark.asyncio +async def test_get_djsql_illegal_clause_metric_query( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test djsql with metric subquery from non `metrics` + """ + + query = """ + SELECT + default.avg_repair_price avg_repair_price, + default.hard_hat.country, + default.hard_hat.city + FROM + metrics + GROUP BY + default.hard_hat.country, + default.hard_hat.city + HAVING 5 + """ + + response = await module__client_with_roads.get( + "/djsql/data/", + params={"query": query}, + ) + assert ( + response.json()["message"] + == "HAVING, LATERAL VIEWS, and SET OPERATIONS are not allowed on `metrics` queries." + ) + + +@pytest.mark.asyncio +async def test_get_djsql_illegal_column_expression( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test djsql with non col exp in projection + """ + + query = """ + SELECT + default.avg_repair_price avg_repair_price, + default.hard_hat.country, + default.hard_hat.id+5 + FROM + metrics + GROUP BY + default.hard_hat.country, + default.hard_hat.city + """ + + response = await module__client_with_roads.get( + "/djsql/data/", + params={"query": query}, + ) + assert ( + response.json()["message"] + == "Only direct Columns are allowed in `metrics` queries, found `default.hard_hat.id + 5`." + ) + + +@pytest.mark.asyncio +async def test_get_djsql_illegal_column( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test djsql with bad col in projection + """ + + query = """ + SELECT + default.avg_repair_price avg_repair_price, + default.hard_hat.country, + default.repair_orders.id + FROM + metrics + GROUP BY + default.hard_hat.country, + default.hard_hat.city + """ + + response = await module__client_with_roads.get( + "/djsql/data/", + params={"query": query}, + ) + assert ( + response.json()["message"] + == "You can only select direct METRIC nodes or a column from your GROUP BY on `metrics` queries, found `default.repair_orders.id`" + ) + + +@pytest.mark.asyncio +async def test_get_djsql_illegal_limit( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test djsql with bad limit + """ + + query = """ + SELECT + default.avg_repair_price avg_repair_price + FROM + metrics + GROUP BY + default.hard_hat.country + LIMIT 1+2 + """ + + response = await module__client_with_roads.get( + "/djsql/data/", + params={"query": query}, + ) + assert ( + response.json()["message"] + == "LIMITs on `metrics` queries can only be integers not `1 + 2`." + ) + + +@pytest.mark.asyncio +async def test_get_djsql_no_nodes( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test djsql without dj node refs + """ + + query = """ + SELECT 1 + """ + + response = await module__client_with_roads.get( + "/djsql/data/", + params={"query": query}, + ) + assert response.json()["message"].startswith("Found no dj nodes in query") + + +@pytest.mark.asyncio +async def test_djsql_stream( + module__client: AsyncClient, +) -> None: + """ + Test streaming djsql + """ + query = """ + SELECT 1 + """ + + response = await module__client.get( + "/djsql/stream/", + params={"query": query}, + ) + assert response.status_code == 422 + assert response.json()["message"].startswith("Found no dj nodes in query") diff --git a/datajunction-server/tests/api/engine_test.py b/datajunction-server/tests/api/engine_test.py new file mode 100644 index 000000000..592d353e7 --- /dev/null +++ b/datajunction-server/tests/api/engine_test.py @@ -0,0 +1,153 @@ +""" +Tests for the engine API. +""" + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_engine_adding_a_new_engine( + module__client: AsyncClient, +) -> None: + """ + Test adding an engine + """ + response = await module__client.post( + "/engines/", + json={ + "name": "spark-one", + "version": "3.3.1", + "dialect": "spark", + }, + ) + data = response.json() + assert response.status_code == 201 + assert data == { + "dialect": "spark", + "name": "spark-one", + "uri": None, + "version": "3.3.1", + } + + +@pytest.mark.asyncio +async def test_engine_list( + module__client: AsyncClient, +) -> None: + """ + Test listing engines + """ + response = await module__client.post( + "/engines/", + json={ + "name": "spark-foo", + "version": "2.4.4", + "dialect": "spark", + }, + ) + assert response.status_code == 201 + + response = await module__client.post( + "/engines/", + json={ + "name": "spark-foo", + "version": "3.3.0", + "dialect": "spark", + }, + ) + assert response.status_code == 201 + + response = await module__client.post( + "/engines/", + json={ + "name": "spark-foo", + "version": "3.3.1", + "dialect": "spark", + }, + ) + assert response.status_code == 201 + + response = await module__client.get("/engines/") + assert response.status_code == 200 + data = [engine for engine in response.json() if engine["name"] == "spark-foo"] + assert data == [ + { + "name": "spark-foo", + "uri": None, + "version": "2.4.4", + "dialect": "spark", + }, + { + "name": "spark-foo", + "uri": None, + "version": "3.3.0", + "dialect": "spark", + }, + { + "name": "spark-foo", + "uri": None, + "version": "3.3.1", + "dialect": "spark", + }, + ] + + +@pytest.mark.asyncio +async def test_engine_get_engine( + module__client: AsyncClient, +) -> None: + """ + Test getting an engine + """ + response = await module__client.post( + "/engines/", + json={ + "name": "spark-two", + "version": "3.3.1", + "dialect": "spark", + }, + ) + assert response.status_code == 201 + + response = await module__client.get( + "/engines/spark-two/3.3.1", + ) + assert response.status_code == 200 + data = response.json() + assert data == { + "name": "spark-two", + "uri": None, + "version": "3.3.1", + "dialect": "spark", + } + + +@pytest.mark.asyncio +async def test_engine_raise_on_engine_already_exists( + module__client: AsyncClient, +) -> None: + """ + Test raise on engine already exists + """ + response = await module__client.post( + "/engines/", + json={ + "name": "spark-three", + "version": "3.3.1", + "dialect": "spark", + }, + ) + assert response.status_code == 201 + + response = await module__client.post( + "/engines/", + json={ + "name": "spark-three", + "version": "3.3.1", + "dialect": "spark", + }, + ) + assert response.status_code == 409 + data = response.json() + assert data == {"detail": "Engine already exists: `spark-three` version `3.3.1`"} diff --git a/datajunction-server/tests/api/files/client_test/create_cube.repairs_cube.namespace.txt b/datajunction-server/tests/api/files/client_test/create_cube.repairs_cube.namespace.txt new file mode 100644 index 000000000..cdb406db2 --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/create_cube.repairs_cube.namespace.txt @@ -0,0 +1,16 @@ +repairs_cube = dj.create_cube( + name=f"{namespace}.repairs_cube", + display_name="Repairs Cube", + description="""Cube of various metrics related to repairs""", + dimensions=[ + "default.hard_hat.country", + "default.hard_hat.city", + ], + metrics=[ + f"{namespace}.num_repair_orders", + f"{namespace}.total_repair_cost", + ], + mode="published", + tags=[], + update_if_exists=True, +) diff --git a/datajunction-server/tests/api/files/client_test/create_cube.repairs_cube.txt b/datajunction-server/tests/api/files/client_test/create_cube.repairs_cube.txt new file mode 100644 index 000000000..85c8fced6 --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/create_cube.repairs_cube.txt @@ -0,0 +1,16 @@ +repairs_cube = dj.create_cube( + name="default.repairs_cube", + display_name="Repairs Cube", + description="""Cube of various metrics related to repairs""", + dimensions=[ + "default.hard_hat.country", + "default.hard_hat.city", + ], + metrics=[ + "default.num_repair_orders", + "default.total_repair_cost", + ], + mode="published", + tags=[], + update_if_exists=True, +) diff --git a/datajunction-server/tests/api/files/client_test/create_dimension.repair_order.namespace.txt b/datajunction-server/tests/api/files/client_test/create_dimension.repair_order.namespace.txt new file mode 100644 index 000000000..c0bd0d5ef --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/create_dimension.repair_order.namespace.txt @@ -0,0 +1,19 @@ +repair_order = dj.create_dimension( + name=f"{namespace}.repair_order", + display_name="Repair Order", + description="""Repair order dimension""", + mode="published", + primary_key=[ + "repair_order_id", + ], + tags=[], + query=f"""SELECT repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM {namespace}.repair_orders""", + update_if_exists=True, +) diff --git a/datajunction-server/tests/api/files/client_test/create_dimension.repair_order.txt b/datajunction-server/tests/api/files/client_test/create_dimension.repair_order.txt new file mode 100644 index 000000000..ab62883b7 --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/create_dimension.repair_order.txt @@ -0,0 +1,20 @@ +repair_order = dj.create_dimension( + name="default.repair_order", + display_name="Repair Order", + description="""Repair order dimension""", + mode="published", + primary_key=[ + "repair_order_id", + ], + tags=[], + query="""SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM default.repair_orders""", + update_if_exists=True, +) diff --git a/datajunction-server/tests/api/files/client_test/create_metric.num_repair_orders.namespace.txt b/datajunction-server/tests/api/files/client_test/create_metric.num_repair_orders.namespace.txt new file mode 100644 index 000000000..562336d2a --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/create_metric.num_repair_orders.namespace.txt @@ -0,0 +1,13 @@ +num_repair_orders = dj.create_metric( + name=f"{namespace}.num_repair_orders", + display_name="Num Repair Orders", + description="""Number of repair orders""", + mode="published", + required_dimensions=[], + tags=[], + query=f"""SELECT count(repair_order_id) + FROM {namespace}.repair_orders_fact""", + direction=MetricDirection.HIGHER_IS_BETTER, + unit=MetricUnit.DOLLAR, + update_if_exists=True, +) diff --git a/datajunction-server/tests/api/files/client_test/create_metric.num_repair_orders.txt b/datajunction-server/tests/api/files/client_test/create_metric.num_repair_orders.txt new file mode 100644 index 000000000..8675f99fc --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/create_metric.num_repair_orders.txt @@ -0,0 +1,12 @@ +num_repair_orders = dj.create_metric( + name="default.num_repair_orders", + display_name="Num Repair Orders", + description="""Number of repair orders""", + mode="published", + required_dimensions=[], + tags=[], + query="""SELECT count(repair_order_id) FROM default.repair_orders_fact""", + direction=MetricDirection.HIGHER_IS_BETTER, + unit=MetricUnit.DOLLAR, + update_if_exists=True, +) diff --git a/datajunction-server/tests/api/files/client_test/create_transform.regional_level_agg.namespace.txt b/datajunction-server/tests/api/files/client_test/create_transform.regional_level_agg.namespace.txt new file mode 100644 index 000000000..6dadd8fe7 --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/create_transform.regional_level_agg.namespace.txt @@ -0,0 +1,52 @@ +regional_level_agg = dj.create_transform( + name=f"{namespace}.regional_level_agg", + display_name="Regional Level Agg", + description="""Regional-level aggregates""", + mode="published", + primary_key=[ + "us_region_id", + "state_name", + "order_year", + "order_month", + "order_day", + ], + tags=[], + query=f"""WITH +ro AS ( +SELECT repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM {namespace}.repair_orders +) + +SELECT usr.us_region_id, + us.state_name, + CONCAT(us.state_name, '-', usr.us_region_description) AS location_hierarchy, + EXTRACT(YEAR, ro.order_date) AS order_year, + EXTRACT(MONTH, ro.order_date) AS order_month, + EXTRACT(DAY, ro.order_date) AS order_day, + COUNT( DISTINCT CASE + WHEN ro.dispatched_date IS NOT NULL THEN ro.repair_order_id + ELSE NULL + END) AS completed_repairs, + COUNT( DISTINCT ro.repair_order_id) AS total_repairs_dispatched, + SUM(rd.price * rd.quantity) AS total_amount_in_region, + AVG(rd.price * rd.quantity) AS avg_repair_amount_in_region, + AVG(DATEDIFF(ro.dispatched_date, ro.order_date)) AS avg_dispatch_delay, + COUNT( DISTINCT c.contractor_id) AS unique_contractors + FROM ro JOIN {namespace}.municipality m ON ro.municipality_id = m.municipality_id +JOIN {namespace}.us_states us ON m.state_id = us.state_id AND AVG(rd.price * rd.quantity) > (SELECT AVG(price * quantity) + FROM {namespace}.repair_order_details + WHERE repair_order_id = ro.repair_order_id) +JOIN {namespace}.us_states us ON m.state_id = us.state_id +JOIN {namespace}.us_region usr ON us.state_region = usr.us_region_id +JOIN {namespace}.repair_order_details rd ON ro.repair_order_id = rd.repair_order_id +JOIN {namespace}.repair_type rt ON rd.repair_type_id = rt.repair_type_id +JOIN {namespace}.contractors c ON rt.contractor_id = c.contractor_id + GROUP BY usr.us_region_id, EXTRACT(YEAR, ro.order_date), EXTRACT(MONTH, ro.order_date), EXTRACT(DAY, ro.order_date)""", + update_if_exists=True, +) diff --git a/datajunction-server/tests/api/files/client_test/create_transform.regional_level_agg.txt b/datajunction-server/tests/api/files/client_test/create_transform.regional_level_agg.txt new file mode 100644 index 000000000..77a1260dd --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/create_transform.regional_level_agg.txt @@ -0,0 +1,60 @@ +regional_level_agg = dj.create_transform( + name="default.regional_level_agg", + display_name="Regional Level Agg", + description="""Regional-level aggregates""", + mode="published", + primary_key=[ + "us_region_id", + "state_name", + "order_year", + "order_month", + "order_day", + ], + tags=[], + query="""WITH ro as (SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM default.repair_orders) + SELECT + usr.us_region_id, + us.state_name, + CONCAT(us.state_name, '-', usr.us_region_description) AS location_hierarchy, + EXTRACT(YEAR FROM ro.order_date) AS order_year, + EXTRACT(MONTH FROM ro.order_date) AS order_month, + EXTRACT(DAY FROM ro.order_date) AS order_day, + COUNT(DISTINCT CASE WHEN ro.dispatched_date IS NOT NULL THEN ro.repair_order_id ELSE NULL END) AS completed_repairs, + COUNT(DISTINCT ro.repair_order_id) AS total_repairs_dispatched, + SUM(rd.price * rd.quantity) AS total_amount_in_region, + AVG(rd.price * rd.quantity) AS avg_repair_amount_in_region, + -- ELEMENT_AT(ARRAY_SORT(COLLECT_LIST(STRUCT(COUNT(*) AS cnt, rt.repair_type_name AS repair_type_name)), (left, right) -> case when left.cnt < right.cnt then 1 when left.cnt > right.cnt then -1 else 0 end), 0).repair_type_name AS most_common_repair_type, + AVG(DATEDIFF(ro.dispatched_date, ro.order_date)) AS avg_dispatch_delay, + COUNT(DISTINCT c.contractor_id) AS unique_contractors +FROM ro +JOIN + default.municipality m ON ro.municipality_id = m.municipality_id +JOIN + default.us_states us ON m.state_id = us.state_id + AND AVG(rd.price * rd.quantity) > + (SELECT AVG(price * quantity) FROM default.repair_order_details WHERE repair_order_id = ro.repair_order_id) +JOIN + default.us_states us ON m.state_id = us.state_id +JOIN + default.us_region usr ON us.state_region = usr.us_region_id +JOIN + default.repair_order_details rd ON ro.repair_order_id = rd.repair_order_id +JOIN + default.repair_type rt ON rd.repair_type_id = rt.repair_type_id +JOIN + default.contractors c ON rt.contractor_id = c.contractor_id +GROUP BY + usr.us_region_id, + EXTRACT(YEAR FROM ro.order_date), + EXTRACT(MONTH FROM ro.order_date), + EXTRACT(DAY FROM ro.order_date)""", + update_if_exists=True, +) diff --git a/datajunction-server/tests/api/files/client_test/dimension_links.repair_orders.namespace.txt b/datajunction-server/tests/api/files/client_test/dimension_links.repair_orders.namespace.txt new file mode 100644 index 000000000..9d01dae8c --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/dimension_links.repair_orders.namespace.txt @@ -0,0 +1,13 @@ +repair_orders = dj.node("default.repair_orders") + +repair_orders.link_complex_dimension( + dimension_node=f"{namespace}.repair_order", + join_on=f"{namespace}.repair_orders.repair_order_id = {namespace}.repair_order.repair_order_id", + join_type="inner", +) + +repair_orders.link_complex_dimension( + dimension_node=f"{namespace}.dispatcher", + join_on=f"{namespace}.repair_orders.dispatcher_id = {namespace}.dispatcher.dispatcher_id", + join_type="inner", +) diff --git a/datajunction-server/tests/api/files/client_test/dimension_links.repair_orders.txt b/datajunction-server/tests/api/files/client_test/dimension_links.repair_orders.txt new file mode 100644 index 000000000..b0fa8da37 --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/dimension_links.repair_orders.txt @@ -0,0 +1,13 @@ +repair_orders = dj.node("default.repair_orders") + +repair_orders.link_complex_dimension( + dimension_node=f"default.repair_order", + join_on=f"default.repair_orders.repair_order_id = default.repair_order.repair_order_id", + join_type="inner", +) + +repair_orders.link_complex_dimension( + dimension_node=f"default.dispatcher", + join_on=f"default.repair_orders.dispatcher_id = default.dispatcher.dispatcher_id", + join_type="inner", +) diff --git a/datajunction-server/tests/api/files/client_test/include_client_setup.txt b/datajunction-server/tests/api/files/client_test/include_client_setup.txt new file mode 100644 index 000000000..2760deb4c --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/include_client_setup.txt @@ -0,0 +1,22 @@ +from datajunction import ( + DJBuilder, Source, Dimension, Transform, Metric, + Namespace, MetricUnit, MetricDirection, ColumnAttribute, +) + +DJ_URL = "http://test" + +dj = DJBuilder(DJ_URL) +dj.basic_login("dj", "dj") + +num_repair_orders = dj.create_metric( + name="default.num_repair_orders", + display_name="Num Repair Orders", + description="""Number of repair orders""", + mode="published", + required_dimensions=[], + tags=[], + query="""SELECT count(repair_order_id) FROM default.repair_orders_fact""", + direction=MetricDirection.HIGHER_IS_BETTER, + unit=MetricUnit.DOLLAR, + update_if_exists=True, +) diff --git a/datajunction-server/tests/api/files/client_test/notebook.create_cube.txt b/datajunction-server/tests/api/files/client_test/notebook.create_cube.txt new file mode 100644 index 000000000..8b77beca1 --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/notebook.create_cube.txt @@ -0,0 +1,16 @@ +roads_cube = dj.create_cube( + name=f"{NAMESPACE_MAPPING['default']}.roads_cube", + display_name="Roads Cube", + description="""Cube of various metrics related to repairs""", + dimensions=[ + "default.hard_hat.country", + "default.hard_hat.city", + ], + metrics=[ + f"{NAMESPACE_MAPPING['default']}.num_repair_orders", + f"{NAMESPACE_MAPPING['default']}.total_repair_cost", + ], + mode="published", + tags=[], + update_if_exists=True, +) diff --git a/datajunction-server/tests/api/files/client_test/notebook.create_transform.txt b/datajunction-server/tests/api/files/client_test/notebook.create_transform.txt new file mode 100644 index 000000000..da37bc1bf --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/notebook.create_transform.txt @@ -0,0 +1,24 @@ +repair_orders_fact = dj.create_transform( + name=f"{NAMESPACE_MAPPING['default']}.repair_orders_fact", + display_name="Repair Orders Fact", + description="""Fact transform with all details on repair orders""", + mode="published", + primary_key=[], + tags=[], + query=f"""SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM {NAMESPACE_MAPPING['default']}.repair_orders repair_orders JOIN {NAMESPACE_MAPPING['default']}.repair_order_details repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id""", + update_if_exists=True, +) diff --git a/datajunction-server/tests/api/files/client_test/notebook.link_dimension.txt b/datajunction-server/tests/api/files/client_test/notebook.link_dimension.txt new file mode 100644 index 000000000..b96de9a3c --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/notebook.link_dimension.txt @@ -0,0 +1,5 @@ +repair_order_details.link_complex_dimension( + dimension_node=f"{NAMESPACE_MAPPING['default']}.repair_order", + join_on=f"{NAMESPACE_MAPPING['default']}.repair_order_details.repair_order_id = {NAMESPACE_MAPPING['default']}.repair_order.repair_order_id", + join_type="inner", +) diff --git a/datajunction-server/tests/api/files/client_test/notebook.set_attribute.txt b/datajunction-server/tests/api/files/client_test/notebook.set_attribute.txt new file mode 100644 index 000000000..79ddd29f8 --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/notebook.set_attribute.txt @@ -0,0 +1,6 @@ +repair_order_details.set_column_attributes( + "repair_type_id", + [ + ColumnAttribute(namespace="system", name="dimension"), + ], +) diff --git a/datajunction-server/tests/api/files/client_test/register_table.txt b/datajunction-server/tests/api/files/client_test/register_table.txt new file mode 100644 index 000000000..539158956 --- /dev/null +++ b/datajunction-server/tests/api/files/client_test/register_table.txt @@ -0,0 +1,5 @@ +dj.register_table( + catalog="default", + schema="roads", + table="repair_order_details", +) diff --git a/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.full.druid_spec.json b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.full.druid_spec.json new file mode 100644 index 000000000..38729435c --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.full.druid_spec.json @@ -0,0 +1,49 @@ +{ + "dataSchema":{ + "dataSource":"default_DOT_repairs_cube", + "granularitySpec":{ + "intervals":[ + + ], + "segmentGranularity":"DAY", + "type":"uniform" + }, + "metricsSpec":[ + { + "fieldName":"default_DOT_repair_orders_fact_DOT_repair_order_id", + "name":"default_DOT_repair_orders_fact_DOT_repair_order_id", + "type":"longSum" + }, + { + "fieldName":"default_DOT_repair_orders_fact_DOT_total_repair_cost", + "name":"default_DOT_repair_orders_fact_DOT_total_repair_cost", + "type":"floatSum" + } + ], + "parser":{ + "parseSpec":{ + "dimensionsSpec":{ + "dimensions":[ + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_hard_hat_DOT_state", + "default_DOT_municipality_dim_DOT_local_region", + "default_DOT_repair_orders_fact_DOT_order_date" + ] + }, + "format":"parquet", + "timestampSpec":{ + "column":"default_DOT_repair_orders_fact_DOT_order_date", + "format":"yyyyMMdd" + } + } + } + }, + "tuningConfig":{ + "partitionsSpec":{ + "targetPartitionSize":5000000, + "type":"hashed" + }, + "type":"hadoop", + "useCombiner":true + } +} diff --git a/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.full.partition.druid_spec.json b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.full.partition.druid_spec.json new file mode 100644 index 000000000..38729435c --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.full.partition.druid_spec.json @@ -0,0 +1,49 @@ +{ + "dataSchema":{ + "dataSource":"default_DOT_repairs_cube", + "granularitySpec":{ + "intervals":[ + + ], + "segmentGranularity":"DAY", + "type":"uniform" + }, + "metricsSpec":[ + { + "fieldName":"default_DOT_repair_orders_fact_DOT_repair_order_id", + "name":"default_DOT_repair_orders_fact_DOT_repair_order_id", + "type":"longSum" + }, + { + "fieldName":"default_DOT_repair_orders_fact_DOT_total_repair_cost", + "name":"default_DOT_repair_orders_fact_DOT_total_repair_cost", + "type":"floatSum" + } + ], + "parser":{ + "parseSpec":{ + "dimensionsSpec":{ + "dimensions":[ + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_hard_hat_DOT_state", + "default_DOT_municipality_dim_DOT_local_region", + "default_DOT_repair_orders_fact_DOT_order_date" + ] + }, + "format":"parquet", + "timestampSpec":{ + "column":"default_DOT_repair_orders_fact_DOT_order_date", + "format":"yyyyMMdd" + } + } + } + }, + "tuningConfig":{ + "partitionsSpec":{ + "targetPartitionSize":5000000, + "type":"hashed" + }, + "type":"hadoop", + "useCombiner":true + } +} diff --git a/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.full.partition.query.sql b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.full.partition.query.sql new file mode 100644 index 000000000..ff47d1362 --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.full.partition.query.sql @@ -0,0 +1,48 @@ +WITH +default_DOT_repair_orders_fact AS (SELECT default_DOT_repair_orders_fact.repair_order_id default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact.order_date default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact.total_repair_cost default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region default_DOT_municipality_dim_DOT_local_region + FROM (SELECT default_DOT_repair_orders.repair_order_id, + default_DOT_repair_orders.municipality_id, + default_DOT_repair_orders.hard_hat_id, + default_DOT_repair_orders.dispatcher_id, + default_DOT_repair_orders.order_date, + default_DOT_repair_orders.dispatched_date, + default_DOT_repair_orders.required_date, + default_DOT_repair_order_details.discount, + default_DOT_repair_order_details.price, + default_DOT_repair_order_details.quantity, + default_DOT_repair_order_details.repair_type_id, + default_DOT_repair_order_details.price * default_DOT_repair_order_details.quantity AS total_repair_cost, + default_DOT_repair_orders.dispatched_date - default_DOT_repair_orders.order_date AS time_to_dispatch, + default_DOT_repair_orders.dispatched_date - default_DOT_repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS default_DOT_repair_orders JOIN roads.repair_order_details AS default_DOT_repair_order_details ON default_DOT_repair_orders.repair_order_id = default_DOT_repair_order_details.repair_order_id) AS default_DOT_repair_orders_fact LEFT JOIN (SELECT default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name + FROM roads.dispatchers AS default_DOT_dispatchers) AS default_DOT_dispatcher ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id +LEFT JOIN (SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.state + FROM roads.hard_hats AS default_DOT_hard_hats) AS default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id +LEFT JOIN (SELECT default_DOT_municipality.municipality_id AS municipality_id, + default_DOT_municipality.local_region + FROM roads.municipality AS default_DOT_municipality LEFT JOIN roads.municipality_municipality_type AS default_DOT_municipality_municipality_type ON default_DOT_municipality.municipality_id = default_DOT_municipality_municipality_type.municipality_id +LEFT JOIN roads.municipality_type AS default_DOT_municipality_type ON default_DOT_municipality_municipality_type.municipality_type_id = default_DOT_municipality_type.municipality_type_desc) AS default_DOT_municipality_dim ON default_DOT_repair_orders_fact.municipality_id = default_DOT_municipality_dim.municipality_id), +combiner_query AS (SELECT default_DOT_repair_orders_fact.default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact.default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_repair_orders_fact.default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact.default_DOT_hard_hat_DOT_state, + default_DOT_repair_orders_fact.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact.default_DOT_municipality_dim_DOT_local_region, + CAST(CAST(default_DOT_repair_orders_fact.default_DOT_repair_orders_fact_DOT_order_date AS DOUBLE) * 1000 AS LONG) timestamp_column + FROM default_DOT_repair_orders_fact) + +SELECT default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim_DOT_local_region, + timestamp_column + FROM combiner_query diff --git a/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.full.query.sql b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.full.query.sql new file mode 100644 index 000000000..366ae99fa --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.full.query.sql @@ -0,0 +1,69 @@ + WITH + default_DOT_repair_orders_fact AS ( + SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_hard_hat AS ( + SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + ), + default_DOT_dispatcher AS ( + SELECT default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_municipality_dim AS ( + SELECT m.municipality_id AS municipality_id, + m.contact_name, + m.contact_title, + m.local_region, + m.state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM roads.municipality AS m LEFT JOIN roads.municipality_municipality_type AS mmt ON m.municipality_id = mmt.municipality_id + LEFT JOIN roads.municipality_type AS mt ON mmt.municipality_type_id = mt.municipality_type_desc + ), + combiner_query AS ( + SELECT default_DOT_repair_orders_fact.repair_order_id default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact.order_date default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact.total_repair_cost default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region default_DOT_municipality_dim_DOT_local_region + FROM default_DOT_repair_orders_fact INNER JOIN default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_municipality_dim ON default_DOT_repair_orders_fact.municipality_id = default_DOT_municipality_dim.municipality_id + ) + SELECT default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim_DOT_local_region + FROM combiner_query diff --git a/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.incremental.categorical.query.sql b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.incremental.categorical.query.sql new file mode 100644 index 000000000..5da383dfa --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.incremental.categorical.query.sql @@ -0,0 +1,71 @@ + WITH + default_DOT_repair_orders_fact AS ( + SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + WHERE repair_orders.order_date = ${dj_logical_timestamp} + ), + default_DOT_hard_hat AS ( + SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + ), + default_DOT_dispatcher AS ( + SELECT default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_municipality_dim AS ( + SELECT m.municipality_id AS municipality_id, + m.contact_name, + m.contact_title, + m.local_region, + m.state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM roads.municipality AS m LEFT JOIN roads.municipality_municipality_type AS mmt ON m.municipality_id = mmt.municipality_id + LEFT JOIN roads.municipality_type AS mt ON mmt.municipality_type_id = mt.municipality_type_desc + ), + combiner_query AS ( + SELECT default_DOT_repair_orders_fact.repair_order_id default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact.order_date default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact.total_repair_cost default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region default_DOT_municipality_dim_DOT_local_region + FROM default_DOT_repair_orders_fact INNER JOIN default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_municipality_dim ON default_DOT_repair_orders_fact.municipality_id = default_DOT_municipality_dim.municipality_id + ) + SELECT default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim_DOT_local_region + FROM combiner_query + WHERE default_DOT_repair_orders_fact_DOT_order_date = CAST(DATE_FORMAT(CAST(${dj_logical_timestamp} AS TIMESTAMP), 'yyyyMMdd') AS TIMESTAMP) AND default_DOT_dispatcher_DOT_company_name = CAST(DJ_COMPANY_NAME() AS STRING) diff --git a/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.incremental.druid_spec.json b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.incremental.druid_spec.json new file mode 100644 index 000000000..25b7b4daa --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.incremental.druid_spec.json @@ -0,0 +1,49 @@ +{ + "dataSchema":{ + "dataSource":"default_DOT_repairs_cube__incremental", + "granularitySpec":{ + "intervals":[ + + ], + "segmentGranularity":"DAY", + "type":"uniform" + }, + "metricsSpec":[ + { + "fieldName":"default_DOT_repair_orders_fact_DOT_repair_order_id", + "name":"default_DOT_repair_orders_fact_DOT_repair_order_id", + "type":"longSum" + }, + { + "fieldName":"default_DOT_repair_orders_fact_DOT_total_repair_cost", + "name":"default_DOT_repair_orders_fact_DOT_total_repair_cost", + "type":"floatSum" + } + ], + "parser":{ + "parseSpec":{ + "dimensionsSpec":{ + "dimensions":[ + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_hard_hat_DOT_state", + "default_DOT_municipality_dim_DOT_local_region", + "default_DOT_repair_orders_fact_DOT_order_date" + ] + }, + "format":"parquet", + "timestampSpec":{ + "column":"default_DOT_repair_orders_fact_DOT_order_date", + "format":"yyyyMMdd" + } + } + } + }, + "tuningConfig":{ + "partitionsSpec":{ + "targetPartitionSize":5000000, + "type":"hashed" + }, + "type":"hadoop", + "useCombiner":true + } +} diff --git a/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.incremental.patched.query.sql b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.incremental.patched.query.sql new file mode 100644 index 000000000..0d9c2fd05 --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.incremental.patched.query.sql @@ -0,0 +1,71 @@ + WITH + default_DOT_repair_orders_fact AS ( + SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + WHERE repair_orders.order_date = ${dj_logical_timestamp} + ), + default_DOT_hard_hat AS ( + SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + ), + default_DOT_dispatcher AS ( + SELECT default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_municipality_dim AS ( + SELECT m.municipality_id AS municipality_id, + m.contact_name, + m.contact_title, + m.local_region, + m.state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM roads.municipality AS m LEFT JOIN roads.municipality_municipality_type AS mmt ON m.municipality_id = mmt.municipality_id + LEFT JOIN roads.municipality_type AS mt ON mmt.municipality_type_id = mt.municipality_type_desc + ), + combiner_query AS ( + SELECT default_DOT_repair_orders_fact.repair_order_id default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact.order_date default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact.total_repair_cost default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region default_DOT_municipality_dim_DOT_local_region + FROM default_DOT_repair_orders_fact INNER JOIN default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_municipality_dim ON default_DOT_repair_orders_fact.municipality_id = default_DOT_municipality_dim.municipality_id + ) + SELECT default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim_DOT_local_region + FROM combiner_query + WHERE default_DOT_repair_orders_fact_DOT_order_date = CAST(DATE_FORMAT(CAST(${dj_logical_timestamp} AS TIMESTAMP), 'yyyyMMdd') AS TIMESTAMP) diff --git a/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.incremental.query.sql b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.incremental.query.sql new file mode 100644 index 000000000..c901e369c --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/druid_measures_cube.incremental.query.sql @@ -0,0 +1,70 @@ + WITH + default_DOT_repair_orders_fact AS ( + SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_hard_hat AS ( + SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + ), + default_DOT_dispatcher AS ( + SELECT default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_municipality_dim AS ( + SELECT m.municipality_id AS municipality_id, + m.contact_name, + m.contact_title, + m.local_region, + m.state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM roads.municipality AS m LEFT JOIN roads.municipality_municipality_type AS mmt ON m.municipality_id = mmt.municipality_id + LEFT JOIN roads.municipality_type AS mt ON mmt.municipality_type_id = mt.municipality_type_desc + ), + combiner_query AS ( + SELECT default_DOT_repair_orders_fact.repair_order_id default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact.order_date default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact.total_repair_cost default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region default_DOT_municipality_dim_DOT_local_region + FROM default_DOT_repair_orders_fact INNER JOIN default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_municipality_dim ON default_DOT_repair_orders_fact.municipality_id = default_DOT_municipality_dim.municipality_id + ) + SELECT default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim_DOT_local_region + FROM combiner_query + WHERE default_DOT_repair_orders_fact_DOT_order_date = CAST(DATE_FORMAT(CAST(${dj_logical_timestamp} AS TIMESTAMP), 'yyyyMMdd') AS TIMESTAMP) diff --git a/datajunction-server/tests/api/files/materializations_test/druid_metrics_cube.incremental.categorical.query.sql b/datajunction-server/tests/api/files/materializations_test/druid_metrics_cube.incremental.categorical.query.sql new file mode 100644 index 000000000..6dabc7cd8 --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/druid_metrics_cube.incremental.categorical.query.sql @@ -0,0 +1,72 @@ + WITH + default_DOT_repair_orders_fact AS ( + SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + WHERE repair_orders.order_date = ${dj_logical_timestamp} + ), + default_DOT_hard_hat AS ( + SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + ), + default_DOT_dispatcher AS ( + SELECT default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_municipality_dim AS ( + SELECT m.municipality_id AS municipality_id, + m.contact_name, + m.contact_title, + m.local_region, + m.state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM roads.municipality AS m LEFT JOIN roads.municipality_municipality_type AS mmt ON m.municipality_id = mmt.municipality_id + LEFT JOIN roads.municipality_type AS mt ON mmt.municipality_type_id = mt.municipality_type_desc + ), + combiner_query AS ( + SELECT default_DOT_repair_orders_fact.order_date AS default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_hard_hat.state AS default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher.company_name AS default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region AS default_DOT_municipality_dim_DOT_local_region, + COUNT(default_DOT_repair_orders_fact.repair_order_id) AS default_DOT_num_repair_orders, + SUM(default_DOT_repair_orders_fact.total_repair_cost) AS default_DOT_total_repair_cost + FROM default_DOT_repair_orders_fact INNER JOIN default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_municipality_dim ON default_DOT_repair_orders_fact.municipality_id = default_DOT_municipality_dim.municipality_id + GROUP BY default_DOT_repair_orders_fact.order_date, default_DOT_hard_hat.state, default_DOT_dispatcher.company_name, default_DOT_municipality_dim.local_region + ) + SELECT default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim_DOT_local_region, + default_DOT_num_repair_orders, + default_DOT_total_repair_cost + FROM combiner_query + WHERE default_DOT_repair_orders_fact_DOT_order_date = CAST(DATE_FORMAT(CAST(${dj_logical_timestamp} AS TIMESTAMP), 'yyyyMMdd') AS TIMESTAMP) AND default_DOT_hard_hat_DOT_state = CAST(DJ_STATE() AS STRING) diff --git a/datajunction-server/tests/api/files/materializations_test/druid_metrics_cube.incremental.druid_spec.json b/datajunction-server/tests/api/files/materializations_test/druid_metrics_cube.incremental.druid_spec.json new file mode 100644 index 000000000..b897bc93c --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/druid_metrics_cube.incremental.druid_spec.json @@ -0,0 +1,49 @@ +{ + "dataSchema":{ + "dataSource":"default_DOT_repairs_cube__metrics_incremental", + "parser":{ + "parseSpec":{ + "format":"parquet", + "dimensionsSpec":{ + "dimensions":[ + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_hard_hat_DOT_state", + "default_DOT_municipality_dim_DOT_local_region", + "default_DOT_repair_orders_fact_DOT_order_date" + ] + }, + "timestampSpec":{ + "column":"default_DOT_repair_orders_fact_DOT_order_date", + "format":"yyyyMMdd" + } + } + }, + "metricsSpec":[ + { + "fieldName":"default_DOT_num_repair_orders", + "name":"default_DOT_num_repair_orders", + "type":"longSum" + }, + { + "fieldName":"default_DOT_total_repair_cost", + "name":"default_DOT_total_repair_cost", + "type":"doubleSum" + } + ], + "granularitySpec":{ + "type":"uniform", + "segmentGranularity":"DAY", + "intervals":[ + + ] + } + }, + "tuningConfig":{ + "partitionsSpec":{ + "targetPartitionSize":5000000, + "type":"hashed" + }, + "useCombiner":true, + "type":"hadoop" + } +} diff --git a/datajunction-server/tests/api/files/materializations_test/druid_metrics_cube.incremental.query.sql b/datajunction-server/tests/api/files/materializations_test/druid_metrics_cube.incremental.query.sql new file mode 100644 index 000000000..ac29b8465 --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/druid_metrics_cube.incremental.query.sql @@ -0,0 +1,72 @@ + WITH + default_DOT_repair_orders_fact AS ( + SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + WHERE repair_orders.order_date = ${dj_logical_timestamp} + ), + default_DOT_hard_hat AS ( + SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + ), + default_DOT_dispatcher AS ( + SELECT default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_municipality_dim AS ( + SELECT m.municipality_id AS municipality_id, + m.contact_name, + m.contact_title, + m.local_region, + m.state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM roads.municipality AS m LEFT JOIN roads.municipality_municipality_type AS mmt ON m.municipality_id = mmt.municipality_id + LEFT JOIN roads.municipality_type AS mt ON mmt.municipality_type_id = mt.municipality_type_desc + ), + combiner_query AS ( + SELECT default_DOT_repair_orders_fact.order_date AS default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_hard_hat.state AS default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher.company_name AS default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region AS default_DOT_municipality_dim_DOT_local_region, + COUNT(default_DOT_repair_orders_fact.repair_order_id) AS default_DOT_num_repair_orders, + SUM(default_DOT_repair_orders_fact.total_repair_cost) AS default_DOT_total_repair_cost + FROM default_DOT_repair_orders_fact INNER JOIN default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_municipality_dim ON default_DOT_repair_orders_fact.municipality_id = default_DOT_municipality_dim.municipality_id + GROUP BY default_DOT_repair_orders_fact.order_date, default_DOT_hard_hat.state, default_DOT_dispatcher.company_name, default_DOT_municipality_dim.local_region + ) + SELECT default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim_DOT_local_region, + default_DOT_num_repair_orders, + default_DOT_total_repair_cost + FROM combiner_query + WHERE default_DOT_repair_orders_fact_DOT_order_date = CAST(DATE_FORMAT(CAST(${dj_logical_timestamp} AS TIMESTAMP), 'yyyyMMdd') AS TIMESTAMP) diff --git a/datajunction-server/tests/api/files/materializations_test/spark_sql.full.config.json b/datajunction-server/tests/api/files/materializations_test/spark_sql.full.config.json new file mode 100644 index 000000000..744860dd4 --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/spark_sql.full.config.json @@ -0,0 +1,124 @@ +[ + { + "backfills":[], + "config":{ + "columns":[ + { + "column": null, + "name":"hard_hat_id", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"int" + }, + { + "column": null, + "name":"last_name", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"first_name", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"title", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"birth_date", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"timestamp" + }, + { + "column": null, + "name":"hire_date", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"timestamp" + }, + { + "column": null, + "name":"address", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"city", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"state", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"postal_code", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"country", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"manager", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"int" + }, + { + "column": null, + "name":"contractor_id", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"int" + } + ], + "lookback_window": null, + "spark":{ + + }, + "upstream_tables":[ + "default.roads.hard_hats" + ] + }, + "job":"SparkSqlMaterializationJob", + "name":"spark_sql__full", + "schedule":"0 * * * *", + "strategy":"full" + } +] diff --git a/datajunction-server/tests/api/files/materializations_test/spark_sql.full.materializations.json b/datajunction-server/tests/api/files/materializations_test/spark_sql.full.materializations.json new file mode 100644 index 000000000..caaac97c0 --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/spark_sql.full.materializations.json @@ -0,0 +1,132 @@ +{ + "name":"spark_sql__full", + "config":{ + "spark":{ + + }, + "lookback_window": null, + "query":"SELECT default_DOT_hard_hat.hard_hat_id,\n\tdefault_DOT_hard_hat.last_name,\n\tdefault_DOT_hard_hat.first_name,\n\tdefault_DOT_hard_hat.title,\n\tdefault_DOT_hard_hat.birth_date,\n\tdefault_DOT_hard_hat.hire_date,\n\tdefault_DOT_hard_hat.address,\n\tdefault_DOT_hard_hat.city,\n\tdefault_DOT_hard_hat.state,\n\tdefault_DOT_hard_hat.postal_code,\n\tdefault_DOT_hard_hat.country,\n\tdefault_DOT_hard_hat.manager,\n\tdefault_DOT_hard_hat.contractor_id \n FROM (SELECT default_DOT_hard_hats.hard_hat_id,\n\tdefault_DOT_hard_hats.last_name,\n\tdefault_DOT_hard_hats.first_name,\n\tdefault_DOT_hard_hats.title,\n\tdefault_DOT_hard_hats.birth_date,\n\tdefault_DOT_hard_hats.hire_date,\n\tdefault_DOT_hard_hats.address,\n\tdefault_DOT_hard_hats.city,\n\tdefault_DOT_hard_hats.state,\n\tdefault_DOT_hard_hats.postal_code,\n\tdefault_DOT_hard_hats.country,\n\tdefault_DOT_hard_hats.manager,\n\tdefault_DOT_hard_hats.contractor_id \n FROM roads.hard_hats AS default_DOT_hard_hats) AS default_DOT_hard_hat\n\n", + "columns":[ + { + "name":"hard_hat_id", + "type":"int", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"last_name", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"first_name", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"title", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"birth_date", + "type":"timestamp", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"hire_date", + "type":"timestamp", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"address", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"city", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"state", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"postal_code", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"country", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"manager", + "type":"int", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"contractor_id", + "type":"int", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + } + ], + "upstream_tables":[ + "default.roads.hard_hats" + ] + }, + "schedule":"0 * * * *", + "job":"SparkSqlMaterializationJob", + "backfills":[ + + ], + "strategy":"full", + "output_tables":[ + "common.a", + "common.b" + ], + "urls":[ + "http://fake.url/job" + ] +} diff --git a/datajunction-server/tests/api/files/materializations_test/spark_sql.full.partition.config.json b/datajunction-server/tests/api/files/materializations_test/spark_sql.full.partition.config.json new file mode 100644 index 000000000..0a30f8927 --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/spark_sql.full.partition.config.json @@ -0,0 +1,124 @@ +{ + "backfills":[ + + ], + "config":{ + "columns":[ + { + "column": null, + "name":"hard_hat_id", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"int" + }, + { + "column": null, + "name":"last_name", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"first_name", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"title", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"birth_date", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"timestamp" + }, + { + "column": null, + "name":"hire_date", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"timestamp" + }, + { + "column": null, + "name":"address", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"city", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"state", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"postal_code", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"country", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"manager", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"int" + }, + { + "column": null, + "name":"contractor_id", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"int" + } + ], + "lookback_window": null, + "spark":{ + + }, + "upstream_tables":[ + "default.roads.hard_hats" + ] + }, + "job":"SparkSqlMaterializationJob", + "name":"spark_sql__full__birth_date__country", + "schedule":"0 * * * *", + "strategy":"full" +} diff --git a/datajunction-server/tests/api/files/materializations_test/spark_sql.full.partition.materializations.json b/datajunction-server/tests/api/files/materializations_test/spark_sql.full.partition.materializations.json new file mode 100644 index 000000000..5cd6e22c7 --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/spark_sql.full.partition.materializations.json @@ -0,0 +1,132 @@ +{ + "name":"spark_sql__full__birth_date__country", + "config":{ + "spark":{ + + }, + "lookback_window": null, + "query":"SELECT default_DOT_hard_hat.hard_hat_id,\n\tdefault_DOT_hard_hat.last_name,\n\tdefault_DOT_hard_hat.first_name,\n\tdefault_DOT_hard_hat.title,\n\tdefault_DOT_hard_hat.birth_date,\n\tdefault_DOT_hard_hat.hire_date,\n\tdefault_DOT_hard_hat.address,\n\tdefault_DOT_hard_hat.city,\n\tdefault_DOT_hard_hat.state,\n\tdefault_DOT_hard_hat.postal_code,\n\tdefault_DOT_hard_hat.country,\n\tdefault_DOT_hard_hat.manager,\n\tdefault_DOT_hard_hat.contractor_id \n FROM (SELECT default_DOT_hard_hats.hard_hat_id,\n\tdefault_DOT_hard_hats.last_name,\n\tdefault_DOT_hard_hats.first_name,\n\tdefault_DOT_hard_hats.title,\n\tdefault_DOT_hard_hats.birth_date,\n\tdefault_DOT_hard_hats.hire_date,\n\tdefault_DOT_hard_hats.address,\n\tdefault_DOT_hard_hats.city,\n\tdefault_DOT_hard_hats.state,\n\tdefault_DOT_hard_hats.postal_code,\n\tdefault_DOT_hard_hats.country,\n\tdefault_DOT_hard_hats.manager,\n\tdefault_DOT_hard_hats.contractor_id \n FROM roads.hard_hats AS default_DOT_hard_hats) AS default_DOT_hard_hat\n\n", + "columns":[ + { + "name":"hard_hat_id", + "type":"int", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"last_name", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"first_name", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"title", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"birth_date", + "type":"timestamp", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"hire_date", + "type":"timestamp", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"address", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"city", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"state", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"postal_code", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"country", + "type":"string", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"manager", + "type":"int", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + }, + { + "name":"contractor_id", + "type":"int", + "column": null, + "node": null, + "semantic_entity": null, + "semantic_type": null + } + ], + "upstream_tables":[ + "default.roads.hard_hats" + ] + }, + "schedule":"0 * * * *", + "job":"SparkSqlMaterializationJob", + "backfills":[ + + ], + "strategy":"full", + "output_tables":[ + "common.a", + "common.b" + ], + "urls":[ + "http://fake.url/job" + ] +} diff --git a/datajunction-server/tests/api/files/materializations_test/spark_sql.full.partition.query.sql b/datajunction-server/tests/api/files/materializations_test/spark_sql.full.partition.query.sql new file mode 100644 index 000000000..0482317ab --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/spark_sql.full.partition.query.sql @@ -0,0 +1,31 @@ +WITH +default_DOT_hard_hat AS ( +SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats +) +SELECT default_DOT_hard_hat.hard_hat_id, + default_DOT_hard_hat.last_name, + default_DOT_hard_hat.first_name, + default_DOT_hard_hat.title, + default_DOT_hard_hat.birth_date, + default_DOT_hard_hat.hire_date, + default_DOT_hard_hat.address, + default_DOT_hard_hat.city, + default_DOT_hard_hat.state, + default_DOT_hard_hat.postal_code, + default_DOT_hard_hat.country, + default_DOT_hard_hat.manager, + default_DOT_hard_hat.contractor_id + FROM default_DOT_hard_hat diff --git a/datajunction-server/tests/api/files/materializations_test/spark_sql.full.query.sql b/datajunction-server/tests/api/files/materializations_test/spark_sql.full.query.sql new file mode 100644 index 000000000..0482317ab --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/spark_sql.full.query.sql @@ -0,0 +1,31 @@ +WITH +default_DOT_hard_hat AS ( +SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats +) +SELECT default_DOT_hard_hat.hard_hat_id, + default_DOT_hard_hat.last_name, + default_DOT_hard_hat.first_name, + default_DOT_hard_hat.title, + default_DOT_hard_hat.birth_date, + default_DOT_hard_hat.hire_date, + default_DOT_hard_hat.address, + default_DOT_hard_hat.city, + default_DOT_hard_hat.state, + default_DOT_hard_hat.postal_code, + default_DOT_hard_hat.country, + default_DOT_hard_hat.manager, + default_DOT_hard_hat.contractor_id + FROM default_DOT_hard_hat diff --git a/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.additional.query.sql b/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.additional.query.sql new file mode 100644 index 000000000..4246f5cb1 --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.additional.query.sql @@ -0,0 +1,17 @@ +WITH default_DOT_hard_hat_2 AS ( + SELECT + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.country + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE + DATE_FORMAT(default_DOT_hard_hats.birth_date, 'yyyyMMdd') = + DATE_FORMAT(${dj_logical_timestamp}, 'yyyyMMdd') +) +SELECT + default_DOT_hard_hat_2.last_name, + default_DOT_hard_hat_2.first_name, + default_DOT_hard_hat_2.birth_date, + default_DOT_hard_hat_2.country +FROM default_DOT_hard_hat_2 diff --git a/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.categorical.query.sql b/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.categorical.query.sql new file mode 100644 index 000000000..cbd810e3d --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.categorical.query.sql @@ -0,0 +1,16 @@ +WITH +default_DOT_hard_hat_2 AS ( +SELECT default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.country + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE DATE_FORMAT(default_DOT_hard_hats.birth_date, 'yyyyMMdd') = DATE_FORMAT(${dj_logical_timestamp}, 'yyyyMMdd') +) + +SELECT last_name, + first_name, + birth_date, + country +FROM default_DOT_hard_hat_2 +WHERE birth_date = CAST(DATE_FORMAT(CAST(${dj_logical_timestamp} AS TIMESTAMP), 'yyyyMMdd') AS TIMESTAMP) AND country = CAST(${country} AS STRING) diff --git a/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.config.json b/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.config.json new file mode 100644 index 000000000..5daa90b61 --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.config.json @@ -0,0 +1,126 @@ +[ + { + "backfills":[ + + ], + "config":{ + "columns":[ + { + "column": null, + "name":"hard_hat_id", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"int" + }, + { + "column": null, + "name":"last_name", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"first_name", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"title", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"birth_date", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"timestamp" + }, + { + "column": null, + "name":"hire_date", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"timestamp" + }, + { + "column": null, + "name":"address", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"city", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"state", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"postal_code", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"country", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"string" + }, + { + "column": null, + "name":"manager", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"int" + }, + { + "column": null, + "name":"contractor_id", + "node": null, + "semantic_entity": null, + "semantic_type": null, + "type":"int" + } + ], + "lookback_window": null, + "spark":{ + + }, + "upstream_tables":[ + "default.roads.hard_hats" + ] + }, + "job":"SparkSqlMaterializationJob", + "name":"spark_sql__incremental_time__birth_date", + "schedule":"0 * * * *", + "strategy":"incremental_time" + } +] diff --git a/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.lookback.query.sql b/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.lookback.query.sql new file mode 100644 index 000000000..0a340fd2f --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.lookback.query.sql @@ -0,0 +1,15 @@ +WITH +default_DOT_hard_hat_2 AS ( +SELECT default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.country + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE DATE_FORMAT(default_DOT_hard_hats.birth_date, 'yyyyMMdd') = DATE_FORMAT(${dj_logical_timestamp}, 'yyyyMMdd') +) +SELECT last_name, + first_name, + birth_date, + country + FROM default_DOT_hard_hat_2 + WHERE birth_date BETWEEN CAST(DATE_FORMAT(CAST(${dj_logical_timestamp} AS TIMESTAMP) - INTERVAL 100 DAY , 'yyyyMMdd') AS TIMESTAMP) AND CAST(DATE_FORMAT(CAST(${dj_logical_timestamp} AS TIMESTAMP), 'yyyyMMdd') AS TIMESTAMP) AND country = CAST(${country} AS STRING) diff --git a/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.query.sql b/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.query.sql new file mode 100644 index 000000000..a1ba29923 --- /dev/null +++ b/datajunction-server/tests/api/files/materializations_test/spark_sql.incremental.query.sql @@ -0,0 +1,32 @@ +WITH +default_DOT_hard_hat_2 AS ( +SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats +) +SELECT hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id + FROM default_DOT_hard_hat_2 + WHERE birth_date = CAST(DATE_FORMAT(CAST(${dj_logical_timestamp} AS TIMESTAMP), 'yyyyMMdd') AS TIMESTAMP) diff --git a/datajunction-server/tests/api/graphql/__init__.py b/datajunction-server/tests/api/graphql/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/tests/api/graphql/catalog_test.py b/datajunction-server/tests/api/graphql/catalog_test.py new file mode 100644 index 000000000..5685c126b --- /dev/null +++ b/datajunction-server/tests/api/graphql/catalog_test.py @@ -0,0 +1,71 @@ +""" +Tests for the catalog API. +""" + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_catalog_list( + client: AsyncClient, +) -> None: + """ + Test listing catalogs + """ + response = await client.post( + "/engines/", + json={ + "name": "spark", + "version": "3.3.1", + "dialect": "spark", + }, + ) + + response = await client.post( + "/catalogs/", + json={ + "name": "dev", + "engines": [ + { + "name": "spark", + "version": "3.3.1", + "dialect": "spark", + }, + ], + }, + ) + + response = await client.post( + "/catalogs/", + json={ + "name": "test", + }, + ) + + response = await client.post( + "/catalogs/", + json={ + "name": "prod", + }, + ) + query = """ + { + listCatalogs{ + name + } + } + """ + + response = await client.post("/graphql", json={"query": query}) + assert response.status_code == 200 + assert response.json() == { + "data": { + "listCatalogs": [ + {"name": "unknown"}, + {"name": "dev"}, + {"name": "test"}, + {"name": "prod"}, + ], + }, + } diff --git a/datajunction-server/tests/api/graphql/common_dimensions_test.py b/datajunction-server/tests/api/graphql/common_dimensions_test.py new file mode 100644 index 000000000..4f166e321 --- /dev/null +++ b/datajunction-server/tests/api/graphql/common_dimensions_test.py @@ -0,0 +1,217 @@ +""" +Tests for the common dimensions query. +""" + +from typing import AsyncGenerator + +import pytest +import pytest_asyncio +from httpx import AsyncClient +from sqlalchemy import event +from sqlalchemy.ext.asyncio import AsyncSession + + +@pytest_asyncio.fixture +async def capture_queries( + module__session: AsyncSession, +) -> AsyncGenerator[list[str], None]: + """ + Returns a list of strings, where each string represents a SQL statement + captured during the test. + """ + queries = [] + sync_engine = module__session.bind.sync_engine + + def before_cursor_execute( + _conn, + _cursor, + statement, + _parameters, + _context, + _executemany, + ): + queries.append(statement) + + # Attach event listener to capture queries + event.listen(sync_engine, "before_cursor_execute", before_cursor_execute) + + yield queries + + # Detach event listener after the test + event.remove(sync_engine, "before_cursor_execute", before_cursor_execute) + + +@pytest.mark.asyncio +async def test_get_common_dimensions( + module__client_with_roads: AsyncClient, + capture_queries: AsyncGenerator[ + list[str], + None, + ], +) -> None: + """ + Test getting common dimensions for a set of metrics + """ + + query = """ + { + commonDimensions(nodes: ["default.num_repair_orders", "default.avg_repair_price"]) { + name + type + attribute + properties + role + dimensionNode { + name + } + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert len(data["data"]["commonDimensions"]) == 40 + assert { + "attribute": "company_name", + "dimensionNode": { + "name": "default.dispatcher", + }, + "name": "default.dispatcher.company_name", + "properties": [], + "role": None, + "type": "string", + } in data["data"]["commonDimensions"] + + assert { + "attribute": "dispatcher_id", + "dimensionNode": { + "name": "default.dispatcher", + }, + "name": "default.dispatcher.dispatcher_id", + "properties": [ + "primary_key", + ], + "role": None, + "type": "int", + } in data["data"]["commonDimensions"] + assert len(capture_queries) <= 11 # type: ignore + + +@pytest.mark.asyncio +async def test_get_common_dimensions_with_full_dim_node( + module__client_with_roads: AsyncClient, + capture_queries: AsyncGenerator[ + list[str], + None, + ], +) -> None: + """ + Test getting common dimensions and requesting a full dimension node for each + """ + + query = """ + { + commonDimensions(nodes: ["default.num_repair_orders", "default.avg_repair_price"]) { + name + type + attribute + properties + role + dimensionNode { + name + current { + columns { + name + attributes { + attributeType{ + name + } + } + } + } + tags { + name + } + } + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert len(data["data"]["commonDimensions"]) == 40 + + assert { + "attribute": "state_name", + "dimensionNode": { + "current": { + "columns": [ + { + "attributes": [], + "name": "state_id", + }, + { + "attributes": [], + "name": "state_name", + }, + { + "attributes": [ + { + "attributeType": { + "name": "primary_key", + }, + }, + ], + "name": "state_short", + }, + { + "attributes": [], + "name": "state_region", + }, + ], + }, + "name": "default.us_state", + "tags": [], + }, + "name": "default.us_state.state_name", + "properties": [], + "role": None, + "type": "string", + } in data["data"]["commonDimensions"] + assert len(capture_queries) > 200 # type: ignore + + +@pytest.mark.asyncio +async def test_get_common_dimensions_non_metric_nodes( + module__client_with_roads: AsyncClient, +): + """ + Test getting common dimensions and requesting a full dimension node for each + """ + + query = """ + { + commonDimensions(nodes: ["default.num_repair_orders", "default.repair_order_fact"]) { + name + type + dimensionNode { + name + } + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert len(data["data"]["commonDimensions"]) == 40 + + assert { + "dimensionNode": { + "name": "default.us_state", + }, + "name": "default.us_state.state_name", + "type": "string", + } in data["data"]["commonDimensions"] diff --git a/datajunction-server/tests/api/graphql/engine_test.py b/datajunction-server/tests/api/graphql/engine_test.py new file mode 100644 index 000000000..01801774b --- /dev/null +++ b/datajunction-server/tests/api/graphql/engine_test.py @@ -0,0 +1,64 @@ +""" +Tests for the engine API. +""" + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_engine_list( + client: AsyncClient, +) -> None: + """ + Test listing engines + """ + response = await client.post( + "/engines/", + json={ + "name": "spark", + "version": "2.4.4", + "dialect": "spark", + }, + ) + + response = await client.post( + "/engines/", + json={ + "name": "spark", + "version": "3.3.0", + "dialect": "spark", + }, + ) + + response = await client.post( + "/engines/", + json={ + "name": "spark", + "version": "3.3.1", + "dialect": "spark", + }, + ) + query = """ + { + listEngines{ + name + uri + version + dialect + } + } + """ + + response = await client.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data == { + "data": { + "listEngines": [ + {"name": "spark", "uri": None, "version": "2.4.4", "dialect": "SPARK"}, + {"name": "spark", "uri": None, "version": "3.3.0", "dialect": "SPARK"}, + {"name": "spark", "uri": None, "version": "3.3.1", "dialect": "SPARK"}, + ], + }, + } diff --git a/datajunction-server/tests/api/graphql/find_nodes_test.py b/datajunction-server/tests/api/graphql/find_nodes_test.py new file mode 100644 index 000000000..66fda2613 --- /dev/null +++ b/datajunction-server/tests/api/graphql/find_nodes_test.py @@ -0,0 +1,1005 @@ +""" +Tests for the engine API. +""" + +from unittest import mock + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_find_by_node_type( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test finding nodes by node type + """ + + query = """ + { + findNodes(nodeTypes: [TRANSFORM]) { + name + type + tags { + name + } + currentVersion + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodes"] == [ + { + "currentVersion": "v1.0", + "name": "default.repair_orders_fact", + "tags": [], + "type": "TRANSFORM", + }, + { + "currentVersion": "v1.0", + "name": "default.national_level_agg", + "tags": [], + "type": "TRANSFORM", + }, + { + "currentVersion": "v1.0", + "name": "default.regional_level_agg", + "tags": [], + "type": "TRANSFORM", + }, + ] + + query = """ + { + findNodes(nodeTypes: [CUBE]) { + name + type + tags { + name + } + currentVersion + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data == {"data": {"findNodes": []}} + + +@pytest.mark.asyncio +async def test_find_by_node_type_paginated( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test finding nodes by node type with pagination + """ + query = """ + { + findNodesPaginated(nodeTypes: [TRANSFORM], limit: 2) { + edges { + node { + name + type + tags { + name + } + currentVersion + } + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPrevPage + } + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodesPaginated"] == { + "edges": [ + { + "node": { + "currentVersion": "v1.0", + "name": "default.repair_orders_fact", + "tags": [], + "type": "TRANSFORM", + }, + }, + { + "node": { + "currentVersion": "v1.0", + "name": "default.national_level_agg", + "tags": [], + "type": "TRANSFORM", + }, + }, + ], + "pageInfo": { + "endCursor": mock.ANY, + "hasNextPage": True, + "hasPrevPage": False, + "startCursor": mock.ANY, + }, + } + after = data["data"]["findNodesPaginated"]["pageInfo"]["endCursor"] + query = """ + query ListNodes($after: String) { + findNodesPaginated(nodeTypes: [TRANSFORM], limit: 2, after: $after) { + edges { + node { + name + type + tags { + name + } + currentVersion + } + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPrevPage + } + } + } + """ + response = await module__client_with_roads.post( + "/graphql", + json={"query": query, "variables": {"after": after}}, + ) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodesPaginated"] == { + "edges": [ + { + "node": { + "currentVersion": "v1.0", + "name": "default.regional_level_agg", + "tags": [], + "type": "TRANSFORM", + }, + }, + ], + "pageInfo": { + "endCursor": mock.ANY, + "hasNextPage": False, + "hasPrevPage": True, + "startCursor": mock.ANY, + }, + } + before = data["data"]["findNodesPaginated"]["pageInfo"]["startCursor"] + query = """ + query ListNodes($before: String) { + findNodesPaginated(nodeTypes: [TRANSFORM], limit: 2, before: $before) { + edges { + node { + name + type + tags { + name + } + currentVersion + } + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPrevPage + } + } + } + """ + response = await module__client_with_roads.post( + "/graphql", + json={"query": query, "variables": {"before": before}}, + ) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodesPaginated"] == { + "edges": [ + { + "node": { + "currentVersion": "v1.0", + "name": "default.repair_orders_fact", + "tags": [], + "type": "TRANSFORM", + }, + }, + { + "node": { + "currentVersion": "v1.0", + "name": "default.national_level_agg", + "tags": [], + "type": "TRANSFORM", + }, + }, + ], + "pageInfo": { + "endCursor": mock.ANY, + "hasNextPage": True, + "hasPrevPage": True, + "startCursor": mock.ANY, + }, + } + + +@pytest.mark.asyncio +async def test_find_by_fragment( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test finding nodes by fragment + """ + query = """ + { + findNodes(fragment: "repair_order_dis") { + name + type + current { + columns { + name + type + } + } + currentVersion + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodes"] == [ + { + "current": { + "columns": [ + { + "name": "default_DOT_avg_repair_order_discounts", + "type": "double", + }, + ], + }, + "currentVersion": "v1.0", + "name": "default.avg_repair_order_discounts", + "type": "METRIC", + }, + { + "current": { + "columns": [ + { + "name": "default_DOT_total_repair_order_discounts", + "type": "double", + }, + ], + }, + "currentVersion": "v1.0", + "name": "default.total_repair_order_discounts", + "type": "METRIC", + }, + ] + + +@pytest.mark.asyncio +async def test_find_by_names( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test finding nodes by their names + """ + query = """ + { + findNodes(names: ["default.regional_level_agg", "default.repair_orders"]) { + name + type + current { + columns { + name + type + } + } + currentVersion + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodes"] == [ + { + "current": { + "columns": [ + { + "name": "us_region_id", + "type": "int", + }, + { + "name": "state_name", + "type": "string", + }, + { + "name": "location_hierarchy", + "type": "string", + }, + { + "name": "order_year", + "type": "int", + }, + { + "name": "order_month", + "type": "int", + }, + { + "name": "order_day", + "type": "int", + }, + { + "name": "completed_repairs", + "type": "bigint", + }, + { + "name": "total_repairs_dispatched", + "type": "bigint", + }, + { + "name": "total_amount_in_region", + "type": "double", + }, + { + "name": "avg_repair_amount_in_region", + "type": "double", + }, + { + "name": "avg_dispatch_delay", + "type": "double", + }, + { + "name": "unique_contractors", + "type": "bigint", + }, + ], + }, + "currentVersion": "v1.0", + "name": "default.regional_level_agg", + "type": "TRANSFORM", + }, + { + "current": { + "columns": [ + { + "name": "repair_order_id", + "type": "int", + }, + { + "name": "municipality_id", + "type": "string", + }, + { + "name": "hard_hat_id", + "type": "int", + }, + { + "name": "order_date", + "type": "timestamp", + }, + { + "name": "required_date", + "type": "timestamp", + }, + { + "name": "dispatched_date", + "type": "timestamp", + }, + { + "name": "dispatcher_id", + "type": "int", + }, + ], + }, + "currentVersion": "v1.0", + "name": "default.repair_orders", + "type": "SOURCE", + }, + ] + + +@pytest.mark.asyncio +async def test_find_by_tags( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test finding nodes by tags + """ + + query = """ + { + findNodes(tags: ["random"]) { + name + type + tags { + name + } + currentVersion + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodes"] == [] + + +@pytest.mark.asyncio +async def test_find_source( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test finding source nodes + """ + + query = """ + { + findNodes(names: ["default.repair_type"]) { + name + type + current { + catalog { + name + } + schema_ + table + status + dimensionLinks { + joinSql + joinType + role + dimension { + name + } + } + } + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodes"] == [ + { + "current": { + "catalog": { + "name": "default", + }, + "dimensionLinks": [ + { + "dimension": { + "name": "default.contractor", + }, + "joinSql": "default.repair_type.contractor_id = " + "default.contractor.contractor_id", + "joinType": "INNER", + "role": None, + }, + ], + "schema_": "roads", + "status": "VALID", + "table": "repair_type", + }, + "name": "default.repair_type", + "type": "SOURCE", + }, + ] + + +@pytest.mark.asyncio +async def test_find_transform( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test finding transform nodes + """ + + query = """ + { + findNodes(names: ["default.repair_orders_fact"]) { + name + type + current { + parents { + name + } + materializations { + name + } + availability { + temporalPartitions + minTemporalPartition + maxTemporalPartition + } + cubeMetrics { + name + } + cubeDimensions { + name + } + extractedMeasures { + measures { + name + } + } + metricMetadata { + unit { + name + } + } + } + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodes"] == [ + { + "current": { + "availability": None, + "cubeDimensions": [], + "cubeMetrics": [], + "materializations": [], + "parents": [ + { + "name": "default.repair_orders", + }, + { + "name": "default.repair_order_details", + }, + ], + "extractedMeasures": None, + "metricMetadata": None, + }, + "name": "default.repair_orders_fact", + "type": "TRANSFORM", + }, + ] + + +@pytest.mark.asyncio +async def test_find_metric( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test finding metrics + """ + + query = """ + { + findNodes(names: ["default.regional_repair_efficiency"]) { + name + type + current { + parents { + name + } + metricMetadata { + unit { + name + } + direction + expression + incompatibleDruidFunctions + } + requiredDimensions { + name + } + extractedMeasures { + measures { + name + expression + aggregation + rule { + type + } + } + derivedQuery + derivedExpression + } + } + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodes"] == [ + { + "current": { + "metricMetadata": { + "direction": None, + "unit": None, + "expression": ( + "(SUM(rm.completed_repairs) * 1.0 / SUM(rm.total_repairs_dispatched)) * " + "(SUM(rm.total_amount_in_region) * 1.0 / " + "SUM(na.total_amount_nationwide)) * 100" + ), + "incompatibleDruidFunctions": [], + }, + "parents": [ + { + "name": "default.regional_level_agg", + }, + { + "name": "default.national_level_agg", + }, + ], + "requiredDimensions": [], + "extractedMeasures": { + "measures": [ + { + "aggregation": "SUM", + "expression": "completed_repairs", + "name": "completed_repairs_sum_81105666", + "rule": { + "type": "FULL", + }, + }, + { + "aggregation": "SUM", + "expression": "total_repairs_dispatched", + "name": "total_repairs_dispatched_sum_01dc2341", + "rule": { + "type": "FULL", + }, + }, + { + "aggregation": "SUM", + "expression": "total_amount_in_region", + "name": "total_amount_in_region_sum_1c94ab45", + "rule": { + "type": "FULL", + }, + }, + { + "aggregation": "SUM", + "expression": "na.total_amount_nationwide", + "name": "na.total_amount_nationwide_sum_fed946fe", + "rule": { + "type": "FULL", + }, + }, + ], + "derivedQuery": "SELECT (SUM(completed_repairs_sum_81105666) * 1.0 / " + "SUM(total_repairs_dispatched_sum_01dc2341)) * " + "(SUM(total_amount_in_region_sum_1c94ab45) * 1.0 / " + "SUM(na.total_amount_nationwide_sum_fed946fe)) * 100 \n" + " FROM default.regional_level_agg CROSS JOIN " + "default.national_level_agg na\n" + "\n", + "derivedExpression": "(SUM(completed_repairs_sum_81105666) * 1.0 / " + "SUM(total_repairs_dispatched_sum_01dc2341)) * " + "(SUM(total_amount_in_region_sum_1c94ab45) * 1.0 / " + "SUM(na.total_amount_nationwide_sum_fed946fe)) * 100", + }, + }, + "name": "default.regional_repair_efficiency", + "type": "METRIC", + }, + ] + + +@pytest.mark.asyncio +async def test_find_cubes( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test finding cubes + """ + response = await module__client_with_roads.post( + "/nodes/cube/", + json={ + "metrics": [ + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + ], + "dimensions": [ + "default.hard_hat.city", + "default.hard_hat.state", + "default.dispatcher.company_name", + ], + "filters": ["default.hard_hat.state='AZ'"], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.repairs_cube", + }, + ) + query = """ + { + findNodes(nodeTypes: [CUBE]) { + name + type + current { + cubeMetrics { + name + description + } + cubeDimensions { + name + dimensionNode { + name + } + } + } + } + } + """ + + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodes"] == [ + { + "current": { + "cubeDimensions": [ + { + "dimensionNode": { + "name": "default.hard_hat", + }, + "name": "default.hard_hat.city", + }, + { + "dimensionNode": { + "name": "default.hard_hat", + }, + "name": "default.hard_hat.state", + }, + { + "dimensionNode": { + "name": "default.dispatcher", + }, + "name": "default.dispatcher.company_name", + }, + ], + "cubeMetrics": [ + { + "description": "Number of repair orders", + "name": "default.num_repair_orders", + }, + { + "description": "Average repair price", + "name": "default.avg_repair_price", + }, + { + "description": "Total repair cost", + "name": "default.total_repair_cost", + }, + ], + }, + "name": "default.repairs_cube", + "type": "CUBE", + }, + ] + + +@pytest.mark.asyncio +async def test_find_node_with_revisions( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test finding nodes with revisions + """ + + query = """ + { + findNodesPaginated(nodeTypes: [TRANSFORM], namespace: "default", editedBy: "dj", limit: -1) { + edges { + node { + name + type + revisions { + displayName + dimensionLinks { + dimension { + name + } + joinSql + } + } + currentVersion + createdBy { + email + id + isAdmin + name + oauthProvider + username + } + } + } + } + } + """ + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + results = data["data"]["findNodesPaginated"] + results["edges"][0]["node"]["revisions"][0]["dimensionLinks"] = sorted( + results["edges"][0]["node"]["revisions"][0]["dimensionLinks"], + key=lambda x: x["dimension"]["name"], + ) + assert results["edges"] == [ + { + "node": { + "name": "default.repair_orders_fact", + "type": "TRANSFORM", + "revisions": [ + { + "displayName": "Repair Orders Fact", + "dimensionLinks": [ + { + "dimension": { + "name": "default.dispatcher", + }, + "joinSql": "default.repair_orders_fact.dispatcher_id = " + "default.dispatcher.dispatcher_id", + }, + { + "dimension": { + "name": "default.hard_hat", + }, + "joinSql": "default.repair_orders_fact.hard_hat_id = " + "default.hard_hat.hard_hat_id", + }, + { + "dimension": {"name": "default.hard_hat_to_delete"}, + "joinSql": "default.repair_orders_fact.hard_hat_id = default.hard_hat_to_delete.hard_hat_id", + }, + { + "dimension": { + "name": "default.municipality_dim", + }, + "joinSql": "default.repair_orders_fact.municipality_id = " + "default.municipality_dim.municipality_id", + }, + ], + }, + ], + "currentVersion": "v1.0", + "createdBy": { + "email": None, + "id": 1, + "isAdmin": False, + "name": None, + "oauthProvider": "BASIC", + "username": "dj", + }, + }, + }, + { + "node": { + "name": "default.national_level_agg", + "type": "TRANSFORM", + "revisions": [ + { + "displayName": "National Level Agg", + "dimensionLinks": [], + }, + ], + "currentVersion": "v1.0", + "createdBy": { + "email": None, + "id": 1, + "isAdmin": False, + "name": None, + "oauthProvider": "BASIC", + "username": "dj", + }, + }, + }, + { + "node": { + "name": "default.regional_level_agg", + "type": "TRANSFORM", + "revisions": [ + { + "displayName": "Regional Level Agg", + "dimensionLinks": [], + }, + ], + "currentVersion": "v1.0", + "createdBy": { + "email": None, + "id": 1, + "isAdmin": False, + "name": None, + "oauthProvider": "BASIC", + "username": "dj", + }, + }, + }, + ] + + +@pytest.mark.asyncio +async def test_find_nodes_with_created_edited_by( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test finding nodes with created by / edited by metadata + """ + + query = """ + { + findNodes(names: ["default.repair_orders_fact"]) { + name + createdBy { + username + } + editedBy + } + } + """ + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodes"] == [ + { + "name": "default.repair_orders_fact", + "createdBy": {"username": "dj"}, + "editedBy": ["dj"], + }, + ] + + +@pytest.mark.asyncio +async def test_find_nodes_paginated_empty_list( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test finding nodes with pagination when there are none + """ + + query = """ + { + findNodesPaginated(names: ["default.repair_orders_fact111"], before: "eyJjcmVhdGVkX2F0IjogIjIwMjQtMTAtMjZUMTQ6Mzc6MjkuNzI4MzE3KzAwOjAwIiwgImlkIjogMjV9") { + edges { + node { + name + } + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPrevPage + } + } + } + """ + response = await module__client_with_roads.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data["data"]["findNodesPaginated"] == { + "edges": [], + "pageInfo": { + "startCursor": None, + "endCursor": None, + "hasNextPage": False, + "hasPrevPage": False, + }, + } diff --git a/datajunction-server/tests/api/graphql/measures_sql_test.py b/datajunction-server/tests/api/graphql/measures_sql_test.py new file mode 100644 index 000000000..e3f8ea613 --- /dev/null +++ b/datajunction-server/tests/api/graphql/measures_sql_test.py @@ -0,0 +1,113 @@ +""" +Tests for generate SQL queries +""" + +from unittest import mock + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_measures_sql( + module__client_with_roads: AsyncClient, +): + """ + Test requesting measures SQL for a set of metrics, dimensions, and filters + """ + + query = """ + query GetMeasuresSQL($metrics: [String!], $dimensions: [String!], $filters: [String!]) { + measuresSql( + cube: {metrics: $metrics, dimensions: $dimensions, filters: $filters} + preaggregate: true + ) { + sql + node { + name + } + columns { + name + semanticType + semanticEntity { + name + node + column + } + } + dialect + upstreamTables + errors { + message + } + } + } + """ + + response = await module__client_with_roads.post( + "/graphql", + json={ + "query": query, + "variables": { + "metrics": ["default.num_repair_orders", "default.avg_repair_price"], + "dimensions": ["default.us_state.state_name"], + "filters": ["default.us_state.state_name = 'AZ'"], + }, + }, + ) + assert response.status_code == 200 + data = response.json() + assert len(data["data"]["measuresSql"]) == 1 + + assert data["data"]["measuresSql"][0] == { + "columns": [ + { + "name": "default_DOT_us_state_DOT_state_name", + "semanticEntity": { + "column": "state_name", + "name": "default.us_state.state_name", + "node": "default.us_state", + }, + "semanticType": "DIMENSION", + }, + { + "name": "repair_order_id_count_0b7dfba0", + "semanticEntity": { + "column": "repair_order_id_count_0b7dfba0", + "name": "default.repair_orders_fact.repair_order_id_count_0b7dfba0", + "node": "default.repair_orders_fact", + }, + "semanticType": "MEASURE", + }, + { + "name": "price_count_78a5eb43", + "semanticEntity": { + "column": "price_count_78a5eb43", + "name": "default.repair_orders_fact.price_count_78a5eb43", + "node": "default.repair_orders_fact", + }, + "semanticType": "MEASURE", + }, + { + "name": "price_sum_78a5eb43", + "semanticEntity": { + "column": "price_sum_78a5eb43", + "name": "default.repair_orders_fact.price_sum_78a5eb43", + "node": "default.repair_orders_fact", + }, + "semanticType": "MEASURE", + }, + ], + "dialect": "SPARK", + "errors": [], + "node": { + "name": "default.repair_orders_fact", + }, + "sql": mock.ANY, + "upstreamTables": [ + "default.roads.repair_orders", + "default.roads.repair_order_details", + "default.roads.hard_hats", + "default.roads.us_states", + ], + } diff --git a/datajunction-server/tests/api/graphql/tags_test.py b/datajunction-server/tests/api/graphql/tags_test.py new file mode 100644 index 000000000..faca710c0 --- /dev/null +++ b/datajunction-server/tests/api/graphql/tags_test.py @@ -0,0 +1,313 @@ +""" +Tests for tags GQL queries. +""" + +import pytest +import pytest_asyncio +from httpx import AsyncClient + + +@pytest_asyncio.fixture(scope="module") +async def client_with_tags( + module__client_with_roads: AsyncClient, +) -> AsyncClient: + """ + Provides a DJ client fixture seeded with tags + """ + await module__client_with_roads.post( + "/tags/", + json={ + "name": "sales_report", + "display_name": "Sales Report", + "description": "All metrics for sales", + "tag_type": "report", + "tag_metadata": {}, + }, + ) + await module__client_with_roads.post( + "/tags/", + json={ + "name": "other_report", + "display_name": "Other Report", + "description": "Random", + "tag_type": "report", + "tag_metadata": {}, + }, + ) + await module__client_with_roads.post( + "/tags/", + json={ + "name": "coffee", + "display_name": "Coffee", + "description": "A drink", + "tag_type": "drinks", + "tag_metadata": {}, + }, + ) + await module__client_with_roads.post( + "/tags/", + json={ + "name": "tea", + "display_name": "Tea", + "description": "Another drink", + "tag_type": "drinks", + "tag_metadata": {}, + }, + ) + + await module__client_with_roads.post( + "/nodes/default.total_repair_cost/tags/?tag_names=sales_report", + ) + await module__client_with_roads.post( + "/nodes/default.avg_repair_price/tags/?tag_names=sales_report", + ) + await module__client_with_roads.post( + "/nodes/default.num_repair_orders/tags/?tag_names=other_report", + ) + return module__client_with_roads + + +@pytest.mark.asyncio +async def test_list_tags( + client_with_tags: AsyncClient, +) -> None: + """ + Test listing tags + """ + query = """ + { + listTags { + name + description + displayName + tagType + tagMetadata + } + } + """ + response = await client_with_tags.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data == { + "data": { + "listTags": [ + { + "description": "All metrics for sales", + "displayName": "Sales Report", + "name": "sales_report", + "tagMetadata": {}, + "tagType": "report", + }, + { + "description": "Random", + "displayName": "Other Report", + "name": "other_report", + "tagMetadata": {}, + "tagType": "report", + }, + { + "description": "A drink", + "displayName": "Coffee", + "name": "coffee", + "tagMetadata": {}, + "tagType": "drinks", + }, + { + "description": "Another drink", + "displayName": "Tea", + "name": "tea", + "tagMetadata": {}, + "tagType": "drinks", + }, + ], + }, + } + + +@pytest.mark.asyncio +async def test_list_tag_types( + client_with_tags: AsyncClient, +) -> None: + """ + Test listing tag types + """ + query = """ + { + listTagTypes + } + """ + response = await client_with_tags.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data == {"data": {"listTagTypes": ["report", "drinks"]}} + + +@pytest.mark.asyncio +async def test_find_tags_by_type( + client_with_tags: AsyncClient, +) -> None: + """ + Test finding tags by tag type + """ + query = """ + { + listTags(tagTypes: ["drinks"]) { + name + } + } + """ + response = await client_with_tags.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data == {"data": {"listTags": [{"name": "coffee"}, {"name": "tea"}]}} + + query = """ + { + listTags(tagTypes: ["report"]) { + name + } + } + """ + response = await client_with_tags.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data == { + "data": {"listTags": [{"name": "sales_report"}, {"name": "other_report"}]}, + } + + query = """ + { + listTags(tagTypes: ["random"]) { + name + } + } + """ + response = await client_with_tags.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data == {"data": {"listTags": []}} + + +@pytest.mark.asyncio +async def test_find_tags_by_name( + client_with_tags: AsyncClient, +) -> None: + """ + Test finding tags by tag type + """ + query = """ + { + listTags(tagNames: ["coffee", "tea"]) { + name + } + } + """ + response = await client_with_tags.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data == {"data": {"listTags": [{"name": "coffee"}, {"name": "tea"}]}} + + +@pytest.mark.asyncio +async def test_tag_get_nodes( + client_with_tags: AsyncClient, +) -> None: + """ + Test listing tags + """ + query = """ + { + listTags (tagNames: ["sales_report"]) { + name + nodes { + name + current { + columns { + name + } + } + } + } + } + """ + response = await client_with_tags.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data == { + "data": { + "listTags": [ + { + "name": "sales_report", + "nodes": [ + { + "current": { + "columns": [ + { + "name": "default_DOT_avg_repair_price", + }, + ], + }, + "name": "default.avg_repair_price", + }, + { + "current": { + "columns": [ + { + "name": "default_DOT_total_repair_cost", + }, + ], + }, + "name": "default.total_repair_cost", + }, + ], + }, + ], + }, + } + + query = """ + { + listTags { + name + nodes { + name + } + } + } + """ + response = await client_with_tags.post("/graphql", json={"query": query}) + assert response.status_code == 200 + data = response.json() + assert data == { + "data": { + "listTags": [ + { + "name": "sales_report", + "nodes": [ + { + "name": "default.avg_repair_price", + }, + { + "name": "default.total_repair_cost", + }, + ], + }, + { + "name": "other_report", + "nodes": [ + { + "name": "default.num_repair_orders", + }, + ], + }, + { + "name": "coffee", + "nodes": [], + }, + { + "name": "tea", + "nodes": [], + }, + ], + }, + } diff --git a/datajunction-server/tests/api/health_test.py b/datajunction-server/tests/api/health_test.py new file mode 100644 index 000000000..bbeaad38f --- /dev/null +++ b/datajunction-server/tests/api/health_test.py @@ -0,0 +1,36 @@ +""" +Tests for the healthcheck API. +""" + +import asyncio + +import pytest +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession + + +@pytest.mark.asyncio +async def test_successful_health(module__client: AsyncClient) -> None: + """ + Test ``GET /health/``. + """ + response = await module__client.get("/health/") + data = response.json() + assert data == [{"name": "database", "status": "ok"}] + + +@pytest.mark.asyncio +async def test_failed_health( + session: AsyncSession, + client: AsyncClient, + mocker, +) -> None: + """ + Test failed healthcheck. + """ + future: asyncio.Future = asyncio.Future() + future.set_result(mocker.MagicMock()) + session.execute = mocker.MagicMock(return_value=future) + response = await client.get("/health/") + data = response.json() + assert data == [{"name": "database", "status": "failed"}] diff --git a/datajunction-server/tests/api/helpers_test.py b/datajunction-server/tests/api/helpers_test.py new file mode 100644 index 000000000..f556475cc --- /dev/null +++ b/datajunction-server/tests/api/helpers_test.py @@ -0,0 +1,167 @@ +""" +Tests for API helpers. +""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.api import helpers +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.user import OAuthProvider, User +from datajunction_server.errors import DJDoesNotExistException, DJException +from datajunction_server.internal.nodes import propagate_valid_status +from datajunction_server.models.node import NodeStatus + + +@pytest.mark.asyncio +async def test_raise_get_node_when_node_does_not_exist(module__session: AsyncSession): + """ + Test raising when a node doesn't exist + """ + with pytest.raises(DJException) as exc_info: + await helpers.get_node_by_name( + session=module__session, + name="foo", + raise_if_not_exists=True, + ) + + assert "A node with name `foo` does not exist." in str(exc_info.value) + with pytest.raises(DJException) as exc_info: + await Node.get_by_name( + session=module__session, + name="foo", + raise_if_not_exists=True, + ) + + assert "A node with name `foo` does not exist." in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_propagate_valid_status(module__session: AsyncSession): + """ + Test raising when trying to propagate a valid status using an invalid node + """ + invalid_node = NodeRevision( + name="foo", + status=NodeStatus.INVALID, + ) + example_user = User( + id=1, + username="userfoo", + password="passwordfoo", + name="djuser", + email="userfoo@datajunction.io", + oauth_provider=OAuthProvider.BASIC, + ) + with pytest.raises(DJException) as exc_info: + await propagate_valid_status( + session=module__session, + valid_nodes=[invalid_node], + catalog_id=1, + current_user=example_user, + ) + + assert "Cannot propagate valid status: Node `foo` is not valid" in str( + exc_info.value, + ) + + +@pytest.mark.asyncio +async def test_get_node_namespace(): + """ + Test getting a node's namespace + """ + # success + mock_execute = AsyncMock(scalar_one_or_none=MagicMock(return_value="bar")) + mock_session = AsyncMock(execute=AsyncMock(return_value=mock_execute)) + namespace = await helpers.get_node_namespace( + session=mock_session, + namespace="foo", + raise_if_not_exists=True, + ) + assert namespace == "bar" + # error + mock_execute = AsyncMock(scalar_one_or_none=MagicMock(return_value=None)) + mock_session = AsyncMock(execute=AsyncMock(return_value=mock_execute)) + with pytest.raises(DJDoesNotExistException) as exc_info: + namespace = await helpers.get_node_namespace( + session=mock_session, + namespace="foo", + raise_if_not_exists=True, + ) + assert "node namespace `foo` does not exist" in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_find_existing_cube(): + """ + Test finding an existing cube + """ + mock_cube = MagicMock(current="foo-cube") + mock_execute = AsyncMock( + unique=MagicMock( + return_value=MagicMock( + scalars=MagicMock( + return_value=MagicMock(all=MagicMock(return_value=[mock_cube])), + ), + ), + ), + ) + mock_session = AsyncMock(execute=AsyncMock(return_value=mock_execute)) + node = await helpers.find_existing_cube( + session=mock_session, + metric_columns=[], + dimension_columns=[], + materialized=False, + ) + assert node == "foo-cube" + + +@pytest.mark.asyncio +@patch("datajunction_server.api.helpers.ColumnMetadata", MagicMock) +@patch("datajunction_server.api.helpers.validate_cube") +@patch("datajunction_server.api.helpers.Node.get_by_name") +@patch("datajunction_server.api.helpers.find_existing_cube") +@patch("datajunction_server.api.helpers.get_catalog_by_name") +@patch("datajunction_server.api.helpers.build_materialized_cube_node") +@patch("datajunction_server.api.helpers.TranslatedSQL", MagicMock) +async def test_build_sql_for_multiple_metrics( + mock_build_materialized_cube_node, + mock_get_catalog_by_name, + mock_find_existing_cube, + mock_get_by_name, + mock_validate_cube, +): + """ + Test building SQL for multiple metrics + """ + mock_build_materialized_cube_node.return_value = MagicMock() + mock_engines = [MagicMock(name="eng1"), MagicMock(name="eng2")] + mock_get_catalog_by_name.return_value = MagicMock(engines=mock_engines) + mock_find_existing_cube.return_value = MagicMock( + availability=MagicMock(catalog="cata-foo"), + ) + mock_get_by_name.return_value = MagicMock( + current=MagicMock(catalog=MagicMock(name="cata-foo", engines=mock_engines)), + ) + mock_metric_columns = [MagicMock(name="col1"), MagicMock(name="col2")] + mock_metric_nodes = ["mnode1", "mnode2"] + dimension_columns = [MagicMock(name="dim1"), MagicMock(name="dim2")] + _ = MagicMock() + mock_validate_cube.return_value = ( + mock_metric_columns, + mock_metric_nodes, + _, + dimension_columns, + _, + ) + mock_session = AsyncMock() + + sql = await helpers.build_sql_for_multiple_metrics( + session=mock_session, + metrics=["m1", "m2"], + dimensions=[], + ) + assert sql is not None diff --git a/datajunction-server/tests/api/history_test.py b/datajunction-server/tests/api/history_test.py new file mode 100644 index 000000000..732092ea9 --- /dev/null +++ b/datajunction-server/tests/api/history_test.py @@ -0,0 +1,201 @@ +""" +Tests for the history endpoint +""" + +from unittest import mock + +import pytest +from httpx import AsyncClient + +from datajunction_server.database.history import ActivityType, EntityType, History + + +def test_history_hash(): + """ + Test hash comparison of history events + """ + foo1 = History( + id=1, + entity_name="bar", + entity_type=EntityType.NODE, + activity_type=ActivityType.CREATE, + ) + foo2 = History( + id=1, + entity_name="bar", + entity_type=EntityType.NODE, + activity_type=ActivityType.CREATE, + ) + assert hash(foo1) == hash(foo2) + + +@pytest.mark.asyncio +async def test_get_history_entity(module__client_with_roads: AsyncClient): + """ + Test getting history for an entity + """ + response = await module__client_with_roads.get( + "/history/node/default.repair_orders/", + ) + assert response.status_code in (200, 201) + history = response.json() + assert len(history) == 1 + entity = history[0] + entity.pop("created_at") + assert history == [ + { + "id": mock.ANY, + "pre": {}, + "post": {}, + "node": "default.repair_orders", + "entity_type": "node", + "entity_name": "default.repair_orders", + "activity_type": "create", + "user": "dj", + "details": {}, + }, + ] + + +@pytest.mark.asyncio +async def test_get_history_node(module__client_with_roads: AsyncClient): + """ + Test getting history for a node + """ + + response = await module__client_with_roads.get("/history?node=default.repair_order") + assert response.status_code in (200, 201) + history = response.json() + assert len(history) == 6 + assert history == [ + { + "activity_type": "create", + "node": "default.repair_order", + "created_at": mock.ANY, + "details": { + "dimension": "default.municipality_dim", + "join_cardinality": "many_to_one", + "join_sql": "default.repair_order.municipality_id = " + "default.municipality_dim.municipality_id", + "role": None, + }, + "entity_name": "default.repair_order", + "entity_type": "link", + "id": mock.ANY, + "post": {}, + "pre": {}, + "user": "dj", + }, + { + "activity_type": "create", + "node": "default.repair_order", + "created_at": mock.ANY, + "details": { + "dimension": "default.hard_hat_to_delete", + "join_cardinality": "many_to_one", + "join_sql": "default.repair_order.hard_hat_id = " + "default.hard_hat_to_delete.hard_hat_id", + "role": None, + }, + "entity_name": "default.repair_order", + "entity_type": "link", + "id": mock.ANY, + "post": {}, + "pre": {}, + "user": "dj", + }, + { + "activity_type": "create", + "node": "default.repair_order", + "created_at": mock.ANY, + "details": { + "dimension": "default.hard_hat", + "join_cardinality": "many_to_one", + "join_sql": "default.repair_order.hard_hat_id = " + "default.hard_hat.hard_hat_id", + "role": None, + }, + "entity_name": "default.repair_order", + "entity_type": "link", + "id": mock.ANY, + "post": {}, + "pre": {}, + "user": "dj", + }, + { + "activity_type": "create", + "node": "default.repair_order", + "created_at": mock.ANY, + "details": { + "dimension": "default.dispatcher", + "join_cardinality": "many_to_one", + "join_sql": "default.repair_order.dispatcher_id = " + "default.dispatcher.dispatcher_id", + "role": None, + }, + "entity_name": "default.repair_order", + "entity_type": "link", + "id": mock.ANY, + "post": {}, + "pre": {}, + "user": "dj", + }, + { + "activity_type": "set_attribute", + "node": "default.repair_order", + "created_at": mock.ANY, + "details": { + "column": "repair_order_id", + "attributes": [ + { + "name": "primary_key", + "namespace": "system", + }, + ], + }, + "entity_name": None, + "entity_type": "column_attribute", + "id": mock.ANY, + "post": {}, + "pre": {}, + "user": "dj", + }, + { + "activity_type": "create", + "node": "default.repair_order", + "created_at": mock.ANY, + "details": {}, + "entity_name": "default.repair_order", + "entity_type": "node", + "id": mock.ANY, + "post": {}, + "pre": {}, + "user": "dj", + }, + ] + + +@pytest.mark.asyncio +async def test_get_history_namespace(client_with_service_setup: AsyncClient): + """ + Test getting history for a node context + """ + + response = await client_with_service_setup.get("/history/namespace/default") + assert response.status_code in (200, 201) + history = response.json() + assert len(history) == 1 + assert history == [ + { + "activity_type": "create", + "node": None, + "created_at": mock.ANY, + "details": {}, + "entity_name": "default", + "entity_type": "namespace", + "id": mock.ANY, + "post": {}, + "pre": {}, + "user": "dj", + }, + ] diff --git a/datajunction-server/tests/api/materializations_test.py b/datajunction-server/tests/api/materializations_test.py new file mode 100644 index 000000000..eb8226b4f --- /dev/null +++ b/datajunction-server/tests/api/materializations_test.py @@ -0,0 +1,1626 @@ +"""Tests for /materialization api""" + +import json +import os +from pathlib import Path +from unittest import mock + +import pytest +import pytest_asyncio +from httpx import AsyncClient + +from datajunction_server.models.cube_materialization import ( + Aggregability, + AggregationRule, + CubeMetric, + Measure, + MeasureKey, + NodeNameVersion, +) +from datajunction_server.models.partition import Granularity, PartitionBackfill +from datajunction_server.models.query import ColumnMetadata +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.sql.parsing.backends.antlr4 import parse + +TEST_DIR = os.path.dirname(os.path.abspath(__file__)) + + +@pytest.fixture +def load_expected_file(): + """ + Loads expected fixture file + """ + + def _load(filename: str): + expected_path = TEST_DIR / Path("files/materializations_test") + with open(expected_path / filename, encoding="utf-8") as fe: + if filename.endswith(".json"): + return json.loads(fe.read().strip()) + return fe.read().strip() + + return _load + + +@pytest_asyncio.fixture +async def client_with_repairs_cube( + module__client_with_roads: AsyncClient, +): + """ + Adds a repairs cube to the test client + """ + + async def _client_with_repairs_cube(cube_name: str = "default.repairs_cube"): + response = await module__client_with_roads.post( + "/nodes/default.repair_orders_fact/columns/order_date/attributes/", + json=[{"name": "dimension"}], + ) + assert response.status_code in (200, 201) + response = await module__client_with_roads.post( + "/nodes/cube/", + json={ + "metrics": [ + "default.num_repair_orders", + "default.total_repair_cost", + ], + "dimensions": [ + "default.repair_orders_fact.order_date", + "default.hard_hat.state", + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + ], + "filters": ["default.hard_hat.state='AZ'"], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": f"{cube_name}", + }, + ) + assert response.status_code == 201 + assert response.json()["version"] == "v1.0" + return module__client_with_roads + + return _client_with_repairs_cube + + +@pytest.fixture +def set_temporal_column(): + """ + Sets the given column as a temporal partition on the specified node. + """ + + async def _set_temporal_column(client: AsyncClient, node_name: str, column: str): + response = await client.post( + f"/nodes/{node_name}/columns/{column}/partition", + json={ + "type_": "temporal", + "granularity": "day", + "format": "yyyyMMdd", + }, + ) + assert response.status_code in (200, 201) + + return _set_temporal_column + + +@pytest.fixture +def set_categorical_partition(): + """ + Sets the given column as a categorical partition on the specified node. + """ + + async def _set_categorical_partition( + client: AsyncClient, + node_name: str, + column: str, + ): + response = await client.post( + f"/nodes/{node_name}/columns/{column}/partition", + json={ + "type_": "categorical", + }, + ) + assert response.status_code in (200, 201) + + return _set_categorical_partition + + +@pytest.mark.asyncio +async def test_materialization_info(module__client: AsyncClient) -> None: + """ + Test ``GET /materialization/info``. + """ + response = await module__client.get("/materialization/info") + data = response.json() + + assert response.status_code == 200 + assert data == { + "job_types": [ + { + "allowed_node_types": ["transform", "dimension", "cube"], + "description": "Spark SQL materialization job", + "job_class": "SparkSqlMaterializationJob", + "label": "Spark SQL", + "name": "spark_sql", + }, + { + "allowed_node_types": ["cube"], + "description": "Used to materialize a cube's measures to Druid for " + "low-latency access to a set of metrics and " + "dimensions. While the logical cube definition " + "is at the level of metrics and dimensions, this " + "materialized Druid cube will contain " + "measures and dimensions, with rollup " + "configured on the measures where appropriate.", + "job_class": "DruidMeasuresCubeMaterializationJob", + "label": "Druid Measures Cube (Pre-Agg Cube)", + "name": "druid_measures_cube", + }, + { + "allowed_node_types": ["cube"], + "description": "Used to materialize a cube of metrics and " + "dimensions to Druid for low-latency access. " + "The materialized cube is at the metric level, " + "meaning that all metrics will be aggregated to " + "the level of the cube's dimensions.", + "job_class": "DruidMetricsCubeMaterializationJob", + "label": "Druid Metrics Cube (Post-Agg Cube)", + "name": "druid_metrics_cube", + }, + { + "allowed_node_types": [ + "cube", + ], + "description": "Used to materialize a cube of metrics and dimensions to Druid for " + "low-latency access.Will replace the other cube materialization " + "types.", + "job_class": "DruidCubeMaterializationJob", + "label": "Druid Cube", + "name": "druid_cube", + }, + ], + "strategies": [ + {"label": "Full", "name": "full"}, + {"label": "Snapshot", "name": "snapshot"}, + {"label": "Snapshot Partition", "name": "snapshot_partition"}, + {"label": "Incremental Time", "name": "incremental_time"}, + {"label": "View", "name": "view"}, + ], + } + + +@pytest.mark.asyncio +async def test_crud_materialization(module__client_with_basic: AsyncClient): + """ + Verifies the CRUD endpoints for adding/updating/deleting materialization and backfill + """ + client_with_query_service = module__client_with_basic + # Create the engine and check the existing transform node + await client_with_query_service.post( + "/engines/", + json={ + "name": "spark", + "version": "2.4.4", + "dialect": "spark", + }, + ) + + response = await client_with_query_service.get( + "/nodes/basic.transform.country_agg/", + ) + old_node_data = response.json() + assert old_node_data["version"] == "v1.0" + assert old_node_data["materializations"] == [] + + # Setting the materialization config should succeed + response = await client_with_query_service.post( + "/nodes/basic.transform.country_agg/materialization/", + json={ + "job": "spark_sql", + "strategy": "full", + "config": {}, + "schedule": "0 * * * *", + }, + ) + data = response.json() + assert ( + data["message"] == "Successfully updated materialization config named " + "`spark_sql__full` for node `basic.transform.country_agg`" + ) + + # Check history of the node with materialization + response = await client_with_query_service.get( + "/history?node=basic.transform.country_agg", + ) + history = response.json() + assert [ + (activity["activity_type"], activity["entity_type"]) for activity in history + ] == [("create", "materialization"), ("create", "node")] + + # Setting it again should inform that it already exists + response = await client_with_query_service.post( + "/nodes/basic.transform.country_agg/materialization/", + json={ + "job": "spark_sql", + "strategy": "full", + "config": {}, + "schedule": "0 * * * *", + }, + ) + assert response.json() == { + "info": { + "output_tables": ["common.a", "common.b"], + "urls": ["http://fake.url/job"], + }, + "message": "The same materialization config with name " + "`spark_sql__full` already exists for node " + "`basic.transform.country_agg` so no update was performed.", + } + + # Deactivating it should work + response = await client_with_query_service.delete( + "/nodes/basic.transform.country_agg/materializations/" + "?materialization_name=spark_sql__full", + ) + assert response.json() == { + "message": "The materialization named `spark_sql__full` on node " + "`basic.transform.country_agg` has been successfully deactivated", + } + + # Setting it again should inform that it already exists but was reactivated + response = await client_with_query_service.post( + "/nodes/basic.transform.country_agg/materialization/", + json={ + "job": "spark_sql", + "strategy": "full", + "config": {}, + "schedule": "0 * * * *", + }, + ) + assert response.json()["message"] == ( + "The same materialization config with name `spark_sql__full` already " + "exists for node `basic.transform.country_agg` but was deactivated. It has " + "now been restored." + ) + response = await client_with_query_service.get( + "/history?node=basic.transform.country_agg", + ) + assert [ + ( + activity["activity_type"], + activity["entity_type"], + activity["entity_name"], + ) + for activity in response.json() + ] == [ + ("restore", "materialization", "spark_sql__full"), + ("delete", "materialization", "spark_sql__full"), + ("create", "materialization", "spark_sql__full"), + ("create", "node", "basic.transform.country_agg"), + ] + + +@pytest.mark.asyncio +async def test_druid_measures_cube_full( + client_with_repairs_cube: AsyncClient, + module__query_service_client: QueryServiceClient, + load_expected_file, + set_temporal_column, +): + """ + Verifying this materialization setup: + - Job Type: druid_measures_cube + - Strategy: full + Cases to check: + - [success] When there is a column on the cube with the partition label + - [failure] When there are no columns on the cube with type `timestamp` and no partition labels + - [failure] If nothing has changed, will not update the existing materialization + """ + client_with_repairs_cube = await client_with_repairs_cube() + # [success] When there is a column on the cube with a temporal partition label: + await set_temporal_column( + client_with_repairs_cube, + "default.repairs_cube", + "default.repair_orders_fact.order_date", + ) + response = await client_with_repairs_cube.post( + "/nodes/default.repairs_cube/materialization/", + json={ + "job": "druid_measures_cube", + "strategy": "full", + "config": { + "spark": {}, + }, + "schedule": "", + }, + ) + assert ( + response.json()["message"] + == "Successfully updated materialization config named " + "`druid_measures_cube__full__default.repair_orders_fact.order_date` " + "for node `default.repairs_cube`" + ) + # args, _ = module__query_service_client.materialize.call_args_list[0] # type: ignore + checked = False + for args, _ in module__query_service_client.materialize.call_args_list: # type: ignore + if args[0].node_name == "default.repairs_cube": + assert str(parse(args[0].query)) == str( + parse(load_expected_file("druid_measures_cube.full.query.sql")), + ) + assert args[0].druid_spec == load_expected_file( + "druid_measures_cube.full.druid_spec.json", + ) + checked = True + break + assert checked + + # Reset by deleting the materialization + response = await client_with_repairs_cube.delete( + "/nodes/default.repairs_cube/materializations/", + params={ + "materialization_name": "druid_measures_cube__full", + }, + ) + assert response.status_code in (200, 201) + + # [failure] When there are no columns on the cube with type `timestamp` and no partition labels + response = await client_with_repairs_cube.post( + "/nodes/cube/", + json={ + "metrics": [ + "default.num_repair_orders", + "default.total_repair_cost", + ], + "dimensions": [ + "default.hard_hat.state", + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + ], + "filters": ["default.hard_hat.state='AZ'"], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.bad_repairs_cube", + }, + ) + assert response.status_code in (200, 201) + response = await client_with_repairs_cube.post( + "/nodes/default.bad_repairs_cube/materialization/", + json={ + "job": "druid_measures_cube", + "strategy": "full", + "config": { + "spark": {}, + }, + "schedule": "", + }, + ) + assert response.json()["message"] == ( + "The cube materialization cannot be configured if there is no " + "temporal partition specified on the cube. Please make sure at " + "least one cube element has a temporal partition defined" + ) + + # [failure] If nothing has changed, will not update the existing materialization + response = await client_with_repairs_cube.post( + "/nodes/default.repairs_cube/materialization/", + json={ + "job": "druid_measures_cube", + "strategy": "full", + "config": { + "druid": {"a": "b"}, + "spark": {}, + }, + "schedule": "", + }, + ) + assert response.json()["message"] == ( + "The same materialization config with name " + "`druid_measures_cube__full__default.repair_orders_fact.order_date` already " + "exists for node `default.repairs_cube` so no update was performed." + ) + + +@pytest.mark.asyncio +async def test_druid_measures_cube_incremental( + client_with_repairs_cube: AsyncClient, + module__query_service_client: QueryServiceClient, + load_expected_file, + set_temporal_column, + set_categorical_partition, +): + """ + Verifying this materialization setup: + - Job Type: druid_measures_cube + - Strategy: incremental_time + Cases to check: + - [failure] If there is no time partition column configured, fail. This is because without the + time partition we don't know the granularity for incremental materialization. + - [success] When there is a column on the cube with the partition label + - [success] When the underlying measures node contains DJ_LOGICAL_TIMESTAMP + """ + cube_name = "default.repairs_cube__incremental" + client_with_repairs_cube = await client_with_repairs_cube(cube_name=cube_name) + # [failure] If there is no time partition column configured + response = await client_with_repairs_cube.post( + f"/nodes/{cube_name}/materialization/", + json={ + "job": "druid_measures_cube", + "strategy": "incremental_time", + "config": {}, + "schedule": "@daily", + }, + ) + assert response.json()["message"] == ( + "Cannot create materialization with strategy `incremental_time` " + "without specifying a time partition column!" + ) + + # [success] When there is a column on the cube with the partition label, should succeed. + await set_temporal_column( + client_with_repairs_cube, + cube_name, + "default.repair_orders_fact.order_date", + ) + response = await client_with_repairs_cube.post( + f"/nodes/{cube_name}/materialization/", + json={ + "job": "druid_measures_cube", + "strategy": "incremental_time", + "config": {}, + "schedule": "@daily", + }, + ) + assert response.status_code in (200, 201) + assert response.json()["message"] == ( + "Successfully updated materialization config named " + "`druid_measures_cube__incremental_time__default.repair_orders_fact.order_date` " + f"for node `{cube_name}`" + ) + args, _ = module__query_service_client.materialize.call_args_list[-1] # type: ignore + assert str( + parse( + args[0].query.replace("${dj_logical_timestamp}", "DJ_LOGICAL_TIMESTAMP()"), + ), + ) == str( + parse( + load_expected_file("druid_measures_cube.incremental.query.sql").replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ), + ), + ) + assert args[0].druid_spec == load_expected_file( + "druid_measures_cube.incremental.druid_spec.json", + ) + + # [success] When the node itself contains DJ_LOGICAL_TIMESTAMP + response = await client_with_repairs_cube.patch( + "/nodes/default.repair_orders_fact", + json={ + "query": """SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay +FROM + default.repair_orders repair_orders +JOIN + default.repair_order_details repair_order_details +ON repair_orders.repair_order_id = repair_order_details.repair_order_id +WHERE repair_orders.order_date = DJ_LOGICAL_TIMESTAMP()""", + }, + ) + assert response.status_code in (200, 201) + response = await client_with_repairs_cube.get("/nodes/default.repair_orders_fact") + + # Delete previous + response = await client_with_repairs_cube.delete( + f"/nodes/{cube_name}/materializations/", + params={ + "materialization_name": "druid_measures_cube__incremental_time__default." + "repair_orders_fact.order_date", + }, + ) + assert response.status_code in (200, 201) + response = await client_with_repairs_cube.post( + f"/nodes/{cube_name}/materialization/", + json={ + "job": "druid_measures_cube", + "strategy": "incremental_time", + "config": {}, + "schedule": "@daily", + }, + ) + assert response.status_code in (200, 201) + args, _ = module__query_service_client.materialize.call_args_list[-1] # type: ignore + assert str( + parse( + args[0].query.replace("${dj_logical_timestamp}", "DJ_LOGICAL_TIMESTAMP()"), + ), + ) == str( + parse( + load_expected_file( + "druid_measures_cube.incremental.patched.query.sql", + ).replace("${dj_logical_timestamp}", "DJ_LOGICAL_TIMESTAMP()"), + ), + ) + assert args[0].druid_spec == load_expected_file( + "druid_measures_cube.incremental.druid_spec.json", + ) + + # [success] When there is a column on the cube with the partition label, should succeed. + await set_temporal_column( + client_with_repairs_cube, + cube_name, + "default.repair_orders_fact.order_date", + ) + await set_categorical_partition( + client_with_repairs_cube, + cube_name, + "default.dispatcher.company_name", + ) + response = await client_with_repairs_cube.post( + f"/nodes/{cube_name}/materialization/", + json={ + "job": "druid_measures_cube", + "strategy": "incremental_time", + "config": {}, + "schedule": "@daily", + }, + ) + assert response.status_code in (200, 201) + assert response.json()["message"] == ( + "Successfully updated materialization config named " + "`druid_measures_cube__incremental_time__default.repair_" + "orders_fact.order_date__default.dispatcher.company_name` " + f"for node `{cube_name}`" + ) + args, _ = module__query_service_client.materialize.call_args_list[-1] # type: ignore + assert str( + parse( + args[0] + .query.replace("${dj_logical_timestamp}", "DJ_LOGICAL_TIMESTAMP()") + .replace("${default_DOT_dispatcher_DOT_company_name}", "DJ_COMPANY_NAME()"), + ), + ) == str( + parse( + load_expected_file("druid_measures_cube.incremental.categorical.query.sql") + .replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ) + .replace("${default_DOT_dispatcher_DOT_company_name}", "DJ_COMPANY_NAME()"), + ), + ) + assert args[0].druid_spec == load_expected_file( + "druid_measures_cube.incremental.druid_spec.json", + ) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="The test is unstable depending on run order") +async def test_druid_metrics_cube_incremental( + client_with_repairs_cube: AsyncClient, + module__query_service_client: QueryServiceClient, + load_expected_file, + set_temporal_column, + set_categorical_partition, +): + """ + Verifying this materialization setup: + - Job Type: druid_metrics_cube + - Strategy: incremental_time + Cases to check: + - [failure] If there is no time partition column configured, fail. This is because without the + time partition we don't know the granularity for incremental materialization. + - [success] When there is a column on the cube with the partition label + - [success] When the underlying measures node contains DJ_LOGICAL_TIMESTAMP + """ + cube_name = "default.repairs_cube__metrics_incremental" + client_with_repairs_cube = await client_with_repairs_cube(cube_name=cube_name) + + # [failure] If there is no time partition column configured + response = await client_with_repairs_cube.post( + f"/nodes/{cube_name}/materialization/", + json={ + "job": "druid_metrics_cube", + "strategy": "incremental_time", + "config": {}, + "schedule": "@daily", + }, + ) + assert response.json()["message"] == ( + "Cannot create materialization with strategy `incremental_time` " + "without specifying a time partition column!" + ) + + # [success] When there is a temporal partition column on the cube, should succeed. + await set_temporal_column( + client_with_repairs_cube, + cube_name, + "default.repair_orders_fact.order_date", + ) + response = await client_with_repairs_cube.post( + f"/nodes/{cube_name}/materialization/", + json={ + "job": "druid_metrics_cube", + "strategy": "incremental_time", + "config": {}, + "schedule": "@daily", + }, + ) + assert response.status_code in (200, 201) + assert response.json()["message"] == ( + "Successfully updated materialization config named " + "`druid_metrics_cube__incremental_time__default.repair_orders_fact.order_date` " + f"for node `{cube_name}`" + ) + args, _ = module__query_service_client.materialize.call_args_list[-1] # type: ignore + assert str( + parse( + args[0].query.replace("${dj_logical_timestamp}", "DJ_LOGICAL_TIMESTAMP()"), + ), + ) == str( + parse( + load_expected_file("druid_metrics_cube.incremental.query.sql").replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ), + ), + ) + assert args[0].druid_spec == load_expected_file( + "druid_metrics_cube.incremental.druid_spec.json", + ) + + # [success] When there is both a temporal and categorical partition column on the cube + await set_temporal_column( + client_with_repairs_cube, + cube_name, + "default.repair_orders_fact.order_date", + ) + await set_categorical_partition( + client_with_repairs_cube, + cube_name, + "default.hard_hat.state", + ) + response = await client_with_repairs_cube.post( + f"/nodes/{cube_name}/materialization/", + json={ + "job": "druid_metrics_cube", + "strategy": "incremental_time", + "config": {}, + "schedule": "@daily", + }, + ) + assert response.status_code in (200, 201) + assert response.json()["message"] == ( + "Successfully updated materialization config named " + "`druid_metrics_cube__incremental_time__default.repair_" + "orders_fact.order_date__default.hard_hat.state` " + f"for node `{cube_name}`" + ) + args, _ = module__query_service_client.materialize.call_args_list[-1] # type: ignore + assert str( + parse( + args[0] + .query.replace("${dj_logical_timestamp}", "DJ_LOGICAL_TIMESTAMP()") + .replace("${default_DOT_hard_hat_DOT_state}", "DJ_STATE()"), + ), + ) == str( + parse( + load_expected_file("druid_metrics_cube.incremental.categorical.query.sql") + .replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ) + .replace("${default_DOT_hard_hat_DOT_state}", "DJ_STATE()"), + ), + ) + assert args[0].druid_spec == load_expected_file( + "druid_metrics_cube.incremental.druid_spec.json", + ) + + +class AnyString(str): + "A helper str obj that compares equal to everything." + + def __eq__(self, other): + return True + + def __ne__(self, other): + return False + + def __repr__(self): + return "" + + +ANY_STRING = AnyString() + + +@pytest.mark.asyncio +async def test_druid_cube_incremental( + client_with_repairs_cube: AsyncClient, + module__query_service_client: QueryServiceClient, + set_temporal_column, +): + """ + Verifying this materialization setup: + - Job Type: druid_cube + - Strategy: incremental_time + """ + cube_name = "default.repairs_cube__default_incremental" + client_with_repairs_cube = await client_with_repairs_cube(cube_name=cube_name) + # [failure] If there is no time partition column configured + response = await client_with_repairs_cube.post( + f"/nodes/{cube_name}/materialization/", + json={ + "job": "druid_cube", + "strategy": "incremental_time", + "schedule": "@daily", + "lookback_window": "1 DAY", + }, + ) + assert response.json()["message"] == ( + "Cannot create materialization with strategy `incremental_time` " + "without specifying a time partition column!" + ) + + # [success] When there is a column on the cube with the partition label, should succeed. + await set_temporal_column( + client_with_repairs_cube, + cube_name, + "default.repair_orders_fact.order_date", + ) + response = await client_with_repairs_cube.post( + f"/nodes/{cube_name}/materialization/", + json={ + "job": "druid_cube", + "strategy": "incremental_time", + "schedule": "@daily", + "lookback_window": "1 DAY", + }, + ) + assert response.status_code in (200, 201) + assert response.json()["message"] == ( + "Successfully updated materialization config named `druid_cube__incremental_time__default" + ".repair_orders_fact.order_date` for node `default.repairs_cube__default_incremental`" + ) + _, kwargs = module__query_service_client.materialize_cube.call_args_list[0] # type: ignore + mat = kwargs["materialization_input"] + assert ( + mat.name + == "druid_cube__incremental_time__default.repair_orders_fact.order_date" + ) + assert mat.job == "DruidCubeMaterializationJob" + assert mat.cube == NodeNameVersion( + name="default.repairs_cube__default_incremental", + version="v1.0", + ) + assert mat.dimensions == [ + "default.repair_orders_fact.order_date", + "default.hard_hat.state", + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + ] + assert mat.metrics == [ + CubeMetric( + metric=NodeNameVersion(name="default.num_repair_orders", version="v1.0"), + required_measures=[ + MeasureKey( + node=NodeNameVersion( + name="default.repair_orders_fact", + version=ANY_STRING, + ), + measure_name="repair_order_id_count_0b7dfba0", + ), + ], + derived_expression="SELECT SUM(repair_order_id_count_0b7dfba0)" + " FROM default.repair_orders_fact", + ), + CubeMetric( + metric=NodeNameVersion(name="default.total_repair_cost", version="v1.0"), + required_measures=[ + MeasureKey( + node=NodeNameVersion( + name="default.repair_orders_fact", + version=ANY_STRING, + ), + measure_name="total_repair_cost_sum_9bdaf803", + ), + ], + derived_expression="SELECT sum(total_repair_cost_sum_9bdaf803)" + " FROM default.repair_orders_fact", + ), + ] + assert mat.measures_materializations[0].node == NodeNameVersion( + name="default.repair_orders_fact", + version=ANY_STRING, + ) + assert mat.measures_materializations[0].grain == [ + "default_DOT_repair_orders_fact_DOT_order_date", + "default_DOT_hard_hat_DOT_state", + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_municipality_dim_DOT_local_region", + ] + assert mat.measures_materializations[0].dimensions == [ + "default_DOT_repair_orders_fact_DOT_order_date", + "default_DOT_hard_hat_DOT_state", + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_municipality_dim_DOT_local_region", + ] + assert mat.measures_materializations[0].measures == [ + Measure( + name="repair_order_id_count_0b7dfba0", + expression="repair_order_id", + aggregation="COUNT", + rule=AggregationRule(type=Aggregability.FULL, level=None), + ), + Measure( + name="total_repair_cost_sum_9bdaf803", + expression="total_repair_cost", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL, level=None), + ), + ] + assert mat.measures_materializations[0].columns == [ + ColumnMetadata( + name="default_DOT_repair_orders_fact_DOT_order_date", + type="timestamp", + column="order_date", + node="default.repair_orders_fact", + semantic_entity="default.repair_orders_fact.order_date", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_hard_hat_DOT_state", + type="string", + column="state", + node="default.hard_hat", + semantic_entity="default.hard_hat.state", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_dispatcher_DOT_company_name", + type="string", + column="company_name", + node="default.dispatcher", + semantic_entity="default.dispatcher.company_name", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_municipality_dim_DOT_local_region", + type="string", + column="local_region", + node="default.municipality_dim", + semantic_entity="default.municipality_dim.local_region", + semantic_type="dimension", + ), + ColumnMetadata( + name="repair_order_id_count_0b7dfba0", + type="bigint", + column="repair_order_id_count_0b7dfba0", + node="default.repair_orders_fact", + semantic_entity="default.repair_orders_fact.repair_order_id_count_0b7dfba0", + semantic_type="measure", + ), + ColumnMetadata( + name="total_repair_cost_sum_9bdaf803", + type="double", + column="total_repair_cost_sum_9bdaf803", + node="default.repair_orders_fact", + semantic_entity="default.repair_orders_fact.total_repair_cost_sum_9bdaf803", + semantic_type="measure", + ), + ] + assert ( + mat.measures_materializations[0].timestamp_column + == "default_DOT_repair_orders_fact_DOT_order_date" + ) + assert mat.measures_materializations[0].timestamp_format == "yyyyMMdd" + assert mat.measures_materializations[0].granularity == Granularity.DAY + assert mat.measures_materializations[0].spark_conf is None + assert mat.measures_materializations[0].upstream_tables == [ + "default.roads.repair_orders", + "default.roads.repair_order_details", + "default.roads.hard_hats", + "default.roads.dispatchers", + "default.roads.municipality", + "default.roads.municipality_municipality_type", + "default.roads.municipality_type", + ] + assert mat.measures_materializations[0].output_table_name.startswith( + "default_repair_orders_fact", + ) + assert mat.combiners[0].node == NodeNameVersion( + name="default.repair_orders_fact", + version=ANY_STRING, + ) + assert mat.combiners[0].query is None + assert mat.combiners[0].columns == [ + ColumnMetadata( + name="default_DOT_repair_orders_fact_DOT_order_date", + type="timestamp", + column="order_date", + node="default.repair_orders_fact", + semantic_entity="default.repair_orders_fact.order_date", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_hard_hat_DOT_state", + type="string", + column="state", + node="default.hard_hat", + semantic_entity="default.hard_hat.state", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_dispatcher_DOT_company_name", + type="string", + column="company_name", + node="default.dispatcher", + semantic_entity="default.dispatcher.company_name", + semantic_type="dimension", + ), + ColumnMetadata( + name="default_DOT_municipality_dim_DOT_local_region", + type="string", + column="local_region", + node="default.municipality_dim", + semantic_entity="default.municipality_dim.local_region", + semantic_type="dimension", + ), + ColumnMetadata( + name="repair_order_id_count_0b7dfba0", + type="bigint", + column="repair_order_id_count_0b7dfba0", + node="default.repair_orders_fact", + semantic_entity="default.repair_orders_fact.repair_order_id_count_0b7dfba0", + semantic_type="measure", + ), + ColumnMetadata( + name="total_repair_cost_sum_9bdaf803", + type="double", + column="total_repair_cost_sum_9bdaf803", + node="default.repair_orders_fact", + semantic_entity="default.repair_orders_fact.total_repair_cost_sum_9bdaf803", + semantic_type="measure", + ), + ] + assert mat.combiners[0].grain == [ + "default_DOT_repair_orders_fact_DOT_order_date", + "default_DOT_hard_hat_DOT_state", + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_municipality_dim_DOT_local_region", + ] + assert mat.combiners[0].dimensions == [ + "default_DOT_repair_orders_fact_DOT_order_date", + "default_DOT_hard_hat_DOT_state", + "default_DOT_dispatcher_DOT_company_name", + "default_DOT_municipality_dim_DOT_local_region", + ] + assert mat.combiners[0].measures == [ + Measure( + name="repair_order_id_count_0b7dfba0", + expression="repair_order_id", + aggregation="COUNT", + rule=AggregationRule(type=Aggregability.FULL, level=None), + ), + Measure( + name="total_repair_cost_sum_9bdaf803", + expression="total_repair_cost", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL, level=None), + ), + ] + assert ( + mat.combiners[0].timestamp_column + == "default_DOT_repair_orders_fact_DOT_order_date" + ) + assert mat.combiners[0].timestamp_format == "yyyyMMdd" + assert mat.combiners[0].granularity == Granularity.DAY + assert mat.combiners[0].upstream_tables[0].startswith("default_repair_orders_fact") + + +@pytest.mark.asyncio +async def test_spark_sql_full( + module__client_with_roads: AsyncClient, + module__query_service_client: QueryServiceClient, + load_expected_file, +): + """ + Verifying this materialization setup: + - Job Type: SPARK_SQL + - Strategy: FULL + Cases to check: + - [failure] If the node SQL uses DJ_LOGICAL_TIMESTAMP(), the FULL strategy is not allowed + - [success] A transform/dimension with no partitions should work + - [success] A transform/dimension with partitions but no DJ_LOGICAL_TIMESTAMP() should work + This just means that the output table will be partitioned by the partition cols + """ + # [success] A transform/dimension with no partitions should work + response = await module__client_with_roads.post( + "/nodes/default.hard_hat/materialization/", + json={ + "job": "spark_sql", + "strategy": "full", + "config": {}, + "schedule": "0 * * * *", + }, + ) + data = response.json() + assert ( + data["message"] + == "Successfully updated materialization config named `spark_sql__full` for node " + "`default.hard_hat`" + ) + + # Reading the node should yield the materialization config + response = await module__client_with_roads.get("/nodes/default.hard_hat/") + data = response.json() + assert data["version"] == "v1.0" + materialization_query = data["materializations"][0]["config"]["query"] + assert str(parse(materialization_query)) == str( + parse(load_expected_file("spark_sql.full.query.sql")), + ) + del data["materializations"][0]["config"]["query"] + expected_config = load_expected_file("spark_sql.full.config.json") + expected_config[0]["id"] = mock.ANY + assert data["materializations"] == expected_config + + # Set both temporal and categorical partitions on node + response = await module__client_with_roads.post( + "/nodes/default.hard_hat/columns/birth_date/partition", + json={ + "type_": "temporal", + "granularity": "day", + "format": "yyyyMMdd", + }, + ) + assert response.status_code in (200, 201) + + response = await module__client_with_roads.post( + "/nodes/default.hard_hat/columns/country/partition", + json={ + "type_": "categorical", + }, + ) + assert response.status_code in (200, 201) + + # Setting the materialization config should succeed and it should reschedule + # the materialization with the temporal partition + response = await module__client_with_roads.post( + "/nodes/default.hard_hat/materialization/", + json={ + "job": "spark_sql", + "strategy": "full", + "config": {}, + "schedule": "0 * * * *", + }, + ) + data = response.json() + assert ( + data["message"] == "Successfully updated materialization config named " + "`spark_sql__full__birth_date__country` for node `default.hard_hat`" + ) + expected_query = load_expected_file("spark_sql.full.query.sql") + args, _ = module__query_service_client.materialize.call_args_list[-1] # type: ignore + assert str(parse(args[0].query)) == str(parse(expected_query)) + + # Check that the temporal partition is appended onto the list of partitions in the + # materialization config but is not included directly in the materialization query + response = await module__client_with_roads.get("/nodes/default.hard_hat/") + data = response.json() + assert data["version"] == "v1.0" + assert len(data["materializations"]) == 2 + + expected_query = load_expected_file("spark_sql.full.partition.query.sql") + args, _ = module__query_service_client.materialize.call_args_list[-1] # type: ignore + assert str(parse(args[0].query)) == str(parse(expected_query)) + materialization_with_partitions = data["materializations"][1] + del materialization_with_partitions["config"]["query"] + expected_config = load_expected_file("spark_sql.full.partition.config.json") + expected_config["id"] = mock.ANY + assert materialization_with_partitions == expected_config + + # Check listing materializations of the node + response = await module__client_with_roads.get( + "/nodes/default.hard_hat/materializations/", + ) + materializations = response.json() + materializations[0]["config"]["query"] = mock.ANY + expected_config = load_expected_file("spark_sql.full.materializations.json") + expected_config["id"] = mock.ANY + assert materializations[0] == expected_config + + materializations = response.json() + materializations[1]["config"]["query"] = mock.ANY + expected_config = load_expected_file( + "spark_sql.full.partition.materializations.json", + ) + expected_config["id"] = mock.ANY + assert materializations[1] == expected_config + + # Kick off backfill for this materialization + response = await module__client_with_roads.post( + "/nodes/default.hard_hat/materializations/spark_sql__full__birth_date__country/backfill", + json=[ + { + "column_name": "birth_date", + "range": ["20230101", "20230201"], + }, + ], + ) + assert module__query_service_client.run_backfill.call_args_list[0].args == ( # type: ignore + "default.hard_hat", + "v1.0", + "dimension", + "spark_sql__full__birth_date__country", + [ + PartitionBackfill( + column_name="birth_date", + values=None, + range=["20230101", "20230201"], + ), + ], + ) + assert response.json() == {"output_tables": [], "urls": ["http://fake.url/job"]} + + +@pytest.mark.asyncio +async def test_spark_sql_incremental( + module__client_with_roads: AsyncClient, + module__query_service_client: QueryServiceClient, + set_temporal_column, + set_categorical_partition, + load_expected_file, +): + """ + Verifying this materialization setup: + - Job Type: SPARK_SQL + - Strategy: INCREMENTAL + Cases to check: + - [failure] If the node SQL uses DJ_LOGICAL_TIMESTAMP(), the FULL strategy is not allowed + - [success] A transform/dimension with a time partition should work + - [success] A transform/dimension with a time partition and additional usage of + DJ_LOGICAL_TIMESTAMP() should work + - [success] A transform/dimension with a time partition and a categorical partition should work + """ + # [failure] No time partitions + response = await module__client_with_roads.post( + "/nodes/default.hard_hat_2/materialization/", + json={ + "job": "spark_sql", + "strategy": "incremental_time", + "config": {}, + "schedule": "0 * * * *", + }, + ) + data = response.json() + assert ( + data["message"] + == "Cannot create materialization with strategy `incremental_time` without " + "specifying a time partition column!" + ) + + # [success] A transform/dimension with a time partition should work + await set_temporal_column( + module__client_with_roads, + "default.hard_hat_2", + "birth_date", + ) + response = await module__client_with_roads.post( + "/nodes/default.hard_hat_2/materialization/", + json={ + "job": "spark_sql", + "strategy": "incremental_time", + "config": {}, + "schedule": "0 * * * *", + }, + ) + assert response.json()["message"] == ( + "Successfully updated materialization config named " + "`spark_sql__incremental_time__birth_date` for node `default.hard_hat_2`" + ) + + # The materialization query contains a filter on the time partition column + # to the DJ_LOGICAL_TIMESTAMP + args, _ = module__query_service_client.materialize.call_args_list[-1] # type: ignore + materialization_query = args[0].query + assert str( + parse( + materialization_query.replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ), + ), + ) == str( + parse( + load_expected_file("spark_sql.incremental.query.sql").replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ), + ), + ) + + # Reading the node should yield the materialization config + response = await module__client_with_roads.get("/nodes/default.hard_hat_2/") + data = response.json() + assert data["version"] == "v1.0" + del data["materializations"][0]["config"]["query"] + expected_config = load_expected_file("spark_sql.incremental.config.json") + expected_config[0]["id"] = mock.ANY + assert data["materializations"] == expected_config + + # Kick off backfill for this materialization + response = await module__client_with_roads.post( + "/nodes/default.hard_hat_2/materializations/" + "spark_sql__incremental_time__birth_date/backfill", + json=[ + { + "column_name": "birth_date", + "range": ["20230101", "20230201"], + }, + ], + ) + assert module__query_service_client.run_backfill.call_args_list[-1].args == ( # type: ignore + "default.hard_hat_2", + "v1.0", + "dimension", + "spark_sql__incremental_time__birth_date", + [ + PartitionBackfill( + column_name="birth_date", + values=None, + range=["20230101", "20230201"], + ), + ], + ) + assert response.json() == {"output_tables": [], "urls": ["http://fake.url/job"]} + + # [success] A transform/dimension with a time partition and additional usage of + # DJ_LOGICAL_TIMESTAMP() should work + response = await module__client_with_roads.patch( + "/nodes/default.hard_hat_2", + json={ + "query": "SELECT last_name, first_name, birth_date, country FROM default.hard_hats" + " WHERE DATE_FORMAT(birth_date, 'yyyyMMdd') = " + "DATE_FORMAT(DJ_LOGICAL_TIMESTAMP(), 'yyyyMMdd')", + }, + ) + assert response.status_code in (200, 201) + + # The materialization query contains a filter on the time partition column + # to the DJ_LOGICAL_TIMESTAMP + args, _ = module__query_service_client.materialize.call_args_list[-1] # type: ignore + materialization_query = args[0].query + assert str( + parse( + materialization_query.replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ), + ), + ) == str( + parse( + load_expected_file("spark_sql.incremental.additional.query.sql").replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ), + ), + ) + + # [success] A transform/dimension with a time partition and a categorical partition should work + await set_temporal_column( + module__client_with_roads, + "default.hard_hat_2", + "birth_date", + ) + await set_categorical_partition( + module__client_with_roads, + "default.hard_hat_2", + "country", + ) + response = await module__client_with_roads.post( + "/nodes/default.hard_hat_2/materialization/", + json={ + "job": "spark_sql", + "strategy": "incremental_time", + "config": {}, + "schedule": "0 * * * *", + }, + ) + assert response.status_code in (200, 201) + + # The materialization query contains a filter on the time partition column + # and the categorical partition columns + args, _ = module__query_service_client.materialize.call_args_list[-1] # type: ignore + materialization_query = args[0].query + + assert str( + parse( + materialization_query.replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ).replace("${country}", "DJ_COUNTRY()"), + ), + ) == str( + parse( + load_expected_file("spark_sql.incremental.categorical.query.sql") + .replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ) + .replace("${country}", "DJ_COUNTRY()"), + ), + ) + + # [success] A transform/dimension with a time partition and a categorical partition, + # along with a lookback window configured + await set_temporal_column( + module__client_with_roads, + "default.hard_hat_2", + "birth_date", + ) + await set_categorical_partition( + module__client_with_roads, + "default.hard_hat_2", + "country", + ) + response = await module__client_with_roads.post( + "/nodes/default.hard_hat_2/materialization/", + json={ + "job": "spark_sql", + "strategy": "incremental_time", + "config": { + "lookback_window": "100 DAYS", + }, + "schedule": "0 * * * *", + }, + ) + assert response.status_code in (200, 201) + + # The materialization query's temporal partition filter includes the lookback window + args, _ = module__query_service_client.materialize.call_args_list[-1] # type: ignore + materialization_query = args[0].query + + assert str( + parse( + materialization_query.replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ).replace("${country}", "DJ_COUNTRY()"), + ), + ) == str( + parse( + load_expected_file("spark_sql.incremental.lookback.query.sql") + .replace( + "${dj_logical_timestamp}", + "DJ_LOGICAL_TIMESTAMP()", + ) + .replace("${country}", "DJ_COUNTRY()"), + ), + ) + + +@pytest.mark.asyncio +async def test_spark_with_availablity( + module__client_with_roads: AsyncClient, +): + """ + Verify that we build the Query correctly with or without the materialization availability. + """ + # add one transform node + response = await module__client_with_roads.post( + "/nodes/transform/", + json={ + "query": ("SELECT first_name, birth_date FROM default.hard_hats"), + "description": "test transform", + "mode": "published", + "name": "default.test_transform", + }, + ) + assert response.status_code in (200, 201) + + # add another transform node on top of the 1st one + response = await module__client_with_roads.post( + "/nodes/transform/", + json={ + "query": ("SELECT first_name, birth_date FROM default.test_transform"), + "description": "test transform two", + "mode": "published", + "name": "default.test_transform_two", + }, + ) + assert response.status_code in (200, 201) + + # create a materialization on the 1st node (w/o availability) + response = await module__client_with_roads.post( + "/nodes/default.test_transform/materialization/", + json={ + "job": "spark_sql", + "strategy": "full", + "config": {}, + "schedule": "0 * * * *", + }, + ) + assert response.status_code in (200, 201) + + # check the materialization on 1st node + query_one = """ + WITH default_DOT_test_transform AS ( + SELECT + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.birth_date + FROM roads.hard_hats AS default_DOT_hard_hats + ) + SELECT + default_DOT_test_transform.first_name, + default_DOT_test_transform.birth_date + FROM default_DOT_test_transform + """ + response = await module__client_with_roads.get( + "/nodes/default.test_transform/materializations/", + ) + assert response.status_code in (200, 201) + assert str(parse(response.json()[0]["config"]["query"])) == str(parse(query_one)) + materialization_id = response.json()[0]["id"] + + # create a materialization on the 2nd node (w/o availability) + response = await module__client_with_roads.post( + "/nodes/default.test_transform_two/materialization/", + json={ + "job": "spark_sql", + "strategy": "full", + "config": {}, + "schedule": "0 * * * *", + }, + ) + assert response.status_code in (200, 201) + + # check the materialization on 2nd node + query_two = """WITH default_DOT_test_transform AS ( + SELECT + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.birth_date + FROM roads.hard_hats AS default_DOT_hard_hats + ), + default_DOT_test_transform_two AS ( + SELECT + default_DOT_test_transform.first_name, + default_DOT_test_transform.birth_date + FROM default_DOT_test_transform + ) + SELECT + default_DOT_test_transform_two.first_name, + default_DOT_test_transform_two.birth_date + FROM default_DOT_test_transform_two + """ + response = await module__client_with_roads.get( + "/nodes/default.test_transform_two/materializations/", + ) + assert response.status_code in (200, 201) + + # add some availability to the 1st node + response = await module__client_with_roads.post( + "/data/default.test_transform/availability/", + json={ + "catalog": "default", + "schema_": "accounting", + "table": "test_transform_materialized", + "valid_through_ts": 20230125, + "max_temporal_partition": ["2023", "01", "25"], + "min_temporal_partition": ["2022", "01", "01"], + "custom_metadata": { + "url": "http://some.catalog.com/default.accounting.pmts", + }, + "materialization_id": materialization_id, + }, + ) + assert response.status_code in (200, 201) + + # refresh the materialization on 1st node now with availability (query should be the same) + response = await module__client_with_roads.post( + "/nodes/default.test_transform/materialization/", + json={ + "job": "spark_sql", + "strategy": "full", + "config": {}, + "schedule": "0 * * * *", + }, + ) + assert response.status_code in (200, 201) + + # check the materialization query again (query should be the same) + response = await module__client_with_roads.get( + "/nodes/default.test_transform/materializations/", + ) + assert response.status_code in (200, 201) + assert str(parse(response.json()[0]["config"]["query"])) == str(parse(query_one)) + + # refresh the materialization on 2nd node now with availability + # (query should now include availability of the 1st node) + response = await module__client_with_roads.post( + "/nodes/default.test_transform_two/validate/", + ) + assert response.status_code in (200, 201) + + response = await module__client_with_roads.post( + "/nodes/default.test_transform_two/materialization/", + json={ + "job": "spark_sql", + "strategy": "full", + "config": {}, + "schedule": "0 * * * *", + }, + ) + assert response.status_code in (200, 201) + + # check the materialization query again (query should be the same) + response = await module__client_with_roads.get( + "/nodes/default.test_transform_two/materializations/", + ) + assert response.status_code in (200, 201) + assert response.json()[0]["config"]["query"] != query_two + assert ( + "FROM accounting.test_transform_materialized " + in response.json()[0]["config"]["query"] + ) + + +@pytest.mark.asyncio +async def test_generated_python_client_code_adding_materialization( + module__client_with_basic: AsyncClient, +): + """ + Test that generating python client code for adding materialization works + """ + await module__client_with_basic.post( + "/engines/", + json={ + "name": "spark", + "version": "2.4.4", + "dialect": "spark", + }, + ) + await module__client_with_basic.post( + "/nodes/basic.transform.country_agg/materialization/", + json={ + "job": "spark_sql", + "strategy": "full", + "config": { + "spark": {}, + }, + "schedule": "0 * * * *", + }, + ) + response = await module__client_with_basic.get( + "/datajunction-clients/python/add_materialization/" + "basic.transform.country_agg/spark_sql__full", + ) + assert ( + response.json() + == """dj = DJBuilder(DJ_URL) + +country_agg = dj.transform( + "basic.transform.country_agg" +) +materialization = MaterializationConfig( + job="spark_sql", + strategy="full", + schedule="0 * * * *", + config={ + "spark": {} + }, +) +country_agg.add_materialization( + materialization +)""" + ) diff --git a/datajunction-server/tests/api/measures_test.py b/datajunction-server/tests/api/measures_test.py new file mode 100644 index 000000000..dc9b029fa --- /dev/null +++ b/datajunction-server/tests/api/measures_test.py @@ -0,0 +1,250 @@ +""" +Tests for the namespaces API. +""" + +from typing import Dict + +import pytest +from httpx import AsyncClient + + +def completed_repairs_measure(measure_name: str = "completed_repairs") -> Dict: + """ + Test ``GET /measures/``. + """ + return { + "name": f"{measure_name}", + "description": "Number of completed repairs", + "columns": [ + { + "node": "default.regional_level_agg", + "column": "completed_repairs", + }, + ], + } + + +@pytest.fixture +def failed_measure() -> Dict: + """ + Measure that will fail due to one of the columns not existing + """ + return { + "name": "completed_repairs2", + "description": "Number of completed repairs", + "columns": [ + { + "node": "default.regional_level_agg", + "column": "completed_repairs", + }, + { + "node": "default.national_level_agg", + "column": "completed_repairs", + }, + ], + } + + +@pytest.mark.asyncio +async def test_list_all_measures( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test ``GET /measures/``. + """ + response = await module__client_with_roads.get("/measures/") + assert response.status_code in (200, 201) + assert response.json() == [] + + await module__client_with_roads.post( + "/measures/", + json=completed_repairs_measure(measure_name="completed_repairs_1"), + ) + + response = await module__client_with_roads.get("/measures/") + assert response.status_code in (200, 201) + assert response.json() == ["completed_repairs_1"] + + response = await module__client_with_roads.get("/measures/?prefix=comp") + assert response.status_code in (200, 201) + assert response.json() == ["completed_repairs_1"] + + response = await module__client_with_roads.get("/measures/?prefix=xyz") + assert response.status_code in (200, 201) + assert response.json() == [] + + response = await module__client_with_roads.get("/measures/completed_repairs_1") + assert response.status_code in (200, 201) + assert response.json() == { + "additive": "non-additive", + "columns": [ + { + "name": "completed_repairs", + "node": "default.regional_level_agg", + "type": "bigint", + }, + ], + "description": "Number of completed repairs", + "display_name": "Completed Repairs 1", + "name": "completed_repairs_1", + } + + response = await module__client_with_roads.get("/measures/random_measure") + assert response.status_code >= 400 + assert ( + response.json()["message"] + == "Measure with name `random_measure` does not exist" + ) + + +@pytest.mark.asyncio +async def test_create_measure( + module__client_with_roads: AsyncClient, + failed_measure: Dict, +) -> None: + """ + Test ``POST /measures/``. + """ + # Successful measure creation + response = await module__client_with_roads.post( + "/measures/", + json=completed_repairs_measure(measure_name="completed_repairs_2"), + ) + assert response.status_code in (200, 201) + assert response.json() == { + "additive": "non-additive", + "columns": [ + { + "name": "completed_repairs", + "node": "default.regional_level_agg", + "type": "bigint", + }, + ], + "description": "Number of completed repairs", + "display_name": "Completed Repairs 2", + "name": "completed_repairs_2", + } + + # Creating the same measure again will fail + response = await module__client_with_roads.post( + "/measures/", + json=completed_repairs_measure(measure_name="completed_repairs_2"), + ) + assert response.status_code >= 400 + assert response.json()["message"] == "Measure `completed_repairs_2` already exists!" + + # Failed measure creation + response = await module__client_with_roads.post("/measures/", json=failed_measure) + assert response.status_code >= 400 + assert response.json()["message"] == ( + "Column `completed_repairs` does not exist on node `default.national_level_agg`" + ) + + +@pytest.mark.asyncio +async def test_edit_measure( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test ``PATCH /measures/{name}``. + """ + await module__client_with_roads.post( + "/measures", + json=completed_repairs_measure(measure_name="completed_repairs_3"), + ) + + # Successfully edit measure + response = await module__client_with_roads.patch( + "/measures/completed_repairs_3", + json={ + "additive": "additive", + "display_name": "blah", + "description": "random description", + }, + ) + assert response.status_code in (200, 201) + assert response.json() == { + "additive": "additive", + "columns": [ + { + "name": "completed_repairs", + "node": "default.regional_level_agg", + "type": "bigint", + }, + ], + "description": "random description", + "display_name": "blah", + "name": "completed_repairs_3", + } + + response = await module__client_with_roads.patch( + "/measures/completed_repairs_3", + json={ + "additive": "non-additive", + "columns": [], + }, + ) + assert response.status_code in (200, 201) + assert response.json() == { + "additive": "non-additive", + "columns": [], + "description": "random description", + "display_name": "blah", + "name": "completed_repairs_3", + } + + response = await module__client_with_roads.patch( + "/measures/completed_repairs_3", + json={ + "columns": [ + { + "node": "default.regional_level_agg", + "column": "completed_repairs", + }, + { + "node": "default.national_level_agg", + "column": "total_amount_nationwide", + }, + ], + }, + ) + assert response.status_code in (200, 201) + assert response.json() == { + "additive": "non-additive", + "columns": [ + { + "name": "total_amount_nationwide", + "node": "default.national_level_agg", + "type": "double", + }, + { + "name": "completed_repairs", + "node": "default.regional_level_agg", + "type": "bigint", + }, + ], + "description": "random description", + "display_name": "blah", + "name": "completed_repairs_3", + } + + # Failed edit + response = await module__client_with_roads.patch( + "/measures/completed_repairs_3", + json={ + "columns": [ + { + "node": "default.regional_level_agg", + "column": "completed_repairs", + }, + { + "node": "default.national_level_agg", + "column": "non_existent_column", + }, + ], + }, + ) + assert response.status_code >= 400 + assert response.json()["message"] == ( + "Column `non_existent_column` does not exist on node `default.national_level_agg`" + ) diff --git a/datajunction-server/tests/api/metrics_test.py b/datajunction-server/tests/api/metrics_test.py new file mode 100644 index 000000000..b6e1b46c0 --- /dev/null +++ b/datajunction-server/tests/api/metrics_test.py @@ -0,0 +1,1332 @@ +""" +Tests for the metrics API. +""" + +import pytest +from httpx import AsyncClient +from sqlalchemy import select, text +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database.attributetype import AttributeType, ColumnAttribute +from datajunction_server.database.column import Column +from datajunction_server.database.database import Database +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.user import User +from datajunction_server.models.node_type import NodeType +from datajunction_server.sql.parsing.types import FloatType, IntegerType, StringType + +expected_dimensions = [ + { + "name": "default.dispatcher.company_name", + "node_name": "default.dispatcher", + "node_display_name": "Dispatcher", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.dispatcher.dispatcher_id", + "node_name": "default.dispatcher", + "node_display_name": "Dispatcher", + "filter_only": False, + "type": "int", + "path": ["default.repair_orders_fact"], + "properties": ["primary_key"], + }, + { + "name": "default.dispatcher.phone", + "node_name": "default.dispatcher", + "node_display_name": "Dispatcher", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat.address", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat.birth_date", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "timestamp", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat.city", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat.contractor_id", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "int", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat.country", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat.first_name", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat.hard_hat_id", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "int", + "path": ["default.repair_orders_fact"], + "properties": ["primary_key"], + }, + { + "name": "default.hard_hat.hire_date", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "timestamp", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat.last_name", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat.manager", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "int", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat.postal_code", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat.state", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat.title", + "node_name": "default.hard_hat", + "node_display_name": "Hard Hat", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat_to_delete.address", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat_to_delete.birth_date", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "timestamp", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat_to_delete.city", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat_to_delete.contractor_id", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "int", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat_to_delete.country", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat_to_delete.first_name", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat_to_delete.hard_hat_id", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "int", + "path": ["default.repair_orders_fact"], + "properties": ["primary_key"], + }, + { + "name": "default.hard_hat_to_delete.hire_date", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "timestamp", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat_to_delete.last_name", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat_to_delete.manager", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "int", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat_to_delete.postal_code", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat_to_delete.state", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.hard_hat_to_delete.title", + "node_name": "default.hard_hat_to_delete", + "node_display_name": "Hard Hat To Delete", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.municipality_dim.contact_name", + "node_name": "default.municipality_dim", + "node_display_name": "Municipality Dim", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.municipality_dim.contact_title", + "node_name": "default.municipality_dim", + "node_display_name": "Municipality Dim", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.municipality_dim.local_region", + "node_name": "default.municipality_dim", + "node_display_name": "Municipality Dim", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.municipality_dim.municipality_id", + "node_name": "default.municipality_dim", + "node_display_name": "Municipality Dim", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": ["primary_key"], + }, + { + "name": "default.municipality_dim.municipality_type_desc", + "node_name": "default.municipality_dim", + "node_display_name": "Municipality Dim", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.municipality_dim.municipality_type_id", + "node_name": "default.municipality_dim", + "node_display_name": "Municipality Dim", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.municipality_dim.state_id", + "node_name": "default.municipality_dim", + "node_display_name": "Municipality Dim", + "filter_only": False, + "type": "int", + "path": ["default.repair_orders_fact"], + "properties": [], + }, + { + "name": "default.us_state.state_id", + "node_name": "default.us_state", + "node_display_name": "Us State", + "filter_only": False, + "type": "int", + "path": ["default.repair_orders_fact", "default.hard_hat"], + "properties": [], + }, + { + "name": "default.us_state.state_name", + "node_name": "default.us_state", + "node_display_name": "Us State", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact", "default.hard_hat"], + "properties": [], + }, + { + "name": "default.us_state.state_region", + "node_name": "default.us_state", + "node_display_name": "Us State", + "filter_only": False, + "type": "int", + "path": ["default.repair_orders_fact", "default.hard_hat"], + "properties": [], + }, + { + "name": "default.us_state.state_short", + "node_name": "default.us_state", + "node_display_name": "Us State", + "filter_only": False, + "type": "string", + "path": ["default.repair_orders_fact", "default.hard_hat"], + "properties": ["primary_key"], + }, +] + + +@pytest.mark.asyncio +async def test_read_metrics(module__client_with_roads: AsyncClient) -> None: + """ + Test ``GET /metrics/``. + """ + response = await module__client_with_roads.get("/metrics/") + data = response.json() + + assert response.status_code == 200 + assert len(data) > 5 + + response = await module__client_with_roads.get("/metrics/default.num_repair_orders") + data = response.json() + assert data["metric_metadata"] == { + "direction": "higher_is_better", + "unit": { + "abbreviation": None, + "category": None, + "description": None, + "label": "Dollar", + "name": "DOLLAR", + }, + } + assert data["upstream_node"] == "default.repair_orders_fact" + assert data["expression"] == "count(repair_order_id)" + + response = await module__client_with_roads.get( + "/metrics/default.discounted_orders_rate", + ) + data = response.json() + assert data["incompatible_druid_functions"] == ["IF"] + assert data["measures"] == [ + { + "aggregation": "SUM", + "expression": "if(discount > 0.0, 1, 0)", + "name": "discount_sum_62846f49", + "rule": { + "level": None, + "type": "full", + }, + }, + { + "aggregation": "COUNT", + "expression": "*", + "name": "count_3389dae3", + "rule": { + "level": None, + "type": "full", + }, + }, + ] + assert data["derived_query"] == ( + "SELECT CAST(sum(discount_sum_62846f49) AS DOUBLE) / SUM(count_3389dae3) AS " + "default_DOT_discounted_orders_rate \n FROM default.repair_orders_fact" + ) + assert data["derived_expression"] == ( + "CAST(sum(discount_sum_62846f49) AS DOUBLE) / SUM(count_3389dae3) " + "AS default_DOT_discounted_orders_rate" + ) + + +@pytest.mark.asyncio +async def test_read_metric( + module__session: AsyncSession, + module__client: AsyncClient, + module__current_user: User, +) -> None: + """ + Test ``GET /metric/{node_id}/``. + """ + await module__client.get("/attributes/") + dimension_attribute = ( + await module__session.execute( + select(AttributeType).where(AttributeType.name == "dimension"), + ) + ).scalar_one() + parent_rev = NodeRevision( + name="parent", + type=NodeType.SOURCE, + version="1", + columns=[ + Column( + name="ds", + type=StringType(), + attributes=[ColumnAttribute(attribute_type=dimension_attribute)], + order=0, + ), + Column( + name="user_id", + type=IntegerType(), + attributes=[ColumnAttribute(attribute_type=dimension_attribute)], + order=2, + ), + Column( + name="foo", + type=FloatType(), + attributes=[ColumnAttribute(attribute_type=dimension_attribute)], + order=3, + ), + ], + created_by_id=module__current_user.id, + ) + parent_node = Node( + name=parent_rev.name, + namespace="default", + type=NodeType.SOURCE, + current_version="1", + created_by_id=module__current_user.id, + ) + parent_rev.node = parent_node + + child_node = Node( + name="child", + namespace="default", + type=NodeType.METRIC, + current_version="1", + created_by_id=module__current_user.id, + ) + child_rev = NodeRevision( + name=child_node.name, + node=child_node, + type=child_node.type, + version="1", + query="SELECT COUNT(*) FROM parent", + parents=[parent_node], + created_by_id=module__current_user.id, + ) + + module__session.add(child_rev) + await module__session.commit() + + response = await module__client.get("/metrics/child/") + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "child" + assert data["query"] == "SELECT COUNT(*) FROM parent" + assert data["dimensions"] == [ + { + "filter_only": False, + "name": "parent.ds", + "node_display_name": "Parent", + "node_name": "parent", + "path": [], + "type": "string", + "properties": ["dimension"], + }, + { + "filter_only": False, + "name": "parent.foo", + "node_display_name": "Parent", + "node_name": "parent", + "path": [], + "type": "float", + "properties": ["dimension"], + }, + { + "filter_only": False, + "name": "parent.user_id", + "node_display_name": "Parent", + "node_name": "parent", + "path": [], + "type": "int", + "properties": ["dimension"], + }, + ] + + +@pytest.mark.asyncio +async def test_read_metrics_errors( + module__session: AsyncSession, + module__client: AsyncClient, + module__current_user: User, +) -> None: + """ + Test errors on ``GET /metrics/{node_id}/``. + """ + database = Database(name="test", URI="sqlite://") + node = Node( + name="a-metric", + namespace="default", + type=NodeType.TRANSFORM, + current_version="1", + created_by_id=module__current_user.id, + ) + node_revision = NodeRevision( + name=node.name, + type=node.type, + node=node, + version="1", + query="SELECT 1 AS col", + created_by_id=module__current_user.id, + ) + module__session.add(database) + module__session.add(node_revision) + await module__session.execute(text("CREATE TABLE my_table (one TEXT)")) + await module__session.commit() + + response = await module__client.get("/metrics/foo") + assert response.status_code == 404 + data = response.json() + assert data["message"] == "A node with name `foo` does not exist." + + response = await module__client.get("/metrics/a-metric") + assert response.status_code == 400 + assert response.json() == {"detail": "Not a metric node: `a-metric`"} + + +@pytest.mark.asyncio +async def test_common_dimensions( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test ``GET /metrics/common/dimensions``. + """ + response = await module__client_with_roads.get( + "/metrics/common/dimensions?" + "metric=default.total_repair_order_discounts" + "&metric=default.total_repair_cost", + ) + assert response.status_code == 200 + assert response.json() == expected_dimensions + + +@pytest.mark.asyncio +async def test_no_common_dimensions( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test getting common dimensions for metrics that have none in common + """ + await module__client_with_roads.post( + "/nodes/source/", + json={ + "columns": [ + { + "name": "counts", + "type": "struct", + }, + ], + "description": "Collection of dreams", + "mode": "published", + "name": "basic.dreams_1", + "catalog": "public", + "schema_": "basic", + "table": "dreams", + }, + ) + + response = await module__client_with_roads.post( + "/nodes/metric/", + json={ + "query": "SELECT SUM(counts.b) FROM basic.dreams_1", + "description": "Dream Counts", + "mode": "published", + "name": "basic.dream_count_1", + }, + ) + response = await module__client_with_roads.get( + "/metrics/common/dimensions?" + "metric=basic.dream_count_1&metric=default.total_repair_order_discounts", + ) + assert response.status_code == 200 + assert response.json() == [] + + +@pytest.mark.asyncio +async def test_raise_common_dimensions_not_a_metric_node( + module__client_with_account_revenue, +) -> None: + """ + Test raising ``GET /metrics/common/dimensions`` when not a metric node + """ + response = await module__client_with_account_revenue.get( + "/metrics/common/dimensions?" + "metric=default.total_repair_order_discounts" + "&metric=default.payment_type", + ) + assert response.status_code == 422 + assert response.json()["message"] == "Not a metric node: default.payment_type" + + +@pytest.mark.asyncio +async def test_raise_common_dimensions_metric_not_found( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test raising ``GET /metrics/common/dimensions`` when metric not found + """ + response = await module__client_with_roads.get( + "/metrics/common/dimensions?metric=default.foo&metric=default.bar", + ) + assert response.status_code == 422 + assert response.json() == { + "errors": [ + { + "code": 203, + "context": "", + "debug": None, + "message": "Metric nodes not found: default.foo,default.bar", + }, + ], + "message": "Metric nodes not found: default.foo,default.bar", + "warnings": [], + } + + +@pytest.mark.asyncio +async def test_get_dimensions(module__client_with_roads: AsyncClient): + """ + Testing get dimensions for a metric + """ + response = await module__client_with_roads.get("/metrics/default.avg_repair_price/") + + data = response.json() + assert data["dimensions"] == expected_dimensions + + +@pytest.mark.asyncio +async def test_get_multi_link_dimensions( + module__client_with_dimension_link, +): + """ + In some cases, the same dimension may be linked to different columns on a node. + The returned dimension attributes should the join path between the given dimension + attribute and the original node, in order to help disambiguate the source of the dimension. + """ + response = await module__client_with_dimension_link.get( + "/metrics/default.avg_user_age/", + ) + assert response.json()["dimensions"] == [ + { + "name": "default.date_dim.dateint[birth_country->formation_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.birth_country", + "default.special_country_dim.formation_date", + ], + "type": "int", + "filter_only": False, + "properties": ["primary_key"], + }, + { + "name": "default.date_dim.dateint[birth_country->last_election_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.birth_country", + "default.special_country_dim.last_election_date", + ], + "type": "int", + "filter_only": False, + "properties": ["primary_key"], + }, + { + "name": "default.date_dim.dateint[residence_country->formation_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.residence_country", + "default.special_country_dim.formation_date", + ], + "type": "int", + "filter_only": False, + "properties": ["primary_key"], + }, + { + "name": "default.date_dim.dateint[residence_country->last_election_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.residence_country", + "default.special_country_dim.last_election_date", + ], + "type": "int", + "filter_only": False, + "properties": ["primary_key"], + }, + { + "name": "default.date_dim.day[birth_country->formation_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.birth_country", + "default.special_country_dim.formation_date", + ], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.date_dim.day[birth_country->last_election_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.birth_country", + "default.special_country_dim.last_election_date", + ], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.date_dim.day[residence_country->formation_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.residence_country", + "default.special_country_dim.formation_date", + ], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.date_dim.day[residence_country->last_election_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.residence_country", + "default.special_country_dim.last_election_date", + ], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.date_dim.month[birth_country->formation_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.birth_country", + "default.special_country_dim.formation_date", + ], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.date_dim.month[birth_country->last_election_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.birth_country", + "default.special_country_dim.last_election_date", + ], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.date_dim.month[residence_country->formation_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.residence_country", + "default.special_country_dim.formation_date", + ], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.date_dim.month[residence_country->last_election_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.residence_country", + "default.special_country_dim.last_election_date", + ], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.date_dim.year[birth_country->formation_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.birth_country", + "default.special_country_dim.formation_date", + ], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.date_dim.year[birth_country->last_election_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.birth_country", + "default.special_country_dim.last_election_date", + ], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.date_dim.year[residence_country->formation_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.residence_country", + "default.special_country_dim.formation_date", + ], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.date_dim.year[residence_country->last_election_date]", + "node_display_name": "Date Dim", + "node_name": "default.date_dim", + "path": [ + "default.user_dim.residence_country", + "default.special_country_dim.last_election_date", + ], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.special_country_dim.country_code[birth_country]", + "node_display_name": "Special Country Dim", + "node_name": "default.special_country_dim", + "path": ["default.user_dim.birth_country"], + "type": "string", + "filter_only": False, + "properties": ["primary_key"], + }, + { + "name": "default.special_country_dim.country_code[residence_country]", + "node_display_name": "Special Country Dim", + "node_name": "default.special_country_dim", + "path": ["default.user_dim.residence_country"], + "type": "string", + "filter_only": False, + "properties": ["primary_key"], + }, + { + "name": "default.special_country_dim.formation_date[birth_country]", + "node_display_name": "Special Country Dim", + "node_name": "default.special_country_dim", + "path": ["default.user_dim.birth_country"], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.special_country_dim.formation_date[residence_country]", + "node_display_name": "Special Country Dim", + "node_name": "default.special_country_dim", + "path": ["default.user_dim.residence_country"], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.special_country_dim.last_election_date[birth_country]", + "node_display_name": "Special Country Dim", + "node_name": "default.special_country_dim", + "path": ["default.user_dim.birth_country"], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.special_country_dim.last_election_date[residence_country]", + "node_display_name": "Special Country Dim", + "node_name": "default.special_country_dim", + "path": ["default.user_dim.residence_country"], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.special_country_dim.name[birth_country]", + "node_display_name": "Special Country Dim", + "node_name": "default.special_country_dim", + "path": ["default.user_dim.birth_country"], + "type": "string", + "filter_only": False, + "properties": [], + }, + { + "name": "default.special_country_dim.name[residence_country]", + "node_display_name": "Special Country Dim", + "node_name": "default.special_country_dim", + "path": ["default.user_dim.residence_country"], + "type": "string", + "filter_only": False, + "properties": [], + }, + { + "name": "default.user_dim.age", + "node_display_name": "User Dim", + "node_name": "default.user_dim", + "path": [], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.user_dim.birth_country", + "node_display_name": "User Dim", + "node_name": "default.user_dim", + "path": [], + "type": "string", + "filter_only": False, + "properties": [], + }, + { + "name": "default.user_dim.residence_country", + "node_display_name": "User Dim", + "node_name": "default.user_dim", + "path": [], + "type": "string", + "filter_only": False, + "properties": [], + }, + { + "name": "default.user_dim.user_id", + "node_display_name": "User Dim", + "node_name": "default.user_dim", + "path": [], + "type": "int", + "filter_only": False, + "properties": ["primary_key"], + }, + ] + + +@pytest.mark.asyncio +async def test_type_inference_structs(module__client_with_roads: AsyncClient): + """ + Testing type resolution for structs select + """ + await module__client_with_roads.post( + "/nodes/source/", + json={ + "columns": [ + { + "name": "counts", + "type": "struct", + }, + ], + "description": "Collection of dreams", + "mode": "published", + "name": "basic.dreams_3", + "catalog": "public", + "schema_": "basic", + "table": "dreams", + }, + ) + + response = await module__client_with_roads.post( + "/nodes/metric/", + json={ + "query": "SELECT SUM(counts.b) FROM basic.dreams_3", + "description": "Dream Counts", + "mode": "published", + "name": "basic.dream_count", + }, + ) + response.json() + + +@pytest.mark.asyncio +async def test_metric_expression_auto_aliased(module__client_with_roads: AsyncClient): + """ + Testing that a metric's expression column is automatically aliased + """ + await module__client_with_roads.post("/namespaces/basic") + await module__client_with_roads.post( + "/nodes/source/", + json={ + "columns": [ + { + "name": "counts", + "type": "struct", + }, + ], + "description": "Collection of dreams", + "mode": "published", + "name": "basic.dreams_4", + "catalog": "public", + "schema_": "basic", + "table": "dreams", + }, + ) + + response = await module__client_with_roads.post( + "/nodes/metric/", + json={ + "query": "SELECT SUM(counts.b) + SUM(counts.b) FROM basic.dreams_4", + "description": "Dream Counts", + "mode": "published", + "name": "basic.dream_count_4", + }, + ) + assert response.status_code == 201 + data = response.json() + assert data["query"] == "SELECT SUM(counts.b) + SUM(counts.b) FROM basic.dreams_4" + assert data["columns"] == [ + { + "attributes": [], + "dimension": None, + "display_name": "Dream Count 4", + "name": "basic_DOT_dream_count_4", + "partition": None, + "type": "bigint", + }, + ] + + +@pytest.mark.asyncio +async def test_raise_on_malformated_expression_alias( + module__client_with_roads: AsyncClient, +): + """ + Test that using an invalid alias for a metric expression is saved, but the alias + is overridden when creating the column name + """ + await module__client_with_roads.post("/namespaces/basic") + response = await module__client_with_roads.post( + "/nodes/source/", + json={ + "columns": [ + { + "name": "counts", + "type": "struct", + }, + ], + "description": "Collection of dreams", + "mode": "published", + "name": "basic.dreams_5", + "catalog": "public", + "schema_": "basic", + "table": "dreams", + }, + ) + assert response.status_code == 200 + + response = await module__client_with_roads.post( + "/nodes/metric/", + json={ + "query": "SELECT SUM(counts.b) as foo FROM basic.dreams_5", + "description": "Dream Counts", + "mode": "published", + "name": "basic.dream_count_5", + }, + ) + assert response.status_code == 201 + data = response.json() + assert data["query"] == "SELECT SUM(counts.b) as foo FROM basic.dreams_5" + assert data["columns"][0]["name"] == "basic_DOT_dream_count_5" + + +@pytest.mark.asyncio +async def test_raise_on_multiple_expressions(module__client_with_roads: AsyncClient): + """ + Testing raising when there is more than one expression + """ + await module__client_with_roads.post("/namespaces/basic") + response = await module__client_with_roads.post( + "/nodes/source/", + json={ + "columns": [ + { + "name": "counts", + "type": "struct", + }, + ], + "description": "Collection of dreams", + "mode": "published", + "name": "basic.dreams_2", + "catalog": "public", + "schema_": "basic", + "table": "dreams", + }, + ) + assert response.status_code == 200 + + response = await module__client_with_roads.post( + "/nodes/metric/", + json={ + "query": "SELECT SUM(counts.b), COUNT(counts.b) FROM basic.dreams_2", + "description": "Dream Counts", + "mode": "published", + "name": "basic.dream_count_2", + }, + ) + assert response.status_code == 400 + assert ( + "Metric queries can only have a single expression, found 2" + ) in response.json()["message"] + + +@pytest.mark.asyncio +async def test_list_metric_metadata(module__client: AsyncClient): + """ + Test listing metric metadata values + """ + metric_metadata_options = (await module__client.get("/metrics/metadata")).json() + assert metric_metadata_options == { + "directions": ["higher_is_better", "lower_is_better", "neutral"], + "units": [ + { + "abbreviation": None, + "category": "", + "description": None, + "label": "Unknown", + "name": "unknown", + }, + { + "abbreviation": None, + "category": "", + "description": None, + "label": "Unitless", + "name": "unitless", + }, + { + "abbreviation": "%", + "category": "", + "description": "A ratio expressed as a number out of 100. Values " + "range from 0 to 100.", + "label": "Percentage", + "name": "percentage", + }, + { + "abbreviation": "", + "category": "", + "description": "A ratio that compares a part to a whole. Values " + "range from 0 to 1.", + "label": "Proportion", + "name": "proportion", + }, + { + "abbreviation": "$", + "category": "currency", + "description": None, + "label": "Dollar", + "name": "dollar", + }, + { + "abbreviation": "s", + "category": "time", + "description": None, + "label": "Second", + "name": "second", + }, + { + "abbreviation": "m", + "category": "time", + "description": None, + "label": "Minute", + "name": "minute", + }, + { + "abbreviation": "h", + "category": "time", + "description": None, + "label": "Hour", + "name": "hour", + }, + { + "abbreviation": "d", + "category": "time", + "description": None, + "label": "Day", + "name": "day", + }, + { + "abbreviation": "w", + "category": "time", + "description": None, + "label": "Week", + "name": "week", + }, + { + "abbreviation": "mo", + "category": "time", + "description": None, + "label": "Month", + "name": "month", + }, + { + "abbreviation": "y", + "category": "time", + "description": None, + "label": "Year", + "name": "year", + }, + ], + } + + +@pytest.mark.asyncio +async def test_create_invalid_metric(module__client_with_roads: AsyncClient): + """ + Validate that creating a metric with invalid SQL raises errors. + """ + response = await module__client_with_roads.post( + "/nodes/metric/", + json={ + "query": ( + "SELECT sum(total_repair_cost), " + "sum(total_repair_cost) FROM default.repair_orders_fact" + ), + "description": "Something invalid", + "mode": "published", + "name": "default.invalid_metric_example", + }, + ) + assert response.json()["message"] == ( + "Metric queries can only have a single expression, found 2" + ) + + response = await module__client_with_roads.post( + "/nodes/metric/", + json={ + "query": ( + "SELECT sum(total_repair_cost) FROM default.repair_orders_fact" + " WHERE total_repair_cost > 0" + ), + "description": "Something invalid", + "mode": "published", + "name": "default.invalid_metric_example", + }, + ) + assert response.json()["message"] == ( + "Metric cannot have a WHERE clause. Please use IF(, ...) instead" + ) + + response = await module__client_with_roads.post( + "/nodes/metric/", + json={ + "query": ( + "SELECT sum(total_repair_cost) FROM default.repair_orders_fact " + "GROUP BY total_repair_cost HAVING count(*) > 0 ORDER BY 1" + ), + "description": "Something invalid", + "mode": "published", + "name": "default.invalid_metric_example", + }, + ) + assert response.json()["message"] == ( + "Metric has an invalid query. The following are not allowed: GROUP BY, HAVING, ORDER BY" + ) diff --git a/datajunction-server/tests/api/namespaces_test.py b/datajunction-server/tests/api/namespaces_test.py new file mode 100644 index 000000000..d772a1de2 --- /dev/null +++ b/datajunction-server/tests/api/namespaces_test.py @@ -0,0 +1,837 @@ +""" +Tests for the namespaces API. +""" + +from unittest import mock + +import pytest +from httpx import AsyncClient + +from datajunction_server.api.main import app +from datajunction_server.internal.access.authorization import validate_access +from datajunction_server.models import access + + +@pytest.mark.asyncio +async def test_list_all_namespaces( + module__client_with_all_examples: AsyncClient, +) -> None: + """ + Test ``GET /namespaces/``. + """ + response = await module__client_with_all_examples.get("/namespaces/") + assert response.status_code in (200, 201) + assert response.json() == [ + {"namespace": "basic", "num_nodes": 8}, + {"namespace": "basic.dimension", "num_nodes": 2}, + {"namespace": "basic.source", "num_nodes": 2}, + {"namespace": "basic.transform", "num_nodes": 1}, + {"namespace": "dbt.dimension", "num_nodes": 1}, + {"namespace": "dbt.source", "num_nodes": 0}, + {"namespace": "dbt.source.jaffle_shop", "num_nodes": 2}, + {"namespace": "dbt.source.stripe", "num_nodes": 1}, + {"namespace": "dbt.transform", "num_nodes": 1}, + {"namespace": "default", "num_nodes": 64}, + { + "namespace": "different.basic", + "num_nodes": 2, + }, + { + "namespace": "different.basic.dimension", + "num_nodes": 2, + }, + { + "namespace": "different.basic.source", + "num_nodes": 2, + }, + { + "namespace": "different.basic.transform", + "num_nodes": 1, + }, + {"namespace": "foo.bar", "num_nodes": 26}, + ] + + +@pytest.mark.asyncio +async def test_list_nodes_by_namespace( + module__client_with_all_examples: AsyncClient, +) -> None: + """ + Test ``GET /namespaces/{namespace}/``. + """ + response = await module__client_with_all_examples.get("/namespaces/basic.source/") + assert response.status_code in (200, 201) + assert {n["name"] for n in response.json()} == { + "basic.source.users", + "basic.source.comments", + } + + response = await module__client_with_all_examples.get( + "/namespaces/basic/?with_edited_by=true", + ) + assert response.status_code in (200, 201) + assert {n["name"] for n in response.json()} == { + "basic.avg_luminosity_patches", + "basic.corrected_patches", + "basic.source.users", + "basic.dimension.users", + "basic.murals", + "basic.source.comments", + "basic.dimension.countries", + "basic.transform.country_agg", + "basic.num_comments", + "basic.num_users", + "basic.paint_colors_spark", + "basic.paint_colors_trino", + "basic.patches", + } + countries_dim = [ + n for n in response.json() if n["name"] == "basic.dimension.countries" + ][0] + assert countries_dim == { + "description": "Country dimension", + "display_name": "Countries", + "edited_by": [ + "dj", + ], + "mode": "published", + "name": "basic.dimension.countries", + "status": "valid", + "tags": [], + "type": "dimension", + "updated_at": mock.ANY, + "version": "v1.0", + } + + response = await module__client_with_all_examples.get( + "/namespaces/basic/?type_=dimension&with_edited_by=false", + ) + countries_dim = [ + n for n in response.json() if n["name"] == "basic.dimension.countries" + ][0] + assert countries_dim["edited_by"] is None + + response = await module__client_with_all_examples.get( + "/namespaces/basic/?type_=dimension", + ) + assert response.status_code in (200, 201) + assert {n["name"] for n in response.json()} == { + "basic.dimension.users", + "basic.dimension.countries", + "basic.paint_colors_trino", + "basic.paint_colors_spark", + } + + response = await module__client_with_all_examples.get( + "/namespaces/basic/?type_=source", + ) + assert response.status_code in (200, 201) + assert {n["name"] for n in response.json()} == { + "basic.source.comments", + "basic.source.users", + "basic.murals", + "basic.patches", + } + + +@pytest.mark.asyncio +async def test_deactivate_namespaces(client_with_namespaced_roads: AsyncClient) -> None: + """ + Test ``DELETE /namespaces/{namespace}``. + """ + # Cannot deactivate if there are nodes under the namespace + response = await client_with_namespaced_roads.delete( + "/namespaces/foo.bar/?cascade=false", + ) + assert response.json() == { + "message": "Cannot deactivate node namespace `foo.bar` as there are still " + "active nodes under that namespace.", + } + + # Can deactivate with cascade + response = await client_with_namespaced_roads.delete( + "/namespaces/foo.bar/?cascade=true", + ) + message = response.json()["message"] + assert ( + "Namespace `foo.bar` has been deactivated. The following nodes " + "have also been deactivated:" + ) in message + nodes = [ + "foo.bar.avg_time_to_dispatch", + "foo.bar.avg_repair_order_discounts", + "foo.bar.total_repair_order_discounts", + "foo.bar.avg_length_of_employment", + "foo.bar.total_repair_cost", + "foo.bar.avg_repair_price", + "foo.bar.num_repair_orders", + "foo.bar.municipality_dim", + "foo.bar.dispatcher", + "foo.bar.us_state", + "foo.bar.local_hard_hats", + "foo.bar.hard_hat", + "foo.bar.contractor", + "foo.bar.repair_order", + "foo.bar.us_region", + "foo.bar.us_states", + "foo.bar.hard_hat_state", + "foo.bar.hard_hats", + "foo.bar.dispatchers", + "foo.bar.municipality", + "foo.bar.municipality_type", + "foo.bar.municipality_municipality_type", + "foo.bar.contractors", + "foo.bar.repair_type", + "foo.bar.repair_order_details", + "foo.bar.repair_orders", + ] + for node in nodes: + assert node in message + + # Check that the namespace is no longer listed + response = await client_with_namespaced_roads.get("/namespaces/") + assert response.status_code in (200, 201) + assert "foo.bar" not in {n["namespace"] for n in response.json()} + + response = await client_with_namespaced_roads.delete( + "/namespaces/foo.bar/?cascade=false", + ) + assert response.json()["message"] == "Namespace `foo.bar` is already deactivated." + + # Try restoring + response = await client_with_namespaced_roads.post("/namespaces/foo.bar/restore/") + assert response.json() == { + "message": "Namespace `foo.bar` has been restored.", + } + + # Check that the namespace is back + response = await client_with_namespaced_roads.get("/namespaces/") + assert response.status_code in (200, 201) + assert "foo.bar" in {n["namespace"] for n in response.json()} + + # Check that nodes in the namespace remain deactivated + response = await client_with_namespaced_roads.get("/namespaces/foo.bar/") + assert response.status_code in (200, 201) + assert response.json() == [] + + # Restore with cascade=true should also restore all the nodes + await client_with_namespaced_roads.delete("/namespaces/foo.bar/?cascade=false") + response = await client_with_namespaced_roads.post( + "/namespaces/foo.bar/restore/?cascade=true", + ) + message = response.json()["message"] + assert ( + "Namespace `foo.bar` has been restored. The following nodes have " + "also been restored:" + ) in message + for node in nodes: + assert node in message + + # Calling restore again will raise + response = await client_with_namespaced_roads.post( + "/namespaces/foo.bar/restore/?cascade=true", + ) + assert ( + response.json()["message"] + == "Node namespace `foo.bar` already exists and is active." + ) + + # Check that nodes in the namespace are restored + response = await client_with_namespaced_roads.get("/namespaces/foo.bar/") + assert response.status_code in (200, 201) + assert {n["name"] for n in response.json()} == { + "foo.bar.repair_orders", + "foo.bar.repair_order_details", + "foo.bar.repair_type", + "foo.bar.contractors", + "foo.bar.municipality_municipality_type", + "foo.bar.municipality_type", + "foo.bar.municipality", + "foo.bar.dispatchers", + "foo.bar.hard_hats", + "foo.bar.hard_hat_state", + "foo.bar.us_states", + "foo.bar.us_region", + "foo.bar.repair_order", + "foo.bar.contractor", + "foo.bar.hard_hat", + "foo.bar.local_hard_hats", + "foo.bar.us_state", + "foo.bar.dispatcher", + "foo.bar.municipality_dim", + "foo.bar.num_repair_orders", + "foo.bar.avg_repair_price", + "foo.bar.total_repair_cost", + "foo.bar.avg_length_of_employment", + "foo.bar.total_repair_order_discounts", + "foo.bar.avg_repair_order_discounts", + "foo.bar.avg_time_to_dispatch", + } + + response = await client_with_namespaced_roads.get("/history/namespace/foo.bar/") + assert [ + (activity["activity_type"], activity["details"]) for activity in response.json() + ] == [ + ( + "restore", + { + "message": mock.ANY, + }, + ), + ("delete", {"message": "Namespace `foo.bar` has been deactivated."}), + ("restore", {"message": "Namespace `foo.bar` has been restored."}), + ( + "delete", + { + "message": mock.ANY, + }, + ), + ("create", {}), + ] + + response = await client_with_namespaced_roads.get( + "/history?node=foo.bar.avg_length_of_employment", + ) + assert [ + (activity["activity_type"], activity["details"]) for activity in response.json() + ] == [ + ("restore", {"message": "Cascaded from restoring namespace `foo.bar`"}), + ("status_change", {"upstream_node": "foo.bar.hard_hats"}), + ("status_change", {"upstream_node": "foo.bar.hard_hats"}), + ("delete", {"message": "Cascaded from deactivating namespace `foo.bar`"}), + ("create", {}), + ] + + +@pytest.mark.asyncio +async def test_hard_delete_namespace(client_with_namespaced_roads: AsyncClient): + """ + Test hard deleting a namespace + """ + response = await client_with_namespaced_roads.delete("/namespaces/foo/hard/") + assert response.json()["message"] == ( + "Cannot hard delete namespace `foo` as there are still the following nodes " + "under it: `['foo.bar.avg_length_of_employment', " + "'foo.bar.avg_repair_order_discounts', 'foo.bar.avg_repair_price', " + "'foo.bar.avg_time_to_dispatch', 'foo.bar.contractor', 'foo.bar.contractors', " + "'foo.bar.dispatcher', 'foo.bar.dispatchers', 'foo.bar.hard_hat', " + "'foo.bar.hard_hats', 'foo.bar.hard_hat_state', 'foo.bar.local_hard_hats', " + "'foo.bar.municipality', 'foo.bar.municipality_dim', " + "'foo.bar.municipality_municipality_type', 'foo.bar.municipality_type', " + "'foo.bar.num_repair_orders', 'foo.bar.repair_order', " + "'foo.bar.repair_order_details', 'foo.bar.repair_orders', " + "'foo.bar.repair_type', 'foo.bar.total_repair_cost', " + "'foo.bar.total_repair_order_discounts', 'foo.bar.us_region', " + "'foo.bar.us_state', 'foo.bar.us_states']`. Set `cascade` to true to " + "additionally hard delete the above nodes in this namespace. WARNING: this " + "action cannot be undone." + ) + + await client_with_namespaced_roads.post("/namespaces/foo/") + await client_with_namespaced_roads.post("/namespaces/foo.bar.baz/") + await client_with_namespaced_roads.post("/namespaces/foo.bar.baf/") + await client_with_namespaced_roads.post("/namespaces/foo.bar.bif.d/") + + # Deactivating a few nodes should still allow the hard delete to go through + await client_with_namespaced_roads.delete( + "/nodes/foo.bar.avg_length_of_employment", + ) + await client_with_namespaced_roads.delete( + "/nodes/foo.bar.avg_repair_order_discounts", + ) + + hard_delete_response = await client_with_namespaced_roads.delete( + "/namespaces/foo.bar/hard/?cascade=true", + ) + assert hard_delete_response.json() == { + "impact": { + "foo.bar": {"namespace": "foo.bar", "status": "deleted"}, + "foo.bar.avg_length_of_employment": [], + "foo.bar.avg_repair_order_discounts": [], + "foo.bar.avg_repair_price": [], + "foo.bar.avg_time_to_dispatch": [], + "foo.bar.baf": {"namespace": "foo.bar.baf", "status": "deleted"}, + "foo.bar.baz": {"namespace": "foo.bar.baz", "status": "deleted"}, + "foo.bar.bif.d": {"namespace": "foo.bar.bif.d", "status": "deleted"}, + "foo.bar.contractor": [ + { + "effect": "broken link", + "name": "foo.bar.repair_type", + "status": "valid", + }, + ], + "foo.bar.contractors": [], + "foo.bar.dispatcher": [ + { + "effect": "broken link", + "name": "foo.bar.repair_orders", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.repair_order_details", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.num_repair_orders", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.total_repair_cost", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.total_repair_order_discounts", + "status": "valid", + }, + ], + "foo.bar.dispatchers": [], + "foo.bar.hard_hat": [ + { + "effect": "broken link", + "name": "foo.bar.repair_orders", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.repair_order_details", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.num_repair_orders", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.total_repair_cost", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.total_repair_order_discounts", + "status": "valid", + }, + ], + "foo.bar.hard_hat_state": [ + { + "effect": "downstream node is now invalid", + "name": "foo.bar.local_hard_hats", + "status": "invalid", + }, + ], + "foo.bar.hard_hats": [ + { + "effect": "downstream node is now invalid", + "name": "foo.bar.local_hard_hats", + "status": "invalid", + }, + ], + "foo.bar.local_hard_hats": [], + "foo.bar.municipality": [ + { + "effect": "downstream node is now invalid", + "name": "foo.bar.municipality_dim", + "status": "invalid", + }, + ], + "foo.bar.municipality_dim": [ + { + "effect": "broken link", + "name": "foo.bar.repair_orders", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.repair_order_details", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.num_repair_orders", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.total_repair_cost", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.total_repair_order_discounts", + "status": "valid", + }, + ], + "foo.bar.municipality_municipality_type": [], + "foo.bar.municipality_type": [], + "foo.bar.num_repair_orders": [], + "foo.bar.repair_order": [ + { + "effect": "broken link", + "name": "foo.bar.total_repair_order_discounts", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.repair_orders", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.repair_order_details", + "status": "valid", + }, + { + "effect": "broken link", + "name": "foo.bar.total_repair_cost", + "status": "valid", + }, + ], + "foo.bar.repair_order_details": [ + { + "effect": "downstream node is now invalid", + "name": "foo.bar.total_repair_cost", + "status": "invalid", + }, + { + "effect": "downstream node is now invalid", + "name": "foo.bar.total_repair_order_discounts", + "status": "invalid", + }, + ], + "foo.bar.repair_orders": [], + "foo.bar.repair_type": [], + "foo.bar.total_repair_cost": [], + "foo.bar.total_repair_order_discounts": [], + "foo.bar.us_region": [ + { + "effect": "downstream node is now invalid", + "name": "foo.bar.us_state", + "status": "invalid", + }, + ], + "foo.bar.us_state": [], + "foo.bar.us_states": [], + }, + "message": "The namespace `foo.bar` has been completely removed.", + } + list_namespaces_response = await client_with_namespaced_roads.get( + "/namespaces/", + ) + assert list_namespaces_response.json() == [ + {"namespace": "basic", "num_nodes": 0}, + {"namespace": "default", "num_nodes": 0}, + {"namespace": "foo", "num_nodes": 0}, + ] + + response = await client_with_namespaced_roads.delete( + "/namespaces/jaffle_shop/hard/?cascade=true", + ) + assert response.json() == { + "errors": [], + "message": "Namespace `jaffle_shop` does not exist.", + "warnings": [], + } + + +@pytest.mark.asyncio +async def test_create_namespace(client_with_service_setup: AsyncClient): + """ + Verify creating namespaces, both successful and validation errors + """ + # By default, creating a namespace will also create its parents (i.e., like mkdir -p) + response = await client_with_service_setup.post( + "/namespaces/aaa.bbb.ccc?include_parents=true", + ) + assert response.json() == { + "message": "The following node namespaces have been successfully created: " + "aaa, aaa.bbb, aaa.bbb.ccc", + } + + # Verify that the parent namespaces already exist if we try to create it again + response = await client_with_service_setup.post("/namespaces/aaa") + assert response.json() == {"message": "Node namespace `aaa` already exists"} + response = await client_with_service_setup.post("/namespaces/aaa.bbb") + assert response.json() == {"message": "Node namespace `aaa.bbb` already exists"} + + # Setting include_parents=false will not create the parents + response = await client_with_service_setup.post( + "/namespaces/acde.mmm?include_parents=false", + ) + assert response.json() == { + "message": "The following node namespaces have been successfully created: acde.mmm", + } + response = await client_with_service_setup.get("/namespaces/acde") + assert response.json()["message"] == "node namespace `acde` does not exist." + + # Setting include_parents=true will create the parents + response = await client_with_service_setup.post( + "/namespaces/a.b.c?include_parents=true", + ) + assert response.json() == { + "message": "The following node namespaces have been successfully created: a, a.b, a.b.c", + } + + # Verify that it raises when creating an invalid namespace + invalid_namespaces = [ + "a.111b.c", + "111mm.abcd", + "aa.bb.111", + "1234", + "aa..bb", + "user.abc", + "[aff].mmm", + "aff._mmm", + "aff.mmm+", + "aff.123_mmm", + ] + for invalid_namespace in invalid_namespaces: + response = await client_with_service_setup.post( + f"/namespaces/{invalid_namespace}", + ) + assert response.status_code == 422 + assert response.json()["message"] == ( + f"{invalid_namespace} is not a valid namespace. Namespace parts cannot start " + "with numbers, be empty, or use the reserved keyword [user]" + ) + + +@pytest.mark.asyncio +async def test_export_namespaces(client_with_roads: AsyncClient): + """ + Test exporting a namespace to a project definition + """ + # Create a cube so that the cube definition export path is tested + response = await client_with_roads.post( + "/nodes/cube/", + json={ + "name": "default.example_cube", + "display_name": "Example Cube", + "description": "An example cube so that the export path is tested", + "metrics": ["default.num_repair_orders"], + "dimensions": ["default.hard_hat.city", "default.hard_hat.hire_date"], + "mode": "published", + }, + ) + assert response.status_code in (200, 201) + + # Mark a column as a dimension attribute + response = await client_with_roads.post( + "/nodes/default.regional_level_agg/columns/location_hierarchy/attributes", + json=[ + { + "name": "dimension", + "namespace": "system", + }, + ], + ) + assert response.status_code in (200, 201) + + # Mark a column as a partition + await client_with_roads.post( + "/nodes/default.example_cube/columns/default.hard_hat.hire_date/partition", + json={ + "type_": "temporal", + "granularity": "day", + "format": "yyyyMMdd", + }, + ) + + response = await client_with_roads.get( + "/namespaces/default/export/", + ) + project_definition = response.json() + + # Check that nodes are topologically sorted + sorted_nodes = [entity["build_name"] for entity in project_definition] + assert sorted_nodes[-1] == "example_cube" + + node_defs = {d["filename"]: d for d in project_definition} + assert node_defs["example_cube.cube.yaml"] == { + "build_name": "example_cube", + "columns": [ + { + "name": "default.hard_hat.hire_date", + "partition": { + "format": "yyyyMMdd", + "granularity": "day", + "type_": "temporal", + }, + }, + ], + "description": "An example cube so that the export path is tested", + "dimensions": ["default.hard_hat.hire_date", "default.hard_hat.city"], + "directory": "", + "display_name": "Example Cube", + "filename": "example_cube.cube.yaml", + "metrics": ["default.num_repair_orders"], + "tags": [], + } + assert node_defs["repair_orders_fact.transform.yaml"]["dimension_links"] == [ + { + "dimension_node": "default.municipality_dim", + "join_on": "default.repair_orders_fact.municipality_id = " + "default.municipality_dim.municipality_id", + "join_type": "inner", + "type": "join", + }, + { + "dimension_node": "default.hard_hat", + "join_on": "default.repair_orders_fact.hard_hat_id = default.hard_hat.hard_hat_id", + "join_type": "inner", + "type": "join", + }, + { + "dimension_node": "default.hard_hat_to_delete", + "join_on": "default.repair_orders_fact.hard_hat_id = " + "default.hard_hat_to_delete.hard_hat_id", + "join_type": "left", + "type": "join", + }, + { + "dimension_node": "default.dispatcher", + "join_on": "default.repair_orders_fact.dispatcher_id = " + "default.dispatcher.dispatcher_id", + "join_type": "inner", + "type": "join", + }, + ] + + assert set(node_defs.keys()) == { + "avg_length_of_employment.metric.yaml", + "avg_repair_order_discounts.metric.yaml", + "avg_repair_price.metric.yaml", + "avg_time_to_dispatch.metric.yaml", + "contractor.dimension.yaml", + "contractors.source.yaml", + "discounted_orders_rate.metric.yaml", + "dispatcher.dimension.yaml", + "dispatchers.source.yaml", + "example_cube.cube.yaml", + "hard_hat.dimension.yaml", + "hard_hat_2.dimension.yaml", + "hard_hat_state.source.yaml", + "hard_hat_to_delete.dimension.yaml", + "hard_hats.source.yaml", + "local_hard_hats.dimension.yaml", + "local_hard_hats_1.dimension.yaml", + "local_hard_hats_2.dimension.yaml", + "municipality.source.yaml", + "municipality_dim.dimension.yaml", + "municipality_municipality_type.source.yaml", + "municipality_type.source.yaml", + "national_level_agg.transform.yaml", + "num_repair_orders.metric.yaml", + "regional_level_agg.transform.yaml", + "regional_repair_efficiency.metric.yaml", + "repair_order.dimension.yaml", + "repair_order_details.source.yaml", + "repair_orders.source.yaml", + "repair_orders_fact.transform.yaml", + "repair_type.source.yaml", + "total_repair_cost.metric.yaml", + "total_repair_order_discounts.metric.yaml", + "us_region.source.yaml", + "us_state.dimension.yaml", + "us_states.source.yaml", + "repair_orders_view.source.yaml", + } + assert {d["directory"] for d in project_definition} == {""} + + +@pytest.mark.asyncio +async def test_list_all_namespaces_access_limited( + client_with_dbt: AsyncClient, +) -> None: + """ + Test ``GET /namespaces/``. + """ + + def validate_access_override(): + def _validate_access(access_control: access.AccessControl): + for request in access_control.requests: + if ( + request.access_object.resource_type == access.ResourceType.NAMESPACE + and "dbt" in request.access_object.name + ): + request.approve() + else: + request.deny() + + return _validate_access + + app.dependency_overrides[validate_access] = validate_access_override + + response = await client_with_dbt.get("/namespaces/") + + assert response.status_code in (200, 201) + assert response.json() == [ + {"namespace": "dbt.dimension", "num_nodes": 1}, + {"namespace": "dbt.source", "num_nodes": 0}, + {"namespace": "dbt.source.jaffle_shop", "num_nodes": 2}, + {"namespace": "dbt.source.stripe", "num_nodes": 1}, + {"namespace": "dbt.transform", "num_nodes": 1}, + ] + app.dependency_overrides.clear() + + +@pytest.mark.asyncio +async def test_list_all_namespaces_access_bad_injection( + client_with_service_setup: AsyncClient, +) -> None: + """ + Test ``GET /namespaces/``. + """ + + def validate_access_override(): + def _validate_access(access_control: access.AccessControl): + for i, request in enumerate(access_control.requests): + if i != 0: + request.approve() + + return _validate_access + + app.dependency_overrides[validate_access] = validate_access_override + + response = await client_with_service_setup.get("/namespaces/") + + assert response.status_code == 403 + assert response.json() == { + "message": "Injected `validate_access` must approve or deny all requests.", + "errors": [ + { + "code": 501, + "message": "Injected `validate_access` must approve or deny all requests.", + "debug": None, + "context": "", + }, + ], + "warnings": [], + } + app.dependency_overrides.clear() + + +@pytest.mark.asyncio +async def test_list_all_namespaces_deny_all( + client_with_service_setup: AsyncClient, +) -> None: + """ + Test ``GET /namespaces/``. + """ + + def validate_access_override(): + def _validate_access(access_control: access.AccessControl): + access_control.deny_all() + + return _validate_access + + app.dependency_overrides[validate_access] = validate_access_override + + response = await client_with_service_setup.get("/namespaces/") + + assert response.status_code in (200, 201) + assert response.json() == [] + app.dependency_overrides.clear() diff --git a/datajunction-server/tests/api/nodes_test.py b/datajunction-server/tests/api/nodes_test.py new file mode 100644 index 000000000..14c3ab7ab --- /dev/null +++ b/datajunction-server/tests/api/nodes_test.py @@ -0,0 +1,6029 @@ +""" +Tests for the nodes API. +""" + +import re +from typing import Any, Dict +from unittest import mock +from uuid import uuid4 + +import pytest +import pytest_asyncio +from httpx import AsyncClient +from pytest_mock import MockerFixture +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database import Catalog +from datajunction_server.database.column import Column +from datajunction_server.database.node import Node, NodeRelationship, NodeRevision +from datajunction_server.database.queryrequest import QueryBuildType, QueryRequest +from datajunction_server.database.user import OAuthProvider, User +from datajunction_server.errors import DJDoesNotExistException +from datajunction_server.internal.materializations import decompose_expression +from datajunction_server.models.node import NodeStatus +from datajunction_server.models.node_type import NodeType +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.sql.dag import get_upstream_nodes +from datajunction_server.sql.parsing import ast, types +from datajunction_server.sql.parsing.backends.antlr4 import parse +from datajunction_server.sql.parsing.types import IntegerType, StringType, TimestampType +from tests.sql.utils import compare_query_strings + + +def materialization_compare(response, expected): + """Compares two materialization lists of json + configs paying special attention to query comparison""" + for materialization_response, materialization_expected in zip(response, expected): + assert compare_query_strings( + materialization_response["config"]["query"], + materialization_expected["config"]["query"], + ) + del materialization_response["config"]["query"] + del materialization_expected["config"]["query"] + assert materialization_response == materialization_expected + + +@pytest.mark.asyncio +async def test_read_node(client_with_roads: AsyncClient) -> None: + """ + Test ``GET /nodes/{node_id}``. + """ + response = await client_with_roads.get("/nodes/default.repair_orders/") + data = response.json() + + assert response.status_code == 200 + assert data["version"] == "v1.0" + assert data["node_id"] == 1 + assert data["node_revision_id"] == 1 + assert data["type"] == "source" + + response = await client_with_roads.get("/nodes/default.nothing/") + data = response.json() + + assert response.status_code == 404 + assert data["message"] == "A node with name `default.nothing` does not exist." + + # Check that getting nodes via prefixes works + response = await client_with_roads.get("/nodes/?prefix=default.ha") + data = response.json() + assert set(data) == { + "default.hard_hats", + "default.hard_hat_state", + "default.hard_hat_to_delete", + "default.hard_hat", + "default.hard_hat_2", + } + + +@pytest.mark.asyncio +async def test_read_nodes( + session: AsyncSession, + client: AsyncClient, + current_user: User, +) -> None: + """ + Test ``GET /nodes/``. + """ + node1 = Node( + name="not-a-metric", + type=NodeType.SOURCE, + current_version="1", + created_by_id=current_user.id, + ) + node_rev1 = NodeRevision( + node=node1, + version="1", + name=node1.name, + type=node1.type, + created_by_id=current_user.id, + ) + node2 = Node( + name="also-not-a-metric", + type=NodeType.TRANSFORM, + current_version="1", + created_by_id=current_user.id, + ) + node_rev2 = NodeRevision( + name=node2.name, + node=node2, + version="1", + query="SELECT 42 AS answer", + type=node2.type, + columns=[ + Column(name="answer", type=IntegerType(), order=0), + ], + created_by_id=current_user.id, + ) + node3 = Node( + name="a-metric", + type=NodeType.METRIC, + current_version="1", + created_by_id=current_user.id, + ) + node_rev3 = NodeRevision( + name=node3.name, + node=node3, + version="1", + query="SELECT COUNT(*) FROM my_table", + columns=[ + Column(name="_col0", type=IntegerType(), order=0), + ], + type=node3.type, + created_by_id=current_user.id, + ) + session.add(node_rev1) + session.add(node_rev2) + session.add(node_rev3) + await session.commit() + + response = await client.get("/nodes/") + data = response.json() + + assert response.status_code == 200 + assert len(data) == 3 + assert set(data) == {"not-a-metric", "also-not-a-metric", "a-metric"} + + response = await client.get("/nodes?node_type=metric") + data = response.json() + + assert response.status_code == 200 + assert len(data) == 1 + assert set(data) == {"a-metric"} + + +@pytest.mark.asyncio +async def test_get_nodes_with_details(client_with_examples: AsyncClient): + """ + Test getting all nodes with some details + """ + response = await client_with_examples.get("/nodes/details/") + assert response.status_code in (200, 201) + data = response.json() + assert {d["name"] for d in data} == { + "different.basic.dimension.countries", + "different.basic.dimension.users", + "different.basic.num_comments", + "different.basic.num_users", + "different.basic.source.comments", + "different.basic.source.users", + "different.basic.transform.country_agg", + "default.country_dim", + "foo.bar.us_state", + "basic.paint_colors_trino", + "foo.bar.us_region", + "default.avg_repair_order_discounts", + "foo.bar.hard_hats", + "foo.bar.dispatchers", + "foo.bar.us_states", + "default.sales", + "basic.patches", + "foo.bar.local_hard_hats", + "default.date_dim", + "foo.bar.hard_hat", + "default.long_events_distinct_countries", + "default.repair_type", + "default.total_repair_order_discounts", + "default.total_repair_cost", + "basic.source.comments", + "foo.bar.municipality_type", + "default.large_revenue_payments_and_business_only", + "default.large_revenue_payments_and_business_only_1", + "default.payment_type_table", + "default.local_hard_hats", + "default.local_hard_hats_1", + "default.local_hard_hats_2", + "default.dispatcher", + "foo.bar.repair_orders", + "basic.transform.country_agg", + "foo.bar.hard_hat_state", + "foo.bar.municipality_dim", + "foo.bar.repair_order_details", + "foo.bar.dispatcher", + "default.dispatchers", + "dbt.source.stripe.payments", + "default.national_level_agg", + "default.us_region", + "default.repair_order_details", + "default.contractor", + "foo.bar.total_repair_order_discounts", + "default.repair_orders", + "default.repair_orders_view", + "basic.paint_colors_spark", + "default.long_events", + "default.items", + "default.special_country_dim", + "default.avg_user_age", + "foo.bar.contractor", + "basic.avg_luminosity_patches", + "default.countries", + "default.discounted_orders_rate", + "default.municipality_municipality_type", + "default.user_dim", + "basic.corrected_patches", + "basic.num_users", + "default.regional_level_agg", + "default.revenue", + "foo.bar.contractors", + "foo.bar.avg_repair_order_discounts", + "foo.bar.municipality", + "dbt.source.jaffle_shop.customers", + "foo.bar.repair_order", + "default.account_type", + "foo.bar.avg_time_to_dispatch", + "basic.dimension.users", + "dbt.dimension.customers", + "basic.num_comments", + "default.us_state", + "default.hard_hats", + "default.items_sold_count", + "default.users", + "default.avg_repair_price", + "basic.murals", + "default.avg_length_of_employment", + "default.municipality_type", + "default.hard_hat_state", + "default.hard_hat_to_delete", + "default.num_repair_orders", + "basic.source.users", + "default.date", + "default.us_states", + "foo.bar.total_repair_cost", + "default.device_ids_count", + "dbt.source.jaffle_shop.orders", + "dbt.transform.customer_agg", + "default.regional_repair_efficiency", + "foo.bar.num_repair_orders", + "default.hard_hat", + "default.hard_hat_2", + "foo.bar.municipality_municipality_type", + "basic.dimension.countries", + "default.number_of_account_types", + "default.municipality", + "default.payment_type", + "default.municipality_dim", + "default.contractors", + "default.total_profit", + "default.account_type_table", + "default.repair_order", + "foo.bar.avg_length_of_employment", + "foo.bar.avg_repair_price", + "default.avg_time_to_dispatch", + "default.event_source", + "foo.bar.repair_type", + "default.large_revenue_payments_only", + "default.large_revenue_payments_only_1", + "default.large_revenue_payments_only_2", + "default.large_revenue_payments_only_custom", + "default.repair_orders_fact", + } + + +class TestNodeCRUD: + """ + Test node CRUD + """ + + @pytest.fixture + def create_dimension_node_payload(self) -> Dict[str, Any]: + """ + Payload for creating a dimension node. + """ + + return { + "description": "Country dimension", + "query": "SELECT country, COUNT(1) AS user_cnt " + "FROM basic.source.users GROUP BY country", + "mode": "published", + "name": "default.countries", + "primary_key": ["country"], + } + + @pytest.fixture + def create_invalid_transform_node_payload(self) -> Dict[str, Any]: + """ + Payload for creating a transform node. + """ + + return { + "name": "default.country_agg", + "query": "SELECT country, COUNT(DISTINCT id) AS num_users FROM comments", + "mode": "published", + "description": "Distinct users per country", + "columns": [ + {"name": "country", "type": "string"}, + {"name": "num_users", "type": "int"}, + ], + } + + @pytest.fixture + def create_transform_node_payload(self) -> Dict[str, Any]: + """ + Payload for creating a transform node. + """ + + return { + "name": "default.country_agg", + "query": "SELECT country, COUNT(DISTINCT id) AS num_users FROM basic.source.users", + "mode": "published", + "description": "Distinct users per country", + "columns": [ + {"name": "country", "type": "string"}, + {"name": "num_users", "type": "int"}, + ], + } + + @pytest_asyncio.fixture + async def catalog(self, session: AsyncSession) -> Catalog: + """ + A database fixture. + """ + + catalog = Catalog(name="prod", uuid=uuid4()) + session.add(catalog) + await session.commit() + return catalog + + @pytest_asyncio.fixture + async def current_user(self, session: AsyncSession) -> User: + """ + A user fixture. + """ + + new_user = User( + username="datajunction", + password="datajunction", + email="dj@datajunction.io", + name="DJ", + oauth_provider=OAuthProvider.BASIC, + is_admin=False, + ) + existing_user = await session.get(User, new_user.id) + if not existing_user: + session.add(new_user) + await session.commit() + user = new_user + else: + user = existing_user + return user + + @pytest_asyncio.fixture + async def source_node(self, session: AsyncSession, current_user: User) -> Node: + """ + A source node fixture. + """ + node = Node( + name="basic.source.users", + type=NodeType.SOURCE, + current_version="v1", + created_by_id=current_user.id, + ) + node_revision = NodeRevision( + node=node, + name=node.name, + catalog_id=1, + type=node.type, + version="v1", + columns=[ + Column(name="id", type=IntegerType(), order=0), + Column(name="full_name", type=StringType(), order=1), + Column(name="age", type=IntegerType(), order=2), + Column(name="country", type=StringType(), order=3), + Column(name="gender", type=StringType(), order=4), + Column(name="preferred_language", type=StringType(), order=5), + ], + created_by_id=current_user.id, + ) + session.add(node_revision) + await session.commit() + return node + + @pytest.mark.asyncio + async def test_create_dimension_without_catalog( + self, + client_with_roads: AsyncClient, + ): + """ + Test that creating a dimension that's purely query-based and therefore + doesn't reference a catalog works. + """ + response = await client_with_roads.post( + "/nodes/dimension/", + json={ + "description": "Title", + "query": ( + "SELECT 0 AS title_code, 'Agha' AS title " + "UNION ALL SELECT 1, 'Abbot' " + "UNION ALL SELECT 2, 'Akhoond' " + "UNION ALL SELECT 3, 'Apostle'" + ), + "mode": "published", + "name": "default.title", + "primary_key": ["title"], + }, + ) + assert response.json()["columns"] == [ + { + "attributes": [], + "dimension": None, + "display_name": "Title Code", + "name": "title_code", + "type": "int", + "partition": None, + }, + { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "Title", + "name": "title", + "type": "string", + "partition": None, + }, + ] + + # Link the dimension to a column on the source node + response = await client_with_roads.post( + "/nodes/default.hard_hats/columns/title/" + "?dimension=default.title&dimension_column=title", + ) + assert response.status_code in (200, 201) + response = await client_with_roads.get("/nodes/default.hard_hats/") + assert { + "attributes": [], + "dimension": None, + "display_name": "Title", + "name": "title", + "type": "string", + "partition": None, + } in response.json()["columns"] + + assert response.json()["dimension_links"] == [ + { + "dimension": {"name": "default.title"}, + "foreign_keys": {"default.hard_hats.title": "default.title.title"}, + "join_cardinality": "many_to_one", + "join_sql": "default.hard_hats.title = default.title.title", + "join_type": "left", + "role": None, + }, + ] + + @pytest.mark.asyncio + async def test_deleting_node( + self, + client_with_basic: AsyncClient, + ): + """ + Test deleting a node + """ + # Delete a node + response = await client_with_basic.delete("/nodes/basic.source.users/") + assert response.status_code == 200 + # Check that then retrieving the node returns an error + response = await client_with_basic.get("/nodes/basic.source.users/") + assert response.status_code >= 400 + assert response.json() == { + "message": "A node with name `basic.source.users` does not exist.", + "errors": [], + "warnings": [], + } + # All downstream nodes should be invalid + expected_downstreams = [ + "basic.dimension.users", + "basic.transform.country_agg", + "basic.dimension.countries", + "basic.num_users", + ] + for downstream in expected_downstreams: + response = await client_with_basic.get(f"/nodes/{downstream}/") + assert response.json()["status"] == NodeStatus.INVALID + + # The downstreams' status change should be recorded in their histories + response = await client_with_basic.get(f"/history?node={downstream}") + assert [ + (activity["pre"], activity["post"], activity["details"]) + for activity in response.json() + if activity["activity_type"] == "status_change" + ] == [ + ( + {"status": "valid"}, + {"status": "invalid"}, + {"upstream_node": "basic.source.users"}, + ), + ] + + # Trying to create the node again should work. + response = await client_with_basic.post( + "/nodes/source/", + json={ + "name": "basic.source.users", + "description": "A user table", + "columns": [ + {"name": "id", "type": "int"}, + {"name": "full_name", "type": "string"}, + {"name": "age", "type": "int"}, + {"name": "country", "type": "string"}, + {"name": "gender", "type": "string"}, + {"name": "preferred_language", "type": "string"}, + {"name": "secret_number", "type": "float"}, + {"name": "created_at", "type": "timestamp"}, + {"name": "post_processing_timestamp", "type": "timestamp"}, + ], + "mode": "published", + "catalog": "public", + "schema_": "basic", + "table": "dim_users", + }, + ) + assert response.status_code in (200, 201) + + # The deletion action should be recorded in the node's history + response = await client_with_basic.get("/history?node=basic.source.users") + history = response.json() + assert history == [ + { + "id": mock.ANY, + "entity_type": "node", + "entity_name": "basic.source.users", + "node": "basic.source.users", + "activity_type": "restore", + "user": "dj", + "pre": {}, + "post": {}, + "details": {}, + "created_at": mock.ANY, + }, + { + "id": mock.ANY, + "entity_type": "node", + "entity_name": "basic.source.users", + "node": "basic.source.users", + "activity_type": "update", + "user": "dj", + "pre": {}, + "post": {}, + "details": {"version": "v2.0"}, + "created_at": mock.ANY, + }, + { + "activity_type": "delete", + "node": "basic.source.users", + "created_at": mock.ANY, + "details": {}, + "entity_name": "basic.source.users", + "entity_type": "node", + "id": mock.ANY, + "post": {}, + "pre": {}, + "user": "dj", + }, + { + "activity_type": "create", + "node": "basic.source.users", + "created_at": mock.ANY, + "details": {}, + "entity_name": "basic.source.users", + "entity_type": "node", + "id": mock.ANY, + "post": {}, + "pre": {}, + "user": "dj", + }, + ] + + @pytest.mark.asyncio + async def test_deleting_source_upstream_from_metric( + self, + client: AsyncClient, + ): + """ + Test deleting a source that's upstream from a metric + """ + response = await client.post("/catalogs/", json={"name": "warehouse"}) + assert response.status_code in (200, 201) + response = await client.post("/namespaces/default/") + assert response.status_code in (200, 201) + response = await client.post( + "/nodes/source/", + json={ + "name": "default.users", + "description": "A user table", + "columns": [ + {"name": "id", "type": "int"}, + {"name": "full_name", "type": "string"}, + {"name": "age", "type": "int"}, + {"name": "country", "type": "string"}, + {"name": "gender", "type": "string"}, + {"name": "preferred_language", "type": "string"}, + {"name": "secret_number", "type": "float"}, + {"name": "created_at", "type": "timestamp"}, + {"name": "post_processing_timestamp", "type": "timestamp"}, + ], + "mode": "published", + "catalog": "warehouse", + "schema_": "db", + "table": "users", + }, + ) + assert response.status_code in (200, 201) + response = await client.post( + "/nodes/metric/", + json={ + "description": "Total number of users", + "query": "SELECT COUNT(DISTINCT id) FROM default.users", + "mode": "published", + "name": "default.num_users", + }, + ) + assert response.status_code in (200, 201) + # Delete the source node + response = await client.delete("/nodes/default.users/") + assert response.status_code in (200, 201) + # The downstream metric should have an invalid status + assert (await client.get("/nodes/default.num_users/")).json()[ + "status" + ] == NodeStatus.INVALID + response = await client.get("/history?node=default.num_users") + assert [ + (activity["pre"], activity["post"], activity["details"]) + for activity in response.json() + if activity["activity_type"] == "status_change" + ] == [ + ( + {"status": "valid"}, + {"status": "invalid"}, + {"upstream_node": "default.users"}, + ), + ] + + # Restore the source node + response = await client.post("/nodes/default.users/restore/") + assert response.status_code in (200, 201) + # Retrieving the restored node should work + response = await client.get("/nodes/default.users/") + assert response.status_code in (200, 201) + # The downstream metric should have been changed to valid + response = await client.get("/nodes/default.num_users/") + assert response.json()["status"] == NodeStatus.VALID + # Check activity history of downstream metric + response = await client.get("/history?node=default.num_users") + assert [ + (activity["pre"], activity["post"], activity["details"]) + for activity in response.json() + if activity["activity_type"] == "status_change" + ] == [ + ( + {"status": "invalid"}, + {"status": "valid"}, + {"upstream_node": "default.users"}, + ), + ( + {"status": "valid"}, + {"status": "invalid"}, + {"upstream_node": "default.users"}, + ), + ] + + @pytest.mark.asyncio + async def test_deleting_transform_upstream_from_metric( + self, + client: AsyncClient, + ): + """ + Test deleting a transform that's upstream from a metric + """ + response = await client.post("/catalogs/", json={"name": "warehouse"}) + assert response.status_code in (200, 201) + response = await client.post("/namespaces/default/") + assert response.status_code in (200, 201) + response = await client.post( + "/nodes/source/", + json={ + "name": "default.users", + "description": "A user table", + "columns": [ + {"name": "id", "type": "int"}, + {"name": "full_name", "type": "string"}, + {"name": "age", "type": "int"}, + {"name": "country", "type": "string"}, + {"name": "gender", "type": "string"}, + {"name": "preferred_language", "type": "string"}, + {"name": "secret_number", "type": "float"}, + {"name": "created_at", "type": "timestamp"}, + {"name": "post_processing_timestamp", "type": "timestamp"}, + ], + "mode": "published", + "catalog": "warehouse", + "schema_": "db", + "table": "users", + }, + ) + assert response.status_code in (200, 201) + response = await client.post( + "/nodes/transform/", + json={ + "name": "default.us_users", + "description": "US users", + "query": """ + SELECT + id, + full_name, + age, + country, + gender, + preferred_language, + secret_number, + created_at, + post_processing_timestamp + FROM default.users + WHERE country = 'US' + """, + "mode": "published", + }, + ) + assert response.status_code in (200, 201) + response = await client.post( + "/nodes/metric/", + json={ + "description": "Total number of US users", + "query": "SELECT COUNT(DISTINCT id) FROM default.us_users", + "mode": "published", + "name": "default.num_us_users", + }, + ) + assert response.status_code in (200, 201) + # Create an invalid draft downstream node + # so we can test that it stays invalid + # when the upstream node is restored + response = await client.post( + "/nodes/metric/", + json={ + "description": "An invalid node downstream of default.us_users", + "query": "SELECT COUNT(DISTINCT non_existent_column) FROM default.us_users", + "mode": "draft", + "name": "default.invalid_metric", + }, + ) + assert response.status_code in (200, 201) + response = await client.get("/nodes/default.invalid_metric/") + assert response.status_code in (200, 201) + assert response.json()["status"] == NodeStatus.INVALID + # Delete the transform node + response = await client.delete("/nodes/default.us_users/") + assert response.status_code in (200, 201) + # Retrieving the deleted node should respond that the node doesn't exist + assert (await client.get("/nodes/default.us_users/")).json()["message"] == ( + "A node with name `default.us_users` does not exist." + ) + # The downstream metrics should have an invalid status + assert (await client.get("/nodes/default.num_us_users/")).json()[ + "status" + ] == NodeStatus.INVALID + assert (await client.get("/nodes/default.invalid_metric/")).json()[ + "status" + ] == NodeStatus.INVALID + + # Check history of downstream metrics + response = await client.get("/history?node=default.num_us_users") + assert [ + (activity["pre"], activity["post"], activity["details"]) + for activity in response.json() + if activity["activity_type"] == "status_change" + ] == [ + ( + {"status": "valid"}, + {"status": "invalid"}, + {"upstream_node": "default.us_users"}, + ), + ] + # No change recorded here because the metric was already invalid + response = await client.get("/history?node=default.invalid_metric") + assert [ + (activity["pre"], activity["post"]) + for activity in response.json() + if activity["activity_type"] == "status_change" + ] == [] + + # Restore the transform node + response = await client.post("/nodes/default.us_users/restore/") + assert response.status_code in (200, 201) + # Retrieving the restored node should work + response = await client.get("/nodes/default.us_users/") + assert response.status_code in (200, 201) + # Check history of the restored node + response = await client.get("/history?node=default.us_users") + history = response.json() + assert [ + (activity["activity_type"], activity["entity_type"]) for activity in history + ] == [("restore", "node"), ("delete", "node"), ("create", "node")] + + # This downstream metric should have been changed to valid + response = await client.get("/nodes/default.num_us_users/") + assert response.json()["status"] == NodeStatus.VALID + # Check history of downstream metric + response = await client.get("/history?node=default.num_us_users") + assert [ + (activity["pre"], activity["post"], activity["details"]) + for activity in response.json() + if activity["activity_type"] == "status_change" + ] == [ + ( + {"status": "invalid"}, + {"status": "valid"}, + {"upstream_node": "default.us_users"}, + ), + ( + {"status": "valid"}, + {"status": "invalid"}, + {"upstream_node": "default.us_users"}, + ), + ] + + # The other downstream metric should have remained invalid + response = await client.get("/nodes/default.invalid_metric/") + assert response.json()["status"] == NodeStatus.INVALID + # Check history of downstream metric + response = await client.get("/history?node=default.invalid_metric") + assert [ + (activity["pre"], activity["post"]) + for activity in response.json() + if activity["activity_type"] == "status_change" + ] == [] + + @pytest.mark.asyncio + async def test_deleting_linked_dimension( + self, + client: AsyncClient, + ): + """ + Test deleting a dimension that's linked to columns on other nodes + """ + response = await client.post("/catalogs/", json={"name": "warehouse"}) + assert response.status_code in (200, 201) + response = await client.post("/namespaces/default/") + assert response.status_code in (200, 201) + response = await client.post( + "/nodes/source/", + json={ + "name": "default.users", + "description": "A user table", + "columns": [ + {"name": "id", "type": "int"}, + {"name": "full_name", "type": "string"}, + {"name": "age", "type": "int"}, + {"name": "country", "type": "string"}, + {"name": "gender", "type": "string"}, + {"name": "preferred_language", "type": "string"}, + {"name": "secret_number", "type": "float"}, + {"name": "created_at", "type": "timestamp"}, + {"name": "post_processing_timestamp", "type": "timestamp"}, + ], + "mode": "published", + "catalog": "warehouse", + "schema_": "db", + "table": "users", + }, + ) + assert response.status_code in (200, 201) + response = await client.post( + "/nodes/dimension/", + json={ + "name": "default.us_users", + "description": "US users", + "query": """ + SELECT + id, + full_name, + age, + country, + gender, + preferred_language, + secret_number, + created_at, + post_processing_timestamp + FROM default.users + WHERE country = 'US' + """, + "primary_key": ["id"], + "mode": "published", + }, + ) + assert response.status_code in (200, 201) + response = await client.post( + "/nodes/source/", + json={ + "name": "default.messages", + "description": "A table of user messages", + "columns": [ + {"name": "id", "type": "int"}, + {"name": "user_id", "type": "int"}, + {"name": "message", "type": "int"}, + {"name": "posted_at", "type": "timestamp"}, + ], + "mode": "published", + "catalog": "warehouse", + "schema_": "db", + "table": "messages", + }, + ) + assert response.status_code in (200, 201) + # Create a metric on the source node + response = await client.post( + "/nodes/metric/", + json={ + "description": "Total number of user messages", + "query": "SELECT COUNT(DISTINCT id) FROM default.messages", + "mode": "published", + "name": "default.num_messages", + }, + ) + assert response.status_code in (200, 201) + + # Create a metric on the source node w/ bound dimensions + response = await client.post( + "/nodes/metric/", + json={ + "description": "Total number of user messages by id", + "query": "SELECT COUNT(DISTINCT id) FROM default.messages", + "mode": "published", + "name": "default.num_messages_id", + "required_dimensions": ["user_id"], + }, + ) + assert response.status_code in (200, 201) + + # Create a metric w/ bound dimensions that to not exist + with pytest.raises(Exception) as exc: + response = await client.post( + "/nodes/metric/", + json={ + "description": "Total number of user messages by id", + "query": "SELECT COUNT(DISTINCT id) FROM default.messages", + "mode": "published", + "name": "default.num_messages_id", + "required_dimensions": ["default.nothin.id"], + }, + ) + assert "required dimensions that are not on parent nodes" in str(exc) + + # Create a metric on the source node w/ an invalid bound dimension + response = await client.post( + "/nodes/metric/", + json={ + "description": "Total number of user messages by id", + "query": "SELECT COUNT(DISTINCT id) FROM default.messages", + "mode": "published", + "name": "default.num_messages_id_invalid_dimension", + "required_dimensions": ["default.messages.foo"], + }, + ) + assert response.status_code == 400 + assert response.json() == { + "message": "Node definition contains references to " + "columns as required dimensions that are not on parent nodes.", + "errors": [ + { + "code": 206, + "message": "Node definition contains references to columns " + "as required dimensions that are not on parent nodes.", + "debug": {"invalid_required_dimensions": ["default.messages.foo"]}, + "context": "", + }, + ], + "warnings": [], + } + + # Link the dimension to a column on the source node + response = await client.post( + "/nodes/default.messages/columns/user_id/" + "?dimension=default.us_users&dimension_column=id", + ) + assert response.status_code in (200, 201) + # The dimension's attributes should now be available to the metric + response = await client.get("/metrics/default.num_messages/") + assert response.status_code in (200, 201) + assert response.json()["dimensions"] == [ + { + "name": "default.us_users.age", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.country", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "string", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.created_at", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "timestamp", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.full_name", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "string", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.gender", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "string", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.id", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "int", + "filter_only": False, + "properties": ["primary_key"], + }, + { + "name": "default.us_users.post_processing_timestamp", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "timestamp", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.preferred_language", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "string", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.secret_number", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "float", + "filter_only": False, + "properties": [], + }, + ] + + # Check history of the node with column dimension link + response = await client.get( + "/history?node=default.messages", + ) + history = response.json() + assert [ + (activity["activity_type"], activity["entity_type"]) for activity in history + ] == [("create", "link"), ("create", "node")] + + # Delete the dimension node + response = await client.delete("/nodes/default.us_users/") + assert response.status_code in (200, 201) + # Retrieving the deleted node should respond that the node doesn't exist + assert (await client.get("/nodes/default.us_users/")).json()["message"] == ( + "A node with name `default.us_users` does not exist." + ) + # The deleted dimension's attributes should no longer be available to the metric + response = await client.get("/metrics/default.num_messages/") + assert response.status_code in (200, 201) + assert [] == response.json()["dimensions"] + # The metric should still be VALID + response = await client.get("/nodes/default.num_messages/") + assert response.json()["status"] == NodeStatus.VALID + # Restore the dimension node + response = await client.post("/nodes/default.us_users/restore/") + assert response.status_code in (200, 201) + # Retrieving the restored node should work + response = await client.get("/nodes/default.us_users/") + assert response.status_code in (200, 201) + # The dimension's attributes should now once again show for the linked metric + response = await client.get("/metrics/default.num_messages/") + assert response.status_code in (200, 201) + assert response.json()["dimensions"] == [ + { + "name": "default.us_users.age", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "int", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.country", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "string", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.created_at", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "timestamp", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.full_name", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "string", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.gender", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "string", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.id", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "int", + "filter_only": False, + "properties": ["primary_key"], + }, + { + "name": "default.us_users.post_processing_timestamp", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "timestamp", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.preferred_language", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "string", + "filter_only": False, + "properties": [], + }, + { + "name": "default.us_users.secret_number", + "node_display_name": "Us Users", + "node_name": "default.us_users", + "path": ["default.messages"], + "type": "float", + "filter_only": False, + "properties": [], + }, + ] + # The metric should still be VALID + response = await client.get("/nodes/default.num_messages/") + assert response.json()["status"] == NodeStatus.VALID + + @pytest.mark.asyncio + async def test_restoring_an_already_active_node( + self, + client: AsyncClient, + ): + """ + Test raising when restoring an already active node + """ + response = await client.post("/catalogs/", json={"name": "warehouse"}) + assert response.status_code in (200, 201) + response = await client.post("/namespaces/default/") + assert response.status_code in (200, 201) + response = await client.post( + "/nodes/source/", + json={ + "name": "default.users", + "description": "A user table", + "columns": [ + {"name": "id", "type": "int"}, + {"name": "full_name", "type": "string"}, + {"name": "age", "type": "int"}, + {"name": "country", "type": "string"}, + {"name": "gender", "type": "string"}, + {"name": "preferred_language", "type": "string"}, + {"name": "secret_number", "type": "float"}, + {"name": "created_at", "type": "timestamp"}, + {"name": "post_processing_timestamp", "type": "timestamp"}, + ], + "mode": "published", + "catalog": "warehouse", + "schema_": "db", + "table": "users", + }, + ) + assert response.status_code in (200, 201) + response = await client.post("/nodes/default.users/restore/") + assert response.status_code == 400 + assert response.json() == { + "message": "Cannot restore `default.users`, node already active.", + "errors": [], + "warnings": [], + } + + async def verify_complete_hard_delete( + self, + session: AsyncSession, + client_with_roads: AsyncClient, + node_name: str, + ): + """ + Verify that after hard deleting a node, all node revisions and node relationship + references are removed. + """ + # Record its upstream nodes + upstream_names = [ + node.name for node in await get_upstream_nodes(session, node_name=node_name) + ] + + # Hard delete the node + response = await client_with_roads.delete(f"/nodes/{node_name}/hard/") + assert response.status_code in (200, 201) + + # Check that all revisions (and their relations) for the node have been deleted + nodes = ( + (await session.execute(select(Node).where(Node.name == node_name))) + .unique() + .scalars() + .all() + ) + revisions = ( + ( + await session.execute( + select(NodeRevision).where(NodeRevision.name == node_name), + ) + ) + .unique() + .scalars() + .all() + ) + relations = ( + ( + await session.execute( + select(NodeRelationship).where( + NodeRelationship.child_id.in_( # type: ignore + [rev.id for rev in revisions], + ), + ), + ) + ) + .unique() + .scalars() + .all() + ) + assert nodes == [] + assert revisions == [] + assert relations == [] + + # Check that upstreams and downstreams of the node still remain + upstreams = ( + ( + await session.execute( + select(Node).where( + Node.name.in_(upstream_names), # type: ignore + ), + ) + ) + .unique() + .scalars() + .all() + ) + assert len(upstreams) == len(upstream_names) + + @pytest.mark.asyncio + async def test_hard_deleting_node_with_versions( + self, + client_with_roads: AsyncClient, + session: AsyncSession, + ): + """ + Test that hard deleting a node will remove all previous node revisions. + """ + # Create a few revisions for the `default.repair_order` dimension + await client_with_roads.patch( + "/nodes/default.repair_order/", + json={"query": """SELECT repair_order_id FROM default.repair_orders"""}, + ) + await client_with_roads.patch( + "/nodes/default.repair_order/", + json={ + "query": """SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM default.repair_orders""", + }, + ) + response = await client_with_roads.get("/nodes/default.repair_order") + assert response.json()["version"] == "v3.0" + + # Hard delete all nodes and verify after each delete + default_nodes = (await client_with_roads.get("/namespaces/default/")).json() + for node_name in default_nodes: + await self.verify_complete_hard_delete( + session, + client_with_roads, + node_name["name"], + ) + + # Check that all nodes under the `default` namespace and their revisions have been deleted + nodes = ( + (await session.execute(select(Node).where(Node.namespace == "default"))) + .unique() + .scalars() + .all() + ) + assert len(nodes) == 0 + + revisions = ( + ( + await session.execute( + select(NodeRevision).where( + NodeRevision.name.like("default%"), # type: ignore + ), + ) + ) + .unique() + .scalars() + .all() + ) + assert len(revisions) == 0 + + @pytest.mark.asyncio + async def test_hard_deleting_a_node( + self, + client_with_roads: AsyncClient, + ): + """ + Test raising when restoring an already active node + """ + # Hard deleting a node causes downstream nodes to become invalid + response = await client_with_roads.delete("/nodes/default.repair_orders/hard/") + assert response.status_code in (200, 201) + data = response.json() + data["impact"] = sorted(data["impact"], key=lambda x: x["name"]) + assert data == { + "impact": [ + { + "effect": "downstream node is now invalid", + "name": "default.avg_repair_order_discounts", + "status": "invalid", + }, + { + "effect": "downstream node is now invalid", + "name": "default.avg_repair_price", + "status": "invalid", + }, + { + "effect": "downstream node is now invalid", + "name": "default.avg_time_to_dispatch", + "status": "invalid", + }, + { + "effect": "downstream node is now invalid", + "name": "default.discounted_orders_rate", + "status": "invalid", + }, + { + "effect": "downstream node is now invalid", + "name": "default.num_repair_orders", + "status": "invalid", + }, + { + "effect": "downstream node is now invalid", + "name": "default.regional_level_agg", + "status": "invalid", + }, + { + "effect": "downstream node is now invalid", + "name": "default.regional_repair_efficiency", + "status": "invalid", + }, + { + "effect": "downstream node is now invalid", + "name": "default.repair_order", + "status": "invalid", + }, + { + "effect": "downstream node is now invalid", + "name": "default.repair_orders_fact", + "status": "invalid", + }, + { + "effect": "downstream node is now invalid", + "name": "default.total_repair_cost", + "status": "invalid", + }, + { + "effect": "downstream node is now invalid", + "name": "default.total_repair_order_discounts", + "status": "invalid", + }, + ], + "message": "The node `default.repair_orders` has been completely removed.", + } + + # Hard deleting a dimension creates broken links + response = await client_with_roads.delete("/nodes/default.repair_order/hard/") + assert response.status_code in (200, 201) + data = response.json() + assert sorted(data["impact"], key=lambda x: x["name"]) == sorted( + [ + { + "effect": "broken link", + "name": "default.repair_order_details", + "status": "valid", + }, + { + "effect": "broken link", + "name": "default.regional_level_agg", + "status": "invalid", + }, + { + "effect": "broken link", + "name": "default.national_level_agg", + "status": "valid", + }, + { + "effect": "broken link", + "name": "default.repair_orders_fact", + "status": "invalid", + }, + { + "effect": "broken link", + "name": "default.regional_repair_efficiency", + "status": "invalid", + }, + { + "effect": "broken link", + "name": "default.num_repair_orders", + "status": "invalid", + }, + { + "effect": "broken link", + "name": "default.avg_repair_price", + "status": "invalid", + }, + { + "effect": "broken link", + "name": "default.total_repair_cost", + "status": "invalid", + }, + { + "effect": "broken link", + "name": "default.discounted_orders_rate", + "status": "invalid", + }, + { + "effect": "broken link", + "name": "default.total_repair_order_discounts", + "status": "invalid", + }, + { + "effect": "broken link", + "name": "default.avg_repair_order_discounts", + "status": "invalid", + }, + { + "effect": "broken link", + "name": "default.avg_time_to_dispatch", + "status": "invalid", + }, + ], + key=lambda x: x["name"], + ) + assert ( + data["message"] + == "The node `default.repair_order` has been completely removed." + ) + + # Hard deleting an unlinked node has no impact + response = await client_with_roads.delete( + "/nodes/default.regional_repair_efficiency/hard/", + ) + assert response.status_code in (200, 201) + assert response.json() == { + "message": "The node `default.regional_repair_efficiency` has been completely removed.", + "impact": [], + } + + # Hard delete a metric + response = await client_with_roads.delete( + "/nodes/default.avg_repair_order_discounts/hard/", + ) + assert response.status_code in (200, 201) + assert response.json() == { + "message": "The node `default.avg_repair_order_discounts` has been completely removed.", + "impact": [], + } + + @pytest.mark.asyncio + async def test_register_table_without_query_service( + self, + client: AsyncClient, + ): + """ + Trying to register a table without a query service set up should fail. + """ + response = await client.post("/register/table/foo/bar/baz/") + data = response.json() + assert data["message"] == ( + "Registering tables or views requires that a query " + "service is configured for columns inference" + ) + assert response.status_code == 500 + + @pytest.mark.asyncio + async def test_register_view_without_query_service( + self, + client: AsyncClient, + ): + """ + Trying to register a view without a query service set up should fail. + """ + response = await client.post( + "/register/view/foo/bar/baz/?query=SELECT+1&replace=True", + ) + data = response.json() + assert data["message"] == ( + "Registering tables or views requires that a query " + "service is configured for columns inference" + ) + assert response.status_code == 500 + + @pytest.mark.asyncio + async def test_register_view_with_query_service( + self, + module__client_with_basic, + ): + """ + Registering a view with a query service set up should succeed. + """ + response = await module__client_with_basic.post( + "/register/view/public/main/view_foo?query=SELECT+1+AS+one+,+'two'+AS+two", + ) + data = response.json() + assert data["name"] == "source.public.main.view_foo" + assert data["type"] == "source" + assert data["display_name"] == "source.public.main.view_foo" + assert data["version"] == "v1.0" + assert data["status"] == "valid" + assert data["mode"] == "published" + assert data["catalog"]["name"] == "public" + assert data["schema_"] == "main" + assert data["table"] == "view_foo" + assert data["columns"] == [ + { + "name": "one", + "type": "int", + "display_name": "One", + "attributes": [], + "dimension": None, + "partition": None, + }, + { + "name": "two", + "type": "string", + "display_name": "Two", + "attributes": [], + "dimension": None, + "partition": None, + }, + ] + assert response.status_code == 201 + + @pytest.mark.asyncio + async def test_create_source_node_with_query_service( + self, + module__client_with_basic, + ): + """ + Creating a source node without columns but with a query service set should + result in the source node columns being inferred via the query service. + """ + response = await module__client_with_basic.post( + "/register/table/public/basic/comments/", + ) + data = response.json() + assert data["name"] == "source.public.basic.comments" + assert data["type"] == "source" + assert data["display_name"] == "source.public.basic.comments" + assert data["version"] == "v1.0" + assert data["status"] == "valid" + assert data["mode"] == "published" + assert data["catalog"]["name"] == "public" + assert data["schema_"] == "basic" + assert data["table"] == "comments" + assert data["columns"] == [ + { + "name": "id", + "type": "int", + "display_name": "Id", + "attributes": [], + "dimension": None, + "partition": None, + }, + { + "name": "user_id", + "type": "int", + "display_name": "User Id", + "attributes": [], + "dimension": None, + "partition": None, + }, + { + "name": "timestamp", + "type": "timestamp", + "display_name": "Timestamp", + "attributes": [], + "dimension": None, + "partition": None, + }, + { + "name": "text", + "type": "string", + "display_name": "Text", + "attributes": [], + "dimension": None, + "partition": None, + }, + ] + assert response.status_code == 201 + + @pytest.mark.asyncio + async def test_refresh_source_node( + self, + module__client_with_roads, + ): + """ + Refresh a source node with a query service + """ + custom_client = module__client_with_roads + response = await custom_client.post( + "/nodes/default.repair_orders/refresh/", + ) + data = response.json() + + # Columns have changed, so the new node revision should be bumped to a new + # version with an additional `ratings` column. Existing dimension links remain + new_columns = [ + { + "attributes": [], + "dimension": None, + "display_name": "Repair Order Id", + "name": "repair_order_id", + "type": "int", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Municipality Id", + "name": "municipality_id", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Hard Hat Id", + "name": "hard_hat_id", + "type": "int", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Order Date", + "name": "order_date", + "type": "timestamp", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Required Date", + "name": "required_date", + "type": "timestamp", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Dispatched Date", + "name": "dispatched_date", + "type": "timestamp", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Dispatcher Id", + "name": "dispatcher_id", + "type": "int", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Rating", + "name": "rating", + "type": "int", + "partition": None, + }, + ] + + assert data["version"] == "v2.0" + assert data["columns"] == new_columns + assert response.status_code == 201 + + response = await custom_client.get("/history?node=default.repair_orders") + history = response.json() + assert [ + (activity["activity_type"], activity["entity_type"]) for activity in history + ] == [ + ("refresh", "node"), + ("create", "link"), + ("create", "link"), + ("create", "node"), + ] + + # Refresh it again, but this time no columns will have changed so + # verify that the node revision stays the same + response = await custom_client.post( + "/nodes/default.repair_orders/refresh/", + ) + data_second = response.json() + assert data_second["version"] == "v2.0" + assert data_second["node_revision_id"] == data["node_revision_id"] + assert data_second["columns"] == new_columns + + # The refreshed source node should retain the existing dimension links + response = await custom_client.get("/nodes/default.repair_orders") + assert response.json()["dimension_links"] == [ + { + "dimension": {"name": "default.repair_order"}, + "foreign_keys": { + "default.repair_orders.repair_order_id": "default.repair_order.repair_order_id", + }, + "join_cardinality": "many_to_one", + "join_sql": "default.repair_orders.repair_order_id = " + "default.repair_order.repair_order_id", + "join_type": "inner", + "role": None, + }, + { + "dimension": {"name": "default.dispatcher"}, + "foreign_keys": { + "default.repair_orders.dispatcher_id": "default.dispatcher.dispatcher_id", + }, + "join_cardinality": "many_to_one", + "join_sql": "default.repair_orders.dispatcher_id = " + "default.dispatcher.dispatcher_id", + "join_type": "inner", + "role": None, + }, + ] + + @pytest.mark.asyncio + async def test_refresh_source_node_with_problems( + self, + module__client_with_roads, + module__query_service_client: QueryServiceClient, + mocker: MockerFixture, + ): + """ + Refresh a source node with a query service and find that no columns are returned. + """ + response = await module__client_with_roads.post( + "/nodes/default.repair_orders/refresh/", + ) + data = response.json() + + the_good_columns = module__query_service_client.get_columns_for_table( + "default", + "roads", + "repair_orders", + request_headers={}, + ) + + # Columns have changed, so the new node revision should be bumped to a new version + assert data["version"] == "v2.0" + assert len(data["columns"]) == 8 + assert response.status_code == 201 + assert data["status"] == "valid" + assert data["missing_table"] is False + + response = await module__client_with_roads.get( + "/history?node=default.repair_orders", + ) + history = response.json() + assert [ + (activity["activity_type"], activity["entity_type"]) for activity in history + ] == [ + ("refresh", "node"), + ("create", "link"), + ("create", "link"), + ("create", "node"), + ] + + # Refresh it again, but this time no columns are found + mocker.patch.object( + module__query_service_client, + "get_columns_for_table", + lambda *args: [], + ) + response = await module__client_with_roads.post( + "/nodes/default.repair_orders/refresh/", + ) + data_new = response.json() + assert data_new["version"] == "v3.0" + assert data_new["node_revision_id"] != data["node_revision_id"] + assert len(data_new["columns"]) == 8 + assert data_new["status"] == "valid" + assert data_new["missing_table"] is True + + # Refresh it again, but this time the table is missing + data = data_new + mocker.patch.object( + module__query_service_client, + "get_columns_for_table", + lambda *args: (_ for _ in ()).throw( + DJDoesNotExistException(message="Table not found: foo.bar.baz"), + ), + ) + response = await module__client_with_roads.post( + "/nodes/default.repair_orders/refresh/", + ) + data_new = response.json() + assert data_new["version"] == "v3.0" + assert data_new["node_revision_id"] == data["node_revision_id"] + assert len(data_new["columns"]) == 8 + assert data_new["status"] == "valid" + assert data_new["missing_table"] is True + + # Refresh it again, table is still missing + data = data_new + response = await module__client_with_roads.post( + "/nodes/default.repair_orders/refresh/", + ) + data_new = response.json() + assert data_new["version"] == "v3.0" + assert data_new["node_revision_id"] == data["node_revision_id"] + assert len(data_new["columns"]) == 8 + assert data_new["status"] == "valid" + assert data_new["missing_table"] is True + + # Refresh it again, back to normal state + data = data_new + mocker.patch.object( + module__query_service_client, + "get_columns_for_table", + lambda *args: the_good_columns, + ) + response = await module__client_with_roads.post( + "/nodes/default.repair_orders/refresh/", + ) + data_new = response.json() + assert data_new["version"] == "v4.0" + assert data_new["node_revision_id"] != data["node_revision_id"] + assert len(data_new["columns"]) == 8 + assert data_new["status"] == "valid" + assert data_new["missing_table"] is False + + @pytest.mark.asyncio + async def test_refresh_source_node_with_query( + self, + module__client_with_roads, + ): + """ + Refresh a source node based on a view. + """ + custom_client = module__client_with_roads + response = await custom_client.post( + "/nodes/default.repair_orders_view/refresh/", + ) + data = response.json() + + # Columns have changed, so the new node revision should be bumped to a new + # version with an additional `ratings` column. Existing dimension links remain + new_columns = [ + { + "attributes": [], + "dimension": None, + "display_name": "Repair Order Id", + "name": "repair_order_id", + "type": "int", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Municipality Id", + "name": "municipality_id", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Hard Hat Id", + "name": "hard_hat_id", + "type": "int", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Order Date", + "name": "order_date", + "type": "timestamp", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Required Date", + "name": "required_date", + "type": "timestamp", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Dispatched Date", + "name": "dispatched_date", + "type": "timestamp", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Dispatcher Id", + "name": "dispatcher_id", + "type": "int", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Rating", + "name": "rating", + "type": "int", + "partition": None, + }, + ] + + assert data["version"] == "v2.0" + assert data["columns"] == new_columns + assert response.status_code == 201 + + response = await custom_client.get("/history?node=default.repair_orders_view") + history = response.json() + assert [ + (activity["activity_type"], activity["entity_type"]) for activity in history + ] == [ + ("refresh", "node"), + ("create", "node"), + ] + + @pytest.mark.asyncio + async def test_create_update_source_node( + self, + client_with_basic: AsyncClient, + ) -> None: + """ + Test creating and updating a source node + """ + basic_source_comments = { + "name": "basic.source.comments", + "description": "A fact table with comments", + "columns": [ + {"name": "id", "type": "int"}, + { + "name": "user_id", + "type": "int", + "dimension": "basic.dimension.users", + }, + {"name": "timestamp", "type": "timestamp"}, + {"name": "text", "type": "string"}, + ], + "mode": "published", + "catalog": "public", + "schema_": "basic", + "table": "comments", + } + + # Trying to create it again should fail + response = await client_with_basic.post( + "/nodes/source/", + json=basic_source_comments, + ) + data = response.json() + assert ( + data["message"] + == "A node with name `basic.source.comments` already exists." + ) + assert response.status_code == 409 + + # Update node with a new description should create a new revision + response = await client_with_basic.patch( + f"/nodes/{basic_source_comments['name']}/", + json={ + "description": "New description", + "display_name": "Comments facts", + }, + ) + data = response.json() + + assert data["name"] == "basic.source.comments" + assert data["display_name"] == "Comments facts" + assert data["type"] == "source" + assert data["version"] == "v1.1" + assert data["description"] == "New description" + + # Try to update node with no changes + response = await client_with_basic.patch( + f"/nodes/{basic_source_comments['name']}/", + json={"description": "New description", "display_name": "Comments facts"}, + ) + new_data = response.json() + assert data == new_data + + # Try to update a node with a table that has different columns + response = await client_with_basic.patch( + f"/nodes/{basic_source_comments['name']}/", + json={ + "columns": [ + {"name": "id", "type": "int"}, + { + "name": "user_id", + "type": "int", + "dimension": "basic.dimension.users", + }, + {"name": "timestamp", "type": "timestamp"}, + {"name": "text_v2", "type": "string"}, + ], + }, + ) + data = response.json() + assert data["version"] == "v2.0" + assert data["columns"] == [ + { + "attributes": [], + "dimension": None, + "display_name": "Id", + "name": "id", + "type": "int", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "User Id", + "name": "user_id", + "type": "int", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Timestamp", + "name": "timestamp", + "type": "timestamp", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Text V2", + "name": "text_v2", + "type": "string", + "partition": None, + }, + ] + + @pytest.mark.asyncio + async def test_update_nonexistent_node( + self, + client: AsyncClient, + ) -> None: + """ + Test updating a non-existent node. + """ + response = await client.patch( + "/nodes/something/", + json={"description": "new"}, + ) + data = response.json() + assert response.status_code == 404 + assert data["message"] == "A node with name `something` does not exist." + + @pytest.mark.asyncio + async def test_update_node_with_deactivated_children( + self, + client_with_roads: AsyncClient, + ) -> None: + """ + Test updating a node with deactivated children + """ + # Test updating a transform with a deactivated downstream cube + response = await client_with_roads.post( + "/nodes/cube/", + json={ + "metrics": ["default.num_repair_orders", "default.avg_repair_price"], + "dimensions": [ + "default.hard_hat.country", + "default.dispatcher.company_name", + ], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.repairs_cube", + }, + ) + assert response.status_code == 201 + response = await client_with_roads.delete( + "/nodes/default.repairs_cube/", + ) + assert response.status_code == 200 + + response = await client_with_roads.patch( + "/nodes/default.repair_orders_fact", + json={ + "query": """SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id + FROM + default.repair_orders repair_orders""", + }, + ) + assert response.status_code == 200 + + # Test updating a transform with a deactivated downstream metric + response = await client_with_roads.delete( + "/nodes/default.num_repair_orders/", + ) + assert response.status_code == 200 + response = await client_with_roads.patch( + "/nodes/default.repair_orders_fact", + json={ + "query": """SELECT + repair_orders.repair_order_id + FROM + default.repair_orders repair_orders""", + }, + ) + assert response.status_code == 200 + + @pytest.mark.asyncio + async def test_raise_on_source_node_with_no_catalog( + self, + client: AsyncClient, + ) -> None: + """ + Test raise on source node with no catalog + """ + response = await client.post( + "/nodes/source/", + json={ + "name": "basic.source.comments", + "description": "A fact table with comments", + "columns": [ + {"name": "id", "type": "int"}, + { + "name": "user_id", + "type": "int", + "dimension": "basic.dimension.users", + }, + {"name": "timestamp", "type": "timestamp"}, + {"name": "text", "type": "string"}, + ], + "mode": "published", + }, + ) + assert response.status_code >= 400 + assert response.json() == { + "detail": [ + { + "loc": ["body", "catalog"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "schema_"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "table"], + "msg": "field required", + "type": "value_error.missing", + }, + ], + } + + @pytest.mark.asyncio + async def test_create_invalid_transform_node( + self, + catalog: Catalog, + source_node: Node, + client: AsyncClient, + create_invalid_transform_node_payload: Dict[str, Any], + ) -> None: + """ + Test creating an invalid transform node in draft and published modes. + """ + await client.post("/namespaces/default/") + response = await client.post( + "/nodes/transform/", + json=create_invalid_transform_node_payload, + ) + data = response.json() + assert response.status_code == 400 + assert data["message"].startswith( + "Node definition contains references to nodes that do not exist", + ) + + @pytest.mark.asyncio + async def test_create_node_with_type_inference_failure( + self, + client_with_namespaced_roads: AsyncClient, + ): + """ + Attempting to create a published metric where type inference fails should raise + an appropriate error and fail. + """ + response = await client_with_namespaced_roads.post( + "/nodes/metric/", + json={ + "description": "Average length of employment", + "query": ( + "SELECT avg(NOW() - hire_date + 1) as " + "default_DOT_avg_length_of_employment_plus_one " + "FROM foo.bar.hard_hats" + ), + "mode": "published", + "name": "default.avg_length_of_employment_plus_one", + }, + ) + data = response.json() + assert data == { + "message": ( + "Incompatible types in binary operation NOW() - " + "foo.bar.hard_hats.hire_date + 1. Got left timestamp, right int." + ), + "errors": [ + { + "code": 302, + "message": ( + "Incompatible types in binary operation NOW() - " + "foo.bar.hard_hats.hire_date + 1. Got left timestamp, right int." + ), + "debug": { + "columns": ["default_DOT_avg_length_of_employment_plus_one"], + "errors": [], + }, + "context": "", + }, + ], + "warnings": [], + } + + @pytest.mark.asyncio + async def test_create_update_transform_node( + self, + catalog: Catalog, + source_node: Node, + client: AsyncClient, + create_transform_node_payload: Dict[str, Any], + ) -> None: + """ + Test creating and updating a transform node that references an existing source. + """ + await client.post("/namespaces/default/") + # Create a transform node + response = await client.post( + "/nodes/transform/", + json=create_transform_node_payload, + ) + data = response.json() + assert data["name"] == "default.country_agg" + assert data["display_name"] == "Country Agg" + assert data["type"] == "transform" + assert data["description"] == "Distinct users per country" + assert ( + data["query"] + == "SELECT country, COUNT(DISTINCT id) AS num_users FROM basic.source.users" + ) + assert data["status"] == "valid" + assert data["columns"] == [ + { + "attributes": [], + "dimension": None, + "display_name": "Country", + "name": "country", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Num Users", + "name": "num_users", + "type": "bigint", + "partition": None, + }, + ] + + assert data["parents"] == [{"name": "basic.source.users"}] + + # Update the transform node with two minor changes + response = await client.patch( + "/nodes/default.country_agg/", + json={ + "description": "Some new description", + "display_name": "Country Aggregation by User", + }, + ) + data = response.json() + assert data["name"] == "default.country_agg" + assert data["display_name"] == "Country Aggregation by User" + assert data["type"] == "transform" + assert data["version"] == "v1.1" + assert data["description"] == "Some new description" + assert ( + data["query"] + == "SELECT country, COUNT(DISTINCT id) AS num_users FROM basic.source.users" + ) + assert data["status"] == "valid" + assert data["parents"] == [{"name": "basic.source.users"}] + + # Try to update with a new query that references a non-existent source + response = await client.patch( + "/nodes/default.country_agg/", + json={ + "query": "SELECT country, COUNT(DISTINCT id) AS num_users FROM comments", + }, + ) + data = response.json() + assert data["message"].startswith( + "Node definition contains references to nodes that do not exist", + ) + + # Try to update with a new query that references an existing source + response = await client.patch( + "/nodes/default.country_agg/", + json={ + "query": "SELECT country, COUNT(DISTINCT id) AS num_users, " + "COUNT(*) AS num_entries FROM basic.source.users", + }, + ) + data = response.json() + assert data["version"] == "v2.0" + assert ( + data["query"] == "SELECT country, COUNT(DISTINCT id) AS num_users, " + "COUNT(*) AS num_entries FROM basic.source.users" + ) + assert data["columns"] == [ + { + "attributes": [], + "dimension": None, + "display_name": "Country", + "name": "country", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Num Users", + "name": "num_users", + "type": "bigint", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Num Entries", + "name": "num_entries", + "type": "bigint", + "partition": None, + }, + ] + + assert data["status"] == "valid" + assert data["parents"] == [{"name": "basic.source.users"}] + + # Verify that asking for revisions for a non-existent transform fails + response = await client.get("/nodes/random_transform/revisions/") + data = response.json() + assert data["message"] == "A node with name `random_transform` does not exist." + + # Verify that all historical revisions are available for the node + response = await client.get("/nodes/default.country_agg/revisions/") + data = response.json() + assert {rev["version"]: rev["query"] for rev in data} == { + "v1.0": "SELECT country, COUNT(DISTINCT id) AS num_users FROM basic.source.users", + "v1.1": "SELECT country, COUNT(DISTINCT id) AS num_users FROM basic.source.users", + "v2.0": "SELECT country, COUNT(DISTINCT id) AS num_users, COUNT(*) AS num_entries " + "FROM basic.source.users", + } + assert {rev["version"]: rev["columns"] for rev in data} == { + "v1.0": [ + { + "attributes": [], + "dimension": None, + "display_name": "Country", + "name": "country", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Num Users", + "name": "num_users", + "type": "bigint", + "partition": None, + }, + ], + "v1.1": [ + { + "attributes": [], + "dimension": None, + "display_name": "Country", + "name": "country", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Num Users", + "name": "num_users", + "type": "bigint", + "partition": None, + }, + ], + "v2.0": [ + { + "attributes": [], + "dimension": None, + "display_name": "Country", + "name": "country", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Num Users", + "name": "num_users", + "type": "bigint", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Num Entries", + "name": "num_entries", + "type": "bigint", + "partition": None, + }, + ], + } + + @pytest.mark.asyncio + async def test_update_metric_node(self, client_with_roads: AsyncClient): + """ + Verify that during metric node updates, if the query changes, DJ will automatically + alias the metric column. If this aliased query is the same as the current revision's + query, DJ won't promote the version. + """ + response = await client_with_roads.patch( + "/nodes/default.total_repair_cost/", + json={ + "query": ( + "SELECT sum(repair_orders_fact.total_repair_cost) " + "FROM default.repair_orders_fact repair_orders_fact" + ), + "metric_metadata": { + "kind": "count", + "direction": "higher_is_better", + "unit": "dollar", + }, + }, + ) + node_data = response.json() + assert node_data["query"] == ( + "SELECT sum(repair_orders_fact.total_repair_cost) " + "FROM default.repair_orders_fact repair_orders_fact" + ) + response = await client_with_roads.get("/metrics/default.total_repair_cost") + metric_data = response.json() + assert metric_data["metric_metadata"] == { + "direction": "higher_is_better", + "unit": { + "abbreviation": None, + "category": None, + "description": None, + "label": "Dollar", + "name": "DOLLAR", + }, + } + + response = await client_with_roads.get("/nodes/default.total_repair_cost") + assert response.json()["version"] == "v2.0" + + response = await client_with_roads.patch( + "/nodes/default.total_repair_cost/", + json={ + "query": "SELECT count(price) FROM default.repair_order_details", + "required_dimensions": ["repair_order_id"], + }, + ) + node_data = response.json() + assert node_data["query"] == ( + "SELECT count(price) FROM default.repair_order_details" + ) + response = await client_with_roads.get("/nodes/default.total_repair_cost") + data = response.json() + assert data["version"] == "v3.0" + response = await client_with_roads.get("/metrics/default.total_repair_cost") + data = response.json() + assert data["required_dimensions"] == ["repair_order_id"] + + @pytest.mark.asyncio + async def test_create_dimension_node_fails( + self, + catalog: Catalog, + source_node: Node, + client: AsyncClient, + ): + """ + Test various failure cases for dimension node creation. + """ + await client.post("/namespaces/default/") + response = await client.post( + "/nodes/dimension/", + json={ + "description": "Country dimension", + "query": "SELECT country, COUNT(1) AS user_cnt " + "FROM basic.source.users GROUP BY country", + "mode": "published", + "name": "countries", + }, + ) + assert ( + response.json()["message"] == "Dimension nodes must define a primary key!" + ) + + response = await client.post( + "/nodes/dimension/", + json={ + "description": "Country dimension", + "query": "SELECT country, COUNT(1) AS user_cnt " + "FROM basic.source.users GROUP BY country", + "mode": "published", + "name": "default.countries", + "primary_key": ["country", "id"], + }, + ) + assert response.json()["message"] == ( + "Some columns in the primary key [country,id] were not " + "found in the list of available columns for the node " + "default.countries." + ) + + @pytest.mark.asyncio + async def test_create_update_dimension_node( + self, + catalog: Catalog, + source_node: Node, + client: AsyncClient, + create_dimension_node_payload: Dict[str, Any], + ) -> None: + """ + Test creating and updating a dimension node that references an existing source. + """ + await client.post("/namespaces/default/") + response = await client.post( + "/nodes/dimension/", + json=create_dimension_node_payload, + ) + data = response.json() + + assert response.status_code == 201 + assert data["name"] == "default.countries" + assert data["display_name"] == "Countries" + assert data["type"] == "dimension" + assert data["version"] == "v1.0" + assert data["description"] == "Country dimension" + assert ( + data["query"] == "SELECT country, COUNT(1) AS user_cnt " + "FROM basic.source.users GROUP BY country" + ) + assert data["columns"] == [ + { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "Country", + "name": "country", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "User Cnt", + "name": "user_cnt", + "type": "bigint", + "partition": None, + }, + ] + + # Test updating the dimension node with a new query + response = await client.patch( + "/nodes/default.countries/", + json={"query": "SELECT country FROM basic.source.users GROUP BY country"}, + ) + data = response.json() + # Should result in a major version update due to the query change + assert data["version"] == "v2.0" + + # The columns should have been updated + assert data["columns"] == [ + { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "Country", + "name": "country", + "type": "string", + "partition": None, + }, + ] + + # Test updating the dimension node with a new primary key + response = await client.patch( + "/nodes/default.countries/", + json={ + "query": "SELECT country, SUM(age) as sum_age, count(1) AS num_users " + "FROM basic.source.users GROUP BY country", + "primary_key": ["sum_age"], + }, + ) + data = response.json() + # Should result in a major version update + assert data["version"] == "v3.0" + assert data["columns"] == [ + { + "attributes": [], + "dimension": None, + "display_name": "Country", + "name": "country", + "type": "string", + "partition": None, + }, + { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "Sum Age", + "name": "sum_age", + "type": "bigint", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Num Users", + "name": "num_users", + "type": "bigint", + "partition": None, + }, + ] + + response = await client.patch( + "/nodes/default.countries/", + json={ + "primary_key": ["country"], + }, + ) + data = response.json() + assert data["columns"] == [ + { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "Country", + "name": "country", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Sum Age", + "name": "sum_age", + "type": "bigint", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Num Users", + "name": "num_users", + "type": "bigint", + "partition": None, + }, + ] + + @pytest.mark.asyncio + async def test_raise_on_multi_catalog_node(self, client_example_loader): + """ + Test raising when trying to select from multiple catalogs + """ + custom_client = await client_example_loader(["BASIC", "ACCOUNT_REVENUE"]) + response = await custom_client.post( + "/nodes/transform/", + json={ + "query": ( + "SELECT payment_id, payment_amount, customer_id, account_type " + "FROM default.revenue r LEFT JOIN basic.source.comments b on r.id = b.id" + ), + "description": "Multicatalog", + "mode": "published", + "name": "default.multicatalog", + }, + ) + assert ( + "Cannot create nodes with multi-catalog dependencies" + in response.json()["message"] + ) + + @pytest.mark.asyncio + async def test_updating_node_to_invalid_draft( + self, + catalog: Catalog, + source_node: Node, + client: AsyncClient, + create_dimension_node_payload: Dict[str, Any], + ) -> None: + """ + Test creating an invalid node in draft mode + """ + await client.post("/namespaces/default/") + response = await client.post( + "/nodes/dimension/", + json=create_dimension_node_payload, + ) + data = response.json() + + assert response.status_code == 201 + assert data["name"] == "default.countries" + assert data["display_name"] == "Countries" + assert data["type"] == "dimension" + assert data["version"] == "v1.0" + assert data["description"] == "Country dimension" + assert ( + data["query"] == "SELECT country, COUNT(1) AS user_cnt " + "FROM basic.source.users GROUP BY country" + ) + assert data["columns"] == [ + { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "Country", + "name": "country", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "User Cnt", + "name": "user_cnt", + "type": "bigint", + "partition": None, + }, + ] + + response = await client.patch( + "/nodes/default.countries/", + json={"mode": "draft"}, + ) + assert response.status_code == 200 + + # Test updating the dimension node with an invalid query + response = await client.patch( + "/nodes/default.countries/", + json={"query": "SELECT country FROM missing_parent GROUP BY country"}, + ) + assert response.status_code == 200 + + # Check that node is now a draft with an invalid status + response = await client.get("/nodes/default.countries") + assert response.status_code == 200 + data = response.json() + assert data["mode"] == "draft" + assert data["status"] == "invalid" + + @pytest.mark.asyncio + async def test_upsert_materialization_config( + self, + client_with_query_service_example_loader, + ) -> None: + """ + Test creating & updating materialization config for a node. + """ + custom_client = await client_with_query_service_example_loader(["BASIC"]) + # Setting the materialization config for a source node should fail + response = await custom_client.post( + "/nodes/basic.source.comments/materialization/", + json={ + "job": "spark_sql", + "schedule": "0 * * * *", + "config": {}, + "strategy": "full", + }, + ) + assert response.status_code == 400 + assert ( + response.json()["message"] + == "Cannot set materialization config for source node `basic.source.comments`!" + ) + + # Setting the materialization config for a materialization job type that + # doesn't exist should fail + response = await custom_client.post( + "/nodes/basic.transform.country_agg/materialization/", + json={ + "job": "something", + "strategy": "full", + "config": {}, + "schedule": "0 * * * *", + }, + ) + assert response.status_code == 404 + data = response.json() + assert data["message"] == ( + "Materialization job type `SOMETHING` not found. Available job " + "types: ['SPARK_SQL', 'DRUID_MEASURES_CUBE', 'DRUID_METRICS_CUBE', 'DRUID_CUBE']" + ) + + @pytest.mark.asyncio + async def test_node_with_struct( + self, + session: AsyncSession, + client_with_roads: AsyncClient, + ): + """ + Test that building a query string with structs yields a correctly formatted struct + reference. + """ + response = await client_with_roads.post( + "/nodes/transform/", + json={ + "description": "Regional level agg with structs", + "query": """SELECT + usr.us_region_id, + us.state_name, + CONCAT(us.state_name, '-', usr.us_region_description) AS location_hierarchy, + EXTRACT(YEAR FROM ro.order_date) AS order_year, + EXTRACT(MONTH FROM ro.order_date) AS order_month, + EXTRACT(DAY FROM ro.order_date) AS order_day, + STRUCT( + COUNT(DISTINCT CASE WHEN ro.dispatched_date IS NOT NULL THEN ro.repair_order_id ELSE NULL END) AS completed_repairs, + COUNT(DISTINCT ro.repair_order_id) AS total_repairs_dispatched, + SUM(rd.price * rd.quantity) AS total_amount_in_region, + AVG(rd.price * rd.quantity) AS avg_repair_amount_in_region, + AVG(DATEDIFF(ro.dispatched_date, ro.order_date)) AS avg_dispatch_delay, + COUNT(DISTINCT c.contractor_id) AS unique_contractors + ) AS measures +FROM default.repair_orders ro +JOIN + default.municipality m ON ro.municipality_id = m.municipality_id +JOIN + default.us_states us ON m.state_id = us.state_id +JOIN + default.us_states us ON m.state_id = us.state_id +JOIN + default.us_region usr ON us.state_region = usr.us_region_id +JOIN + default.repair_order_details rd ON ro.repair_order_id = rd.repair_order_id +JOIN + default.repair_type rt ON rd.repair_type_id = rt.repair_type_id +JOIN + default.contractors c ON rt.contractor_id = c.contractor_id +GROUP BY + usr.us_region_id, + EXTRACT(YEAR FROM ro.order_date), + EXTRACT(MONTH FROM ro.order_date), + EXTRACT(DAY FROM ro.order_date)""", + "mode": "published", + "name": "default.regional_level_agg_structs", + "primary_key": [ + "us_region_id", + "state_name", + "order_year", + "order_month", + "order_day", + ], + }, + ) + assert { + "attributes": [], + "dimension": None, + "name": "measures", + "type": "struct", + "display_name": "Measures", + "partition": None, + } in response.json()["columns"] + + await client_with_roads.post( + "/nodes/transform/", + json={ + "description": "Total Repair Amounts during the COVID-19 Pandemic", + "name": "default.total_amount_in_region_from_struct_transform", + "query": "SELECT location_hierarchy, SUM(IF(order_year = 2020, " + "measures.total_amount_in_region, 0)) " + "col0 FROM default.regional_level_agg_structs", + "mode": "published", + }, + ) + response = await client_with_roads.get( + "/sql/default.total_amount_in_region_from_struct_transform?filters=" + "&dimensions=location_hierarchy", + ) + expected = """ + WITH default_DOT_regional_level_agg_structs AS ( + SELECT usr.us_region_id, + us.state_name, + CONCAT(us.state_name, '-', usr.us_region_description) AS location_hierarchy, + EXTRACT(YEAR, ro.order_date) AS order_year, + EXTRACT(MONTH, ro.order_date) AS order_month, + EXTRACT(DAY, ro.order_date) AS order_day, + struct(COUNT( DISTINCT CASE + WHEN ro.dispatched_date IS NOT NULL THEN ro.repair_order_id + ELSE NULL + END) AS completed_repairs, COUNT( DISTINCT ro.repair_order_id) AS total_repairs_dispatched, SUM(rd.price * rd.quantity) AS total_amount_in_region, AVG(rd.price * rd.quantity) AS avg_repair_amount_in_region, AVG(DATEDIFF(ro.dispatched_date, ro.order_date)) AS avg_dispatch_delay, COUNT( DISTINCT c.contractor_id) AS unique_contractors) AS measures + FROM roads.repair_orders AS ro JOIN roads.municipality AS m ON ro.municipality_id = m.municipality_id + JOIN roads.us_states AS us ON m.state_id = us.state_id + JOIN roads.us_states AS us ON m.state_id = us.state_id + JOIN roads.us_region AS usr ON us.state_region = usr.us_region_id + JOIN roads.repair_order_details AS rd ON ro.repair_order_id = rd.repair_order_id + JOIN roads.repair_type AS rt ON rd.repair_type_id = rt.repair_type_id + JOIN roads.contractors AS c ON rt.contractor_id = c.contractor_id + GROUP BY usr.us_region_id, EXTRACT(YEAR, ro.order_date), EXTRACT(MONTH, ro.order_date), EXTRACT(DAY, ro.order_date) + ), + default_DOT_total_amount_in_region_from_struct_transform AS ( + SELECT default_DOT_regional_level_agg_structs.location_hierarchy, + SUM(IF(default_DOT_regional_level_agg_structs.order_year = 2020, default_DOT_regional_level_agg_structs.measures.total_amount_in_region, 0)) col0 + FROM default_DOT_regional_level_agg_structs + ) + SELECT default_DOT_total_amount_in_region_from_struct_transform.location_hierarchy default_DOT_total_amount_in_region_from_struct_transform_DOT_location_hierarchy, + default_DOT_total_amount_in_region_from_struct_transform.col0 default_DOT_total_amount_in_region_from_struct_transform_DOT_col0 + FROM default_DOT_total_amount_in_region_from_struct_transform + """ + assert str(parse(response.json()["sql"])) == str(parse(expected)) + + # Check that this query request has been saved + query_request = (await session.execute(select(QueryRequest))).scalars().all() + assert len(query_request) == 1 + assert query_request[0].nodes == [ + "default.total_amount_in_region_from_struct_transform@v1.0", + ] + assert query_request[0].dimensions == ["location_hierarchy@v1.0"] + assert query_request[0].filters == [] + assert query_request[0].orderby == [] + assert query_request[0].limit is None + assert query_request[0].query_type == QueryBuildType.NODE + + @pytest.mark.asyncio + async def test_node_with_incremental_time_materialization( + self, + client_with_query_service_example_loader, + query_service_client, + ) -> None: + """ + 1. Create a transform node that uses dj_logical_timestamp (i.e., it is + meant to be incrementally materialized). + 2. Create a metric node that references the above transform. + 3. When SQL for the metric is requested without the transform having been materialized, + the request will fail. + """ + custom_client = await client_with_query_service_example_loader(["ROADS"]) + await custom_client.post( + "/nodes/transform/", + json={ + "description": "Repair orders transform (partitioned)", + "query": """ + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM default.repair_orders + """, + "mode": "published", + "name": "default.repair_orders_partitioned", + "primary_key": ["repair_order_id"], + }, + ) + # Mark one of the columns as a time partition + await custom_client.post( + "/nodes/default.repair_orders_partitioned/columns/dispatched_date/partition", + json={ + "type_": "temporal", + "granularity": "day", + "format": "yyyyMMdd", + }, + ) + + # Set an incremental time materialization config with a lookback window of 100 days + await custom_client.post( + "/nodes/default.repair_orders_partitioned/materialization/", + json={ + "job": "spark_sql", + "strategy": "incremental_time", + "config": { + "lookback_window": "100 DAYS", + }, + "schedule": "0 * * * *", + }, + ) + + args, _ = query_service_client.materialize.call_args_list[0] # type: ignore + format_regex = r"\${(?P[^}]+)}" + match = re.search(format_regex, args[0].query) + assert match and match.group("capture") == "dj_logical_timestamp" + query = re.sub(format_regex, "DJ_LOGICAL_TIMESTAMP()", args[0].query) + expected_query = """ + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM ( + SELECT + default_DOT_repair_orders.repair_order_id, + default_DOT_repair_orders.municipality_id, + default_DOT_repair_orders.hard_hat_id, + default_DOT_repair_orders.order_date, + default_DOT_repair_orders.required_date, + default_DOT_repair_orders.dispatched_date, + default_DOT_repair_orders.dispatcher_id + FROM roads.repair_orders AS default_DOT_repair_orders + ) AS default_DOT_repair_orders_partitioned + WHERE + dispatched_date BETWEEN CAST( + DATE_FORMAT( + CAST(DJ_LOGICAL_TIMESTAMP() AS TIMESTAMP) - INTERVAL 100 DAYS, 'yyyyMMdd' + ) AS TIMESTAMP + ) + AND CAST( + DATE_FORMAT( + CAST(DJ_LOGICAL_TIMESTAMP() AS TIMESTAMP), + 'yyyyMMdd' + ) AS TIMESTAMP + ) + """ + compare_query_strings(query, expected_query) + + # Set an incremental time materialization config without a lookback window + # (defaults to 1 day) + await custom_client.post( + "/nodes/default.repair_orders_partitioned/materialization/", + json={ + "job": "spark_sql", + "strategy": "incremental_time", + "config": {}, + "schedule": "0 * * * *", + }, + ) + + args, _ = query_service_client.materialize.call_args_list[0] # type: ignore + match = re.search(format_regex, args[0].query) + assert match and match.group("capture") == "dj_logical_timestamp" + query = re.sub(format_regex, "DJ_LOGICAL_TIMESTAMP()", args[0].query) + expected_query = """ + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM ( + SELECT + default_DOT_repair_orders.repair_order_id, + default_DOT_repair_orders.municipality_id, + default_DOT_repair_orders.hard_hat_id, + default_DOT_repair_orders.order_date, + default_DOT_repair_orders.required_date, + default_DOT_repair_orders.dispatched_date, + default_DOT_repair_orders.dispatcher_id + FROM roads.repair_orders AS default_DOT_repair_orders + ) AS default_DOT_repair_orders_partitioned + WHERE dispatched_date = CAST( + DATE_FORMAT( + CAST(DJ_LOGICAL_TIMESTAMP() AS TIMESTAMP), + 'yyyyMMdd' + ) AS TIMESTAMP + ) + """ + compare_query_strings(query, expected_query) + + @pytest.mark.asyncio + async def test_node_with_dj_logical_timestamp( + self, + client_with_query_service_example_loader, + ) -> None: + """ + 1. Create a transform node that uses dj_logical_timestamp (i.e., it is + meant to be incrementally materialized). + 2. Create a metric node that references the above transform. + 3. When SQL for the metric is requested without the transform having been materialized, + the request will fail. + """ + custom_client = await client_with_query_service_example_loader(["ROADS"]) + await custom_client.post( + "/nodes/transform/", + json={ + "description": "Repair orders transform (partitioned)", + "query": """ + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id, + dj_logical_timestamp('%Y%m%d') as date_partition + FROM default.repair_orders + WHERE date_format(order_date, 'yyyyMMdd') = dj_logical_timestamp('%Y%m%d') + """, + "mode": "published", + "name": "default.repair_orders_partitioned", + "primary_key": ["repair_order_id"], + }, + ) + await custom_client.post( + "/nodes/default.repair_orders_partitioned/columns/hard_hat_id/" + "?dimension=default.hard_hat&dimension_column=hard_hat_id", + ) + + await custom_client.post( + "/nodes/metric/", + json={ + "description": "Number of repair orders", + "query": "SELECT count(repair_order_id) FROM default.repair_orders_partitioned", + "mode": "published", + "name": "default.num_repair_orders_partitioned", + }, + ) + response = await custom_client.get( + "/sql?metrics=default.num_repair_orders_partitioned" + "&dimensions=default.hard_hat.last_name", + ) + format_regex = r"\${(?P[^}]+)}" + + result_sql = response.json()["sql"] + + match = re.search(format_regex, result_sql) + assert match and match.group("capture") == "dj_logical_timestamp" + query = re.sub(format_regex, "FORMATTED", result_sql) + compare_query_strings( + query, + """WITH +m0_default_DOT_num_repair_orders_partitioned AS (SELECT default_DOT_hard_hat.last_name, + count(default_DOT_repair_orders_partitioned.repair_order_id) + default_DOT_num_repair_orders_partitioned + FROM (SELECT FORMATTED AS date_partition, + default_DOT_repair_orders.dispatched_date, + default_DOT_repair_orders.dispatcher_id, + default_DOT_repair_orders.hard_hat_id, + default_DOT_repair_orders.municipality_id, + default_DOT_repair_orders.order_date, + default_DOT_repair_orders.repair_order_id, + default_DOT_repair_orders.required_date + FROM roads.repair_orders AS default_DOT_repair_orders + WHERE date_format(default_DOT_repair_orders.order_date, 'yyyyMMdd') = FORMATTED) + AS default_DOT_repair_orders_partitioned LEFT JOIN + (SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.state + FROM roads.hard_hats AS default_DOT_hard_hats) + AS default_DOT_hard_hat ON + default_DOT_repair_orders_partitioned.hard_hat_id = default_DOT_hard_hat.hard_hat_id + GROUP BY default_DOT_hard_hat.last_name +) + +SELECT m0_default_DOT_num_repair_orders_partitioned.default_DOT_num_repair_orders_partitioned, + m0_default_DOT_num_repair_orders_partitioned.last_name + FROM m0_default_DOT_num_repair_orders_partitioned""", + ) + + await custom_client.post( + "/engines/", + json={ + "name": "spark", + "version": "2.4.4", + "dialect": "spark", + }, + ) + + # Setting the materialization config should succeed + response = await custom_client.post( + "/nodes/default.repair_orders_partitioned/materialization/", + json={ + "job": "spark_sql", + "strategy": "full", + "config": { + "partitions": [], + }, + "schedule": "0 * * * *", + }, + ) + data = response.json() + assert ( + data["message"] == "Successfully updated materialization config named " + "`spark_sql__full` for node `default.repair_orders_partitioned`" + ) + + response = await custom_client.get( + "/nodes/default.repair_orders_partitioned", + ) + result_sql = response.json()["materializations"][0]["config"]["query"] + match = re.search(format_regex, result_sql) + assert match and match.group("capture") == "dj_logical_timestamp" + query = re.sub(format_regex, "FORMATTED", result_sql) + compare_query_strings( + query, + "SELECT FORMATTED AS date_partition,\n\t" + "default_DOT_repair_orders.dispatched_date,\n\t" + "default_DOT_repair_orders.dispatcher_id,\n\t" + "default_DOT_repair_orders.hard_hat_id,\n\t" + "default_DOT_repair_orders.municipality_id,\n\t" + "default_DOT_repair_orders.order_date,\n\t" + "default_DOT_repair_orders.repair_order_id,\n\t" + "default_DOT_repair_orders.required_date \n" + " FROM roads.repair_orders AS " + "default_DOT_repair_orders \n" + " WHERE " + "date_format(default_DOT_repair_orders.order_date, " + "'yyyyMMdd') = FORMATTED\n\n", + ) + + @pytest.mark.asyncio + async def test_update_node_query_with_materializations( + self, + client_with_query_service_example_loader, + ): + """ + Testing updating a node's query when the node already has materializations. The node's + materializations should be updated based on the new query and rescheduled. + """ + custom_client = await client_with_query_service_example_loader(["BASIC"]) + await custom_client.post( + "/engines/", + json={ + "name": "spark", + "version": "2.4.4", + "dialect": "spark", + }, + ) + + await custom_client.post( + "/nodes/basic.transform.country_agg/materialization/", + json={ + "job": "spark_sql", + "strategy": "full", + "config": { + "spark": {}, + }, + "schedule": "0 * * * *", + }, + ) + await custom_client.patch( + "/nodes/basic.transform.country_agg/", + json={ + "query": ( + "SELECT country, COUNT(DISTINCT id) AS num_users, " + "COUNT(DISTINCT preferred_language) AS languages " + "FROM basic.source.users GROUP BY 1" + ), + }, + ) + response = await custom_client.get("/nodes/basic.transform.country_agg") + assert response.json()["version"] == "v2.0" + response = await custom_client.get("/nodes/basic.transform.country_agg/") + node_output = response.json() + assert node_output["materializations"] == [ + { + "backfills": [], + "config": { + "columns": [ + { + "column": None, + "name": "country", + "node": None, + "semantic_entity": None, + "semantic_type": None, + "type": "string", + }, + { + "column": None, + "name": "num_users", + "node": None, + "semantic_entity": None, + "semantic_type": None, + "type": "bigint", + }, + { + "column": None, + "name": "languages", + "node": None, + "semantic_entity": None, + "semantic_type": None, + "type": "bigint", + }, + ], + "lookback_window": None, + "query": mock.ANY, + "spark": {}, + "upstream_tables": ["public.basic.dim_users"], + }, + "strategy": "full", + "job": "SparkSqlMaterializationJob", + "name": "spark_sql__full", + "schedule": "0 * * * *", + "id": mock.ANY, + }, + ] + + @pytest.mark.asyncio + async def test_update_column_display_name(self, client_with_roads: AsyncClient): + """ + Test that updating a column display name works. + """ + response = await client_with_roads.patch( + url="/nodes/default.hard_hat/columns/hard_hat_id", + params={"display_name": "test"}, + ) + assert response.status_code == 201 + assert response.json() == { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "test", + "name": "hard_hat_id", + "type": "int", + "partition": None, + } + + @pytest.mark.asyncio + async def test_backfill_failures(self, client_with_query_service): + """Run backfill failure modes""" + + # Kick off backfill for non-existent materalization + response = await client_with_query_service.post( + "/nodes/default.hard_hat/materializations/non_existent/backfill", + json=[ + { + "column_name": "birth_date", + "range": ["20230101", "20230201"], + }, + ], + ) + assert ( + response.json()["message"] + == "Materialization with name non_existent not found" + ) + + +class TestNodeColumnsAttributes: + """ + Test ``POST /nodes/{name}/attributes/``. + """ + + @pytest.fixture + def create_source_node_payload(self) -> Dict[str, Any]: + """ + Payload for creating a source node. + """ + + return { + "name": "comments", + "description": "A fact table with comments", + "type": "source", + "columns": [ + {"name": "id", "type": "int"}, + { + "name": "user_id", + "type": "int", + "dimension": "basic.dimension.users", + }, + {"name": "event_timestamp", "type": "timestamp"}, + {"name": "post_processing_timestamp", "type": "timestamp"}, + {"name": "text", "type": "string"}, + ], + "mode": "published", + } + + @pytest_asyncio.fixture + async def catalog(self, session: AsyncSession) -> Catalog: + """ + A catalog fixture. + """ + + catalog = Catalog(name="postgres", uuid=uuid4()) + session.add(catalog) + await session.commit() + return catalog + + @pytest_asyncio.fixture + async def source_node(self, session: AsyncSession) -> Node: + """ + A source node fixture. + """ + node = Node( + name="basic.source.users", + type=NodeType.SOURCE, + current_version="1", + ) + node_revision = NodeRevision( + node=node, + name=node.name, + type=node.type, + version="1", + columns=[ + Column(name="id", type=IntegerType()), + Column(name="created_at", type=TimestampType()), + Column(name="full_name", type=StringType()), + Column(name="age", type=IntegerType()), + Column(name="country", type=StringType()), + Column(name="gender", type=StringType()), + Column(name="preferred_language", type=StringType()), + ], + ) + session.add(node_revision) + await session.commit() + return node + + async def set_id_primary_key(self, client_with_basic: AsyncClient): + """ + Helper function to set id as primary key on basic.dimension.users + """ + response = await client_with_basic.post( + "/nodes/basic.dimension.users/columns/id/attributes/", + json=[ + { + "namespace": "system", + "name": "primary_key", + }, + ], + ) + data = response.json() + assert data == [ + { + "name": "id", + "type": "int", + "display_name": "Id", + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "partition": None, + }, + ] + + @pytest.mark.asyncio + async def test_set_column_attributes( + self, + client_with_basic: AsyncClient, + ): + """ + Validate that setting column attributes on the node works. + """ + # Set id as primary key + await self.set_id_primary_key(client_with_basic) + + # Can set again (idempotent) + await self.set_id_primary_key(client_with_basic) + + # Set column attributes + response = await client_with_basic.post( + "/nodes/basic.dimension.users/columns/id/attributes/", + json=[ + { + "namespace": "system", + "name": "primary_key", + }, + ], + ) + data = response.json() + assert data == [ + { + "name": "id", + "type": "int", + "display_name": "Id", + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "partition": None, + }, + ] + + # Remove primary key attribute from column + response = await client_with_basic.post( + "/nodes/basic.source.comments/columns/id/attributes", + json=[], + ) + data = response.json() + assert data == [ + { + "name": "id", + "type": "int", + "display_name": "Id", + "attributes": [], + "dimension": None, + "partition": None, + }, + ] + + @pytest.mark.asyncio + async def test_set_column_hidden( + self, + client_with_basic: AsyncClient, + ): + """ + Test setting columns with the `hidden` attribute. + """ + response = await client_with_basic.post( + "/nodes/basic.dimension.users/columns/secret_number/attributes/", + json=[{"namespace": "system", "name": "hidden"}], + ) + data = response.json() + assert data == [ + { + "name": "secret_number", + "type": "float", + "display_name": "Secret Number", + "attributes": [ + {"attribute_type": {"name": "hidden", "namespace": "system"}}, + ], + "dimension": None, + "partition": None, + }, + ] + response = await client_with_basic.get( + "/nodes/basic.dimension.users/dimensions", + ) + column_attributes = {col["name"]: col["properties"] for col in response.json()} + assert column_attributes == { + "basic.dimension.users.age": [], + "basic.dimension.users.country": [], + "basic.dimension.users.created_at": [], + "basic.dimension.users.full_name": [], + "basic.dimension.users.gender": [], + "basic.dimension.users.id": ["primary_key"], + "basic.dimension.users.post_processing_timestamp": [], + "basic.dimension.users.preferred_language": [], + "basic.dimension.users.secret_number": ["hidden"], + } + + @pytest.mark.asyncio + async def test_set_columns_attributes_failed(self, client_with_basic: AsyncClient): + """ + Test setting column attributes with different failure modes. + """ + response = await client_with_basic.post( + "/nodes/basic.dimension.users/columns/created_at/attributes/", + json=[ + { + "namespace": "system", + "name": "dimension", + }, + ], + ) + data = response.json() + assert response.status_code == 422 + assert ( + data["message"] + == "Attribute type `system.dimension` not allowed on node type `dimension`!" + ) + + await client_with_basic.get( + "/nodes/basic.source.comments/", + ) + + response = await client_with_basic.post( + "/nodes/basic.source.comments/columns/nonexistent_col/attributes/", + json=[ + { + "name": "primary_key", + }, + ], + ) + assert response.status_code == 404 + data = response.json() + assert data == { + "message": "Column `nonexistent_col` does not exist on node `basic.source.comments`!", + "errors": [], + "warnings": [], + } + + response = await client_with_basic.post( + "/nodes/basic.source.comments/columns/id/attributes/", + json=[ + { + "name": "nonexistent_attribute", + }, + ], + ) + assert response.status_code == 404 + data = response.json() + assert data == { + "message": "Attribute type `system.nonexistent_attribute` does not exist!", + "errors": [], + "warnings": [], + } + + response = await client_with_basic.post( + "/nodes/basic.source.comments/columns/user_id/attributes/", + json=[ + { + "name": "primary_key", + }, + ], + ) + assert response.status_code == 201 + data = response.json() + assert [col for col in data if col["attributes"]] == [ + { + "name": "user_id", + "type": "int", + "display_name": "User Id", + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": {"name": "basic.dimension.users"}, + "partition": None, + }, + ] + + response = await client_with_basic.post( + "/attributes/", + json={ + "namespace": "example", + "name": "event_time", + "description": "Points to a column which represents the time of the event in a " + "given fact related node. Used to facilitate proper joins with dimension node " + "to match the desired effect.", + "allowed_node_types": ["source", "transform"], + "uniqueness_scope": ["node", "column_type"], + }, + ) + data = response.json() + + await client_with_basic.post( + "/nodes/basic.source.comments/columns/event_timestamp/attributes/", + json=[ + { + "namespace": "example", + "name": "event_time", + }, + ], + ) + + response = await client_with_basic.post( + "/nodes/basic.source.comments/columns/post_processing_timestamp/attributes/", + json=[ + { + "namespace": "example", + "name": "event_time", + }, + ], + ) + data = response.json() + assert data == { + "message": "The column attribute `event_time` is scoped to be unique to the " + "`['node', 'column_type']` level, but there is more than one column" + " tagged with it: `event_timestamp, post_processing_timestamp`", + "errors": [], + "warnings": [], + } + + await client_with_basic.post( + "/nodes/basic.source.comments/columns/event_timestamp/attributes/", + json=[], + ) + + response = await client_with_basic.get("/nodes/basic.source.comments/") + data = response.json() + assert data["columns"] == [ + { + "name": "id", + "type": "int", + "display_name": "Id", + "attributes": [], + "dimension": None, + "partition": None, + }, + { + "name": "user_id", + "type": "int", + "display_name": "User Id", + "attributes": [ + {"attribute_type": {"namespace": "system", "name": "primary_key"}}, + ], + "dimension": {"name": "basic.dimension.users"}, + "partition": None, + }, + { + "name": "timestamp", + "type": "timestamp", + "display_name": "Timestamp", + "attributes": [], + "dimension": None, + "partition": None, + }, + { + "name": "text", + "type": "string", + "display_name": "Text", + "attributes": [], + "dimension": None, + "partition": None, + }, + { + "name": "event_timestamp", + "type": "timestamp", + "display_name": "Event Timestamp", + "attributes": [], + "dimension": None, + "partition": None, + }, + { + "name": "created_at", + "type": "timestamp", + "display_name": "Created At", + "attributes": [], + "dimension": None, + "partition": None, + }, + { + "name": "post_processing_timestamp", + "type": "timestamp", + "display_name": "Post Processing Timestamp", + "attributes": [], + "dimension": None, + "partition": None, + }, + ] + + +class TestValidateNodes: + """ + Test ``POST /nodes/validate/``. + """ + + @pytest.mark.asyncio + async def test_validating_a_valid_node( + self, + client_with_account_revenue: AsyncClient, + ) -> None: + """ + Test validating a valid node + """ + response = await client_with_account_revenue.post( + "/nodes/validate/", + json={ + "name": "foo", + "description": "This is my foo transform node!", + "query": "SELECT payment_id FROM default.large_revenue_payments_only", + "type": "transform", + }, + ) + data = response.json() + + assert response.status_code == 200 + assert len(data) == 6 + assert data["columns"] == [ + { + "attributes": [], + "dimension": None, + "display_name": "Payment Id", + "name": "payment_id", + "partition": None, + "type": "int", + }, + ] + assert data["status"] == "valid" + assert data["dependencies"][0]["name"] == "default.large_revenue_payments_only" + assert data["message"] == "Node `foo` is valid." + assert data["missing_parents"] == [] + assert data["errors"] == [] + + @pytest.mark.asyncio + async def test_validating_an_invalid_node(self, client: AsyncClient) -> None: + """ + Test validating an invalid node + """ + + response = await client.post( + "/nodes/validate/", + json={ + "name": "foo", + "description": "This is my foo transform node!", + "query": "SELECT bar FROM large_revenue_payments_only", + "type": "transform", + }, + ) + data = response.json() + assert data["message"] == "Node `foo` is invalid." + assert [ + e + for e in data["errors"] + if e + == { + "code": 301, + "message": "Node definition contains references to nodes that do not exist: " + "large_revenue_payments_only", + "debug": {"missing_parents": ["large_revenue_payments_only"]}, + "context": "", + } + ] + + @pytest.mark.asyncio + async def test_validating_invalid_sql(self, client: AsyncClient) -> None: + """ + Test validating an invalid node with invalid SQL + """ + + response = await client.post( + "/nodes/validate/", + json={ + "name": "foo", + "description": "This is my foo transform node!", + "query": "SUPER invalid SQL query", + "type": "transform", + }, + ) + data = response.json() + + assert response.status_code == 422 + assert data["message"] == "Node `foo` is invalid." + assert data["status"] == "invalid" + assert data["errors"] == [ + { + "code": 201, + "message": ( + "Error parsing SQL `SUPER invalid SQL query`: " + "('Parse error 1:0:', \"mismatched input 'SUPER' expecting " + "{'(', 'ADD', 'ALTER', 'ANALYZE', 'CACHE', 'CLEAR', 'COMMENT', " + "'COMMIT', 'CREATE', 'DELETE', 'DESC', 'DESCRIBE', 'DFS', 'DROP', " + "'EXPLAIN', 'EXPORT', 'FROM', 'GRANT', 'IMPORT', 'INSERT', " + "'LIST', 'LOAD', 'LOCK', 'MAP', 'MERGE', 'MSCK', 'REDUCE', " + "'REFRESH', 'REPAIR', 'REPLACE', 'RESET', 'REVOKE', 'ROLLBACK', " + "'SELECT', 'SET', 'SHOW', 'START', 'TABLE', 'TRUNCATE', 'UNCACHE', " + "'UNLOCK', 'UPDATE', 'USE', 'VALUES', 'WITH'}\")" + ), + "debug": None, + "context": "", + }, + ] + + @pytest.mark.asyncio + async def test_validating_with_missing_parents(self, client: AsyncClient) -> None: + """ + Test validating a node with a query that has missing parents + """ + + response = await client.post( + "/nodes/validate/", + json={ + "name": "foo", + "description": "This is my foo transform node!", + "query": "SELECT 1 FROM node_that_does_not_exist", + "type": "transform", + }, + ) + data = response.json() + + assert response.status_code == 422 + assert data == { + "message": "Node `foo` is invalid.", + "status": "invalid", + "dependencies": [], + "missing_parents": ["node_that_does_not_exist"], + "columns": [ + { + "attributes": [], + "dimension": None, + "display_name": "Col0", + "name": "col0", + "partition": None, + "type": "int", + }, + ], + "errors": [ + { + "code": 301, + "message": "Node definition contains references to nodes that do not exist: " + "node_that_does_not_exist", + "debug": {"missing_parents": ["node_that_does_not_exist"]}, + "context": "", + }, + ], + } + + @pytest.mark.asyncio + async def test_allowing_missing_parents_for_draft_nodes( + self, + client: AsyncClient, + ) -> None: + """ + Test validating a draft node that's allowed to have missing parents + """ + + response = await client.post( + "/nodes/validate/", + json={ + "name": "foo", + "description": "This is my foo transform node!", + "query": "SELECT 1 FROM node_that_does_not_exist", + "type": "transform", + "mode": "draft", + }, + ) + data = response.json() + + assert response.status_code == 422 + assert data["message"] == "Node `foo` is invalid." + assert data["status"] == "invalid" + assert data["columns"] == [ + { + "attributes": [], + "dimension": None, + "display_name": "Col0", + "name": "col0", + "partition": None, + "type": "int", + }, + ] + assert data["missing_parents"] == ["node_that_does_not_exist"] + assert data["errors"] == [ + { + "code": 301, + "context": "", + "debug": {"missing_parents": ["node_that_does_not_exist"]}, + "message": "Node definition contains references to nodes that do not exist: " + "node_that_does_not_exist", + }, + ] + + @pytest.mark.asyncio + async def test_raise_when_trying_to_validate_a_source_node( + self, + client: AsyncClient, + ) -> None: + """ + Test validating a source node which is not possible + """ + + response = await client.post( + "/nodes/validate/", + json={ + "name": "foo", + "description": "This is my foo source node!", + "type": "source", + "columns": [ + {"name": "payment_id", "type": "int"}, + {"name": "payment_amount", "type": "float"}, + {"name": "customer_id", "type": "int"}, + {"name": "account_type", "type": "int"}, + ], + "tables": [ + { + "database_id": 1, + "catalog": "test", + "schema": "accounting", + "table": "revenue", + }, + ], + }, + ) + data = response.json() + + assert response.status_code == 422 + assert data == { + "message": "Source nodes cannot be validated", + "errors": [], + "warnings": [], + } + + @pytest.mark.asyncio + async def test_adding_dimensions_to_node_columns( + self, + client_example_loader, + ): + """ + Test linking dimensions to node columns + """ + custom_client = await client_example_loader(["ACCOUNT_REVENUE", "BASIC"]) + # Attach the payment_type dimension to the payment_type column on the revenue node + response = await custom_client.post( + "/nodes/default.revenue/columns/payment_type/?dimension=default.payment_type", + ) + data = response.json() + assert data == { + "message": ( + "Dimension node default.payment_type has been successfully " + "linked to node default.revenue using column payment_type." + ), + } + response = await custom_client.get("/nodes/default.revenue") + data = response.json() + assert [ + col["dimension"]["name"] for col in data["columns"] if col["dimension"] + ] == [] + + # Check that after deleting the dimension link, none of the columns have links + response = await custom_client.delete( + "/nodes/default.revenue/columns/payment_type/?dimension=default.payment_type", + ) + data = response.json() + assert data == { + "message": ( + "Dimension link default.payment_type to node default.revenue has " + "been removed." + ), + } + response = await custom_client.get("/nodes/default.revenue") + data = response.json() + assert data["dimension_links"] == [] + # assert all(col["dimension"] is None for col in data["columns"]) + response = await custom_client.get("/history?node=default.revenue") + assert [ + (activity["activity_type"], activity["entity_type"]) + for activity in response.json() + ] == [("delete", "link"), ("create", "link"), ("create", "node")] + + # Removing the dimension link again will result in no change + response = await custom_client.delete( + "/nodes/default.revenue/columns/payment_type/?dimension=default.payment_type", + ) + data = response.json() + assert response.status_code == 404 + assert data == { + "message": "Dimension link to node default.payment_type not found", + } + # Check history again, no change + response = await custom_client.get("/history?node=default.revenue") + assert [ + (activity["activity_type"], activity["entity_type"]) + for activity in response.json() + ] == [("delete", "link"), ("create", "link"), ("create", "node")] + + # Check that the proper error is raised when the column doesn't exist + response = await custom_client.post( + "/nodes/default.revenue/columns/non_existent_column/?dimension=default.payment_type", + ) + assert response.status_code == 404 + data = response.json() + assert data["message"] == ( + "Column non_existent_column does not exist on node default.revenue" + ) + + # Add a dimension including a specific dimension column name + response = await custom_client.post( + "/nodes/default.revenue/columns/payment_type/" + "?dimension=default.payment_type" + "&dimension_column=payment_type_name", + ) + assert response.status_code == 422 + data = response.json() + assert data["message"] == ( + "The column payment_type has type int and is being linked " + "to the dimension default.payment_type via the dimension column " + "payment_type_name, which has type string. These column " + "types are incompatible and the dimension cannot be linked" + ) + + response = await custom_client.post( + "/nodes/default.revenue/columns/payment_type/?dimension=basic.dimension.users", + ) + data = response.json() + assert data["message"] == ( + "Cannot link dimension to node, because catalogs do not match: default, public" + ) + + @pytest.mark.asyncio + async def test_update_node_with_dimension_links( + self, + client_with_roads: AsyncClient, + ): + """ + When a node is updated with a new query, the original dimension links and attributes + on its columns should be preserved where possible (that is, where the new and old + columns have the same names). + """ + await client_with_roads.patch( + "/nodes/default.hard_hat/", + json={ + "query": """ + SELECT + hard_hat_id, + title, + state + FROM default.hard_hats + """, + }, + ) + response = await client_with_roads.get("/nodes/default.hard_hat/dimensions") + dimensions = response.json() + assert [dim["name"] for dim in dimensions] == [ + "default.hard_hat.hard_hat_id", + "default.hard_hat.state", + "default.hard_hat.title", + "default.us_state.state_id", + "default.us_state.state_name", + "default.us_state.state_region", + "default.us_state.state_short", + ] + + response = await client_with_roads.get("/history?node=default.hard_hat") + history = response.json() + assert [ + (activity["activity_type"], activity["entity_type"]) for activity in history + ] == [ + ("update", "node"), + ("create", "link"), + ("set_attribute", "column_attribute"), + ("create", "node"), + ] + + response = (await client_with_roads.get("/nodes/default.hard_hat")).json() + assert response["columns"] == [ + { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "Hard Hat Id", + "name": "hard_hat_id", + "type": "int", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Title", + "name": "title", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "State", + "name": "state", + "type": "string", + "partition": None, + }, + ] + + # Check history of the node with column attribute set + response = await client_with_roads.get( + "/history?node=default.hard_hat", + ) + history = response.json() + assert [ + (activity["activity_type"], activity["entity_type"]) for activity in history + ] == [ + ("update", "node"), + ("create", "link"), + ("set_attribute", "column_attribute"), + ("create", "node"), + ] + + response = await client_with_roads.patch( + "/nodes/default.hard_hat/", + json={ + "query": """ + SELECT + hard_hat_id, + title + FROM default.hard_hats + """, + }, + ) + response = await client_with_roads.get("/nodes/default.hard_hat/dimensions") + dimensions = response.json() + assert [dim["name"] for dim in dimensions] == [ + "default.hard_hat.hard_hat_id", + "default.hard_hat.title", + ] + + @pytest.mark.asyncio + async def test_propagate_update_downstream( + self, + client_with_roads: AsyncClient, + ): + """ + Tests that propagating updates downstream preserves dimension links + """ + # Extract existing dimension links on transform + response = await client_with_roads.get("/nodes/default.repair_orders_fact") + existing_dimension_links = response.json()["dimension_links"] + + # Update one of the transform's parents + response = await client_with_roads.patch( + "/nodes/default.repair_order_details", + json={ + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "repair_type_id", "type": "int"}, + {"name": "discount", "type": "float"}, + ], + }, + ) + assert response.status_code == 200 + + # Check that the transform's original dimension links remain after the parent node's + # update has been propagated to the transform + response = await client_with_roads.get("/nodes/default.repair_orders_fact") + data = response.json() + assert sorted( + data["dimension_links"], + key=lambda key: key["dimension"]["name"], + ) == sorted( + existing_dimension_links, + key=lambda key: key["dimension"]["name"], + ) + assert data["status"] == "invalid" + + @pytest.mark.asyncio + async def test_update_dimension_remove_pk_column( + self, + client_with_roads: AsyncClient, + ): + """ + When a dimension node is updated with a new query that removes the original primary key + column, either a new primary key must be set or the node will be set to invalid. + """ + response = await client_with_roads.patch( + "/nodes/default.hard_hat/", + json={ + "query": """ + SELECT + title, + state + FROM default.hard_hats + """, + # "primary_key": ["title"], + }, + ) + assert response.json()["status"] == "invalid" + response = await client_with_roads.patch( + "/nodes/default.hard_hat/", + json={ + "query": """ + SELECT + title, + state + FROM default.hard_hats + """, + "primary_key": ["title"], + }, + ) + assert response.json()["status"] == "valid" + + @pytest.mark.asyncio + async def test_node_downstreams(self, client_with_event: AsyncClient): + """ + Test getting downstream nodes of different node types. + """ + response = await client_with_event.get( + "/nodes/default.event_source/downstream/?node_type=metric", + ) + data = response.json() + assert {node["name"] for node in data} == { + "default.long_events_distinct_countries", + "default.device_ids_count", + } + + response = await client_with_event.get( + "/nodes/default.event_source/downstream/?node_type=transform", + ) + data = response.json() + assert {node["name"] for node in data} == {"default.long_events"} + + response = await client_with_event.get( + "/nodes/default.event_source/downstream/?node_type=dimension", + ) + data = response.json() + assert {node["name"] for node in data} == {"default.country_dim"} + + response = await client_with_event.get( + "/nodes/default.event_source/downstream/", + ) + data = response.json() + assert {node["name"] for node in data} == { + "default.long_events_distinct_countries", + "default.device_ids_count", + "default.long_events", + "default.country_dim", + } + + # Test depth limiting + response = await client_with_event.get( + "/nodes/default.event_source/downstream/?depth=2", + ) + data = response.json() + assert {node["name"] for node in data} == { + "default.long_events_distinct_countries", + "default.device_ids_count", + "default.long_events", + "default.country_dim", + } + response = await client_with_event.get( + "/nodes/default.event_source/downstream/?depth=1", + ) + data = response.json() + assert {node["name"] for node in data} == { + "default.long_events", + "default.country_dim", + "default.device_ids_count", + } + response = await client_with_event.get( + "/nodes/default.event_source/downstream/?depth=0", + ) + data = response.json() + assert {node["name"] for node in data} == set() + + response = await client_with_event.get( + "/nodes/default.device_ids_count/downstream/", + ) + data = response.json() + assert data == [] + + response = await client_with_event.get("/nodes/default.long_events/downstream/") + data = response.json() + assert {node["name"] for node in data} == { + "default.long_events_distinct_countries", + } + + @pytest.mark.asyncio + async def test_node_upstreams(self, client_with_event: AsyncClient): + """ + Test getting upstream nodes of different node types. + """ + response = await client_with_event.get( + "/nodes/default.long_events_distinct_countries/upstream/", + ) + data = response.json() + assert {node["name"] for node in data} == { + "default.event_source", + "default.long_events", + } + + @pytest.mark.asyncio + async def test_list_node_dag(self, client_example_loader): + """ + Test getting the DAG for a node + """ + custom_client = await client_example_loader(["EVENT", "ROADS"]) + response = await custom_client.get( + "/nodes/default.long_events_distinct_countries/dag", + ) + data = response.json() + assert {node["name"] for node in data} == { + "default.country_dim", + "default.event_source", + "default.long_events", + "default.long_events_distinct_countries", + } + + response = await custom_client.get("/nodes/default.num_repair_orders/dag") + data = response.json() + assert {node["name"] for node in data} == { + "default.dispatcher", + "default.hard_hat", + "default.hard_hat_to_delete", + "default.municipality_dim", + "default.num_repair_orders", + "default.repair_order_details", + "default.repair_orders", + "default.repair_orders_fact", + "default.us_state", + } + + @pytest.mark.asyncio + async def test_node_column_lineage(self, client_with_roads: AsyncClient): + """ + Test endpoint to retrieve a node's column-level lineage + """ + response = await client_with_roads.get( + "/nodes/default.num_repair_orders/lineage/", + ) + assert response.json() == [ + { + "column_name": "default_DOT_num_repair_orders", + "display_name": "Num Repair Orders", + "lineage": [ + { + "column_name": "repair_order_id", + "display_name": "Repair Orders Fact", + "lineage": [ + { + "column_name": "repair_order_id", + "display_name": "default.roads.repair_orders", + "lineage": [], + "node_name": "default.repair_orders", + "node_type": "source", + }, + ], + "node_name": "default.repair_orders_fact", + "node_type": "transform", + }, + ], + "node_name": "default.num_repair_orders", + "node_type": "metric", + }, + ] + + await client_with_roads.post( + "/nodes/metric/", + json={ + "name": "default.discounted_repair_orders", + "query": ( + """ + SELECT + cast(sum(if(discount > 0.0, 1, 0)) as double) / count(repair_order_id) + FROM default.repair_order_details + """ + ), + "mode": "published", + "description": "Discounted Repair Orders", + }, + ) + response = await client_with_roads.get( + "/nodes/default.discounted_repair_orders/lineage/", + ) + assert response.json() == [ + { + "column_name": "default_DOT_discounted_repair_orders", + "node_name": "default.discounted_repair_orders", + "node_type": "metric", + "display_name": "Discounted Repair Orders", + "lineage": [ + { + "column_name": "repair_order_id", + "node_name": "default.repair_order_details", + "node_type": "source", + "display_name": "default.roads.repair_order_details", + "lineage": [], + }, + { + "column_name": "discount", + "node_name": "default.repair_order_details", + "node_type": "source", + "display_name": "default.roads.repair_order_details", + "lineage": [], + }, + ], + }, + ] + + @pytest.mark.asyncio + async def test_revalidating_existing_nodes(self, client_with_roads: AsyncClient): + """ + Test revalidating all example nodes and confirm that they are set to valid + """ + await client_with_roads.post( + "/nodes/cube/", + json={ + "metrics": [ + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + ], + "dimensions": [ + "default.hard_hat.hire_date", + "default.hard_hat.state", + "default.dispatcher.company_name", + ], + "filters": [], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.repairs_cube", + }, + ) + for node in (await client_with_roads.get("/nodes/")).json(): + status = ( + await client_with_roads.post( + f"/nodes/{node}/validate/", + ) + ).json()["status"] + assert status == "valid" + # Confirm that they still show as valid server-side + for node in (await client_with_roads.get("/nodes/")).json(): + node = (await client_with_roads.get(f"/nodes/{node}")).json() + assert node["status"] == "valid" + + @pytest.mark.asyncio + async def test_lineage_on_complex_transforms(self, client_with_roads: AsyncClient): + """ + Test metric lineage on more complex transforms and metrics + """ + response = ( + await client_with_roads.get("/nodes/default.regional_level_agg/") + ).json() + assert response["columns"] == [ + { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "Us Region Id", + "name": "us_region_id", + "type": "int", + "partition": None, + }, + { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "State Name", + "name": "state_name", + "type": "string", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Location Hierarchy", + "name": "location_hierarchy", + "type": "string", + "partition": None, + }, + { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "Order Year", + "name": "order_year", + "type": "int", + "partition": None, + }, + { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "Order Month", + "name": "order_month", + "type": "int", + "partition": None, + }, + { + "attributes": [ + {"attribute_type": {"name": "primary_key", "namespace": "system"}}, + ], + "dimension": None, + "display_name": "Order Day", + "name": "order_day", + "type": "int", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Completed Repairs", + "name": "completed_repairs", + "type": "bigint", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Total Repairs Dispatched", + "name": "total_repairs_dispatched", + "type": "bigint", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Total Amount In Region", + "name": "total_amount_in_region", + "type": "double", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Avg Repair Amount In Region", + "name": "avg_repair_amount_in_region", + "type": "double", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Avg Dispatch Delay", + "name": "avg_dispatch_delay", + "type": "double", + "partition": None, + }, + { + "attributes": [], + "dimension": None, + "display_name": "Unique Contractors", + "name": "unique_contractors", + "type": "bigint", + "partition": None, + }, + ] + + response = ( + await client_with_roads.get( + "/nodes/default.regional_repair_efficiency/", + ) + ).json() + assert response["columns"] == [ + { + "attributes": [], + "dimension": None, + "display_name": "Regional Repair Efficiency", + "name": "default_DOT_regional_repair_efficiency", + "type": "double", + "partition": None, + }, + ] + response = ( + await client_with_roads.get( + "/nodes/default.regional_repair_efficiency/lineage/", + ) + ).json() + assert response == [ + { + "column_name": "default_DOT_regional_repair_efficiency", + "node_name": "default.regional_repair_efficiency", + "node_type": "metric", + "display_name": "Regional Repair Efficiency", + "lineage": [ + { + "column_name": "total_amount_nationwide", + "node_name": "default.national_level_agg", + "node_type": "transform", + "display_name": "National Level Agg", + "lineage": [ + { + "column_name": "quantity", + "node_name": "default.repair_order_details", + "node_type": "source", + "display_name": "default.roads.repair_order_details", + "lineage": [], + }, + { + "column_name": "price", + "node_name": "default.repair_order_details", + "node_type": "source", + "display_name": "default.roads.repair_order_details", + "lineage": [], + }, + ], + }, + { + "column_name": "total_amount_in_region", + "node_name": "default.regional_level_agg", + "node_type": "transform", + "display_name": "Regional Level Agg", + "lineage": [ + { + "column_name": "quantity", + "node_name": "default.repair_order_details", + "node_type": "source", + "display_name": "default.roads.repair_order_details", + "lineage": [], + }, + { + "column_name": "price", + "node_name": "default.repair_order_details", + "node_type": "source", + "display_name": "default.roads.repair_order_details", + "lineage": [], + }, + ], + }, + { + "column_name": "total_repairs_dispatched", + "node_name": "default.regional_level_agg", + "node_type": "transform", + "display_name": "Regional Level Agg", + "lineage": [ + { + "column_name": "repair_order_id", + "node_name": "default.repair_orders", + "node_type": "source", + "display_name": "default.roads.repair_orders", + "lineage": [], + }, + ], + }, + { + "column_name": "completed_repairs", + "node_name": "default.regional_level_agg", + "node_type": "transform", + "display_name": "Regional Level Agg", + "lineage": [ + { + "column_name": "repair_order_id", + "node_name": "default.repair_orders", + "node_type": "source", + "display_name": "default.roads.repair_orders", + "lineage": [], + }, + { + "column_name": "dispatched_date", + "node_name": "default.repair_orders", + "node_type": "source", + "display_name": "default.roads.repair_orders", + "lineage": [], + }, + ], + }, + ], + }, + ] + + +@pytest.mark.asyncio +async def test_node_similarity( + session: AsyncSession, + client: AsyncClient, + current_user: User, +): + """ + Test determining node similarity based on their queries + """ + source_data = Node( + name="source_data", + type=NodeType.SOURCE, + current_version="1", + created_by_id=current_user.id, + ) + source_data_rev = NodeRevision( + node=source_data, + version="1", + name=source_data.name, + type=source_data.type, + created_by_id=current_user.id, + ) + a_transform = Node( + name="a_transform", + type=NodeType.TRANSFORM, + current_version="1", + created_by_id=current_user.id, + ) + a_transform_rev = NodeRevision( + name=a_transform.name, + node=a_transform, + version="1", + query="SELECT 1 as num", + type=a_transform.type, + columns=[ + Column(name="num", type=IntegerType()), + ], + created_by_id=current_user.id, + ) + another_transform = Node( + name="another_transform", + type=NodeType.TRANSFORM, + current_version="1", + created_by_id=current_user.id, + ) + another_transform_rev = NodeRevision( + name=another_transform.name, + node=another_transform, + version="1", + query="SELECT 1 as num", + type=another_transform.type, + columns=[ + Column(name="num", type=IntegerType()), + ], + created_by_id=current_user.id, + ) + yet_another_transform = Node( + name="yet_another_transform", + type=NodeType.TRANSFORM, + current_version="1", + created_by_id=current_user.id, + ) + yet_another_transform_rev = NodeRevision( + name=yet_another_transform.name, + node=yet_another_transform, + version="1", + query="SELECT 2 as num", + type=yet_another_transform.type, + columns=[ + Column(name="num", type=IntegerType()), + ], + created_by_id=current_user.id, + ) + session.add(source_data_rev) + session.add(a_transform_rev) + session.add(another_transform_rev) + session.add(yet_another_transform_rev) + await session.commit() + + response = await client.get("/nodes/similarity/a_transform/another_transform") + assert response.status_code == 200 + data = response.json() + assert data["similarity"] == 1.0 + + response = await client.get("/nodes/similarity/a_transform/yet_another_transform") + assert response.status_code == 200 + data = response.json() + assert data["similarity"] == 0.75 + + response = await client.get( + "/nodes/similarity/yet_another_transform/another_transform", + ) + assert response.status_code == 200 + data = response.json() + assert data["similarity"] == 0.75 + + # Check that the proper error is raised when using a source node + response = await client.get("/nodes/similarity/a_transform/source_data") + assert response.status_code == 409 + data = response.json() + assert data == { + "message": "Cannot determine similarity of source nodes", + "errors": [], + "warnings": [], + } + + +@pytest.mark.asyncio +async def test_resolving_downstream_status( + client_with_service_setup: AsyncClient, +) -> None: + """ + Test creating and updating a source node + """ + # Create draft transform and metric nodes with missing parents + transform1 = { + "name": "default.comments_by_migrated_users", + "description": "Comments by users who have already migrated", + "query": "SELECT id, user_id FROM default.comments WHERE text LIKE '%migrated%'", + "mode": "draft", + } + + transform2 = { + "name": "default.comments_by_users_pending_a_migration", + "description": "Comments by users who have a migration pending", + "query": "SELECT id, user_id FROM default.comments WHERE text LIKE '%migration pending%'", + "mode": "draft", + } + + transform3 = { + "name": "default.comments_by_users_partially_migrated", + "description": "Comments by users are partially migrated", + "query": ( + "SELECT p.id, p.user_id FROM default.comments_by_users_pending_a_migration p " + "INNER JOIN default.comments_by_migrated_users m ON p.user_id = m.user_id" + ), + "mode": "draft", + } + + transform4 = { + "name": "default.comments_by_banned_users", + "description": "Comments by users are partially migrated", + "query": ( + "SELECT id, user_id FROM default.comments AS comment " + "INNER JOIN default.banned_users AS banned_users " + "ON comments.user_id = banned_users.banned_user_id" + ), + "mode": "draft", + } + + transform5 = { + "name": "default.comments_by_users_partially_migrated_sample", + "description": "Sample of comments by users are partially migrated", + "query": "SELECT id, user_id, foo FROM default.comments_by_users_partially_migrated", + "mode": "draft", + } + + metric1 = { + "name": "default.number_of_migrated_users", + "description": "Number of migrated users", + "query": "SELECT COUNT(DISTINCT user_id) FROM default.comments_by_migrated_users", + "mode": "draft", + } + + metric2 = { + "name": "default.number_of_users_with_pending_migration", + "description": "Number of users with a migration pending", + "query": ( + "SELECT COUNT(DISTINCT user_id) FROM " + "default.comments_by_users_pending_a_migration" + ), + "mode": "draft", + } + + metric3 = { + "name": "default.number_of_users_partially_migrated", + "description": "Number of users partially migrated", + "query": "SELECT COUNT(DISTINCT user_id) FROM default.comments_by_users_partially_migrated", + "mode": "draft", + } + + for node, node_type in [ + (transform1, NodeType.TRANSFORM), + (transform2, NodeType.TRANSFORM), + (transform3, NodeType.TRANSFORM), + (transform4, NodeType.TRANSFORM), + (transform5, NodeType.TRANSFORM), + (metric1, NodeType.METRIC), + (metric2, NodeType.METRIC), + (metric3, NodeType.METRIC), + ]: + response = await client_with_service_setup.post( + f"/nodes/{node_type.value}/", + json=node, + ) + assert response.status_code == 201 + data = response.json() + assert data["name"] == node["name"] + assert data["mode"] == node["mode"] + assert data["status"] == "invalid" + + # Add the missing parent + missing_parent_node = { + "name": "default.comments", + "description": "A fact table with comments", + "columns": [ + {"name": "id", "type": "int"}, + {"name": "user_id", "type": "int"}, + {"name": "timestamp", "type": "timestamp"}, + {"name": "text", "type": "string"}, + ], + "mode": "published", + "catalog": "public", + "schema_": "basic", + "table": "comments", + } + + response = await client_with_service_setup.post( + "/nodes/source/", + json=missing_parent_node, + ) + assert response.status_code == 200 + data = response.json() + assert data["name"] == missing_parent_node["name"] + assert data["mode"] == missing_parent_node["mode"] + assert data["status"] == "valid" + + # Check that downstream nodes have now been switched to a "valid" status + for node in [transform1, transform2, transform3, metric1, metric2, metric3]: + response = await client_with_service_setup.get(f"/nodes/{node['name']}/") + assert response.status_code == 200 + data = response.json() + assert data["name"] == node["name"] + assert data["mode"] == node["mode"] # make sure the mode hasn't been changed + assert ( + data["status"] == "valid" + ) # make sure the node's status has been updated to valid + + # Check that nodes still not valid have an invalid status + for node in [transform4, transform5]: + response = await client_with_service_setup.get(f"/nodes/{node['name']}/") + assert response.status_code == 200 + data = response.json() + assert data["name"] == node["name"] + assert data["mode"] == node["mode"] # make sure the mode hasn't been changed + assert data["status"] == "invalid" + + +def test_decompose_expression(): + """ + Verify metric expression decomposition into measures for cubes + """ + res = decompose_expression(ast.Number(value=5.5)) + assert res == (ast.Number(value=5.5), []) + + # Decompose `avg(orders)` + res = decompose_expression( + ast.Function(ast.Name("avg"), args=[ast.Column(ast.Name("orders"))]), + ) + assert str(res[0]) == "sum(orders3845127662_sum) / count(orders3845127662_count)" + assert [measure.alias_or_name.name for measure in res[1]] == [ + "orders3845127662_sum", + "orders3845127662_count", + ] + + # Decompose `avg(orders) + 5.5` + res = decompose_expression( + ast.BinaryOp( + left=ast.Function(ast.Name("avg"), args=[ast.Column(ast.Name("orders"))]), + right=ast.Number(value=5.5), + op=ast.BinaryOpKind.Plus, + ), + ) + assert ( + str(res[0]) == "sum(orders3845127662_sum) / count(orders3845127662_count) + 5.5" + ) + assert [measure.alias_or_name.name for measure in res[1]] == [ + "orders3845127662_sum", + "orders3845127662_count", + ] + + # Decompose `max(avg(orders_a) + avg(orders_b))` + res = decompose_expression( + ast.Function( + ast.Name("max"), + args=[ + ast.BinaryOp( + op=ast.BinaryOpKind.Plus, + left=ast.Function( + ast.Name("avg"), + args=[ast.Column(ast.Name("orders_a"))], + ), + right=ast.Function( + ast.Name("avg"), + args=[ast.Column(ast.Name("orders_b"))], + ), + ), + ], + ), + ) + assert ( + str(res[0]) + == "max(sum(orders_a1170126662_sum) / count(orders_a1170126662_count) " + "+ sum(orders_b3703039740_sum) / count(orders_b3703039740_count))" + ) + + # Decompose `sum(max(orders))` + res = decompose_expression( + ast.Function( + ast.Name("sum"), + args=[ + ast.Function( + ast.Name("max"), + args=[ast.Column(ast.Name("orders"))], + ), + ], + ), + ) + assert str(res[0]) == "sum(max(orders3845127662_max))" + assert [measure.alias_or_name.name for measure in res[1]] == [ + "orders3845127662_max", + ] + + # Decompose `(max(orders) + min(validations))/sum(total)` + res = decompose_expression( + ast.BinaryOp( + left=ast.BinaryOp( + left=ast.Function( + ast.Name("max"), + args=[ast.Column(ast.Name("orders"))], + ), + right=ast.Function( + ast.Name("min"), + args=[ast.Column(ast.Name("validations"))], + ), + op=ast.BinaryOpKind.Plus, + ), + right=ast.Function(ast.Name("sum"), args=[ast.Column(ast.Name("total"))]), + op=ast.BinaryOpKind.Divide, + ), + ) + assert ( + str(res[0]) + == "max(orders3845127662_max) + min(validations2970758927_min) / sum(total3257917790_sum)" + ) + assert [measure.alias_or_name.name for measure in res[1]] == [ + "orders3845127662_max", + "validations2970758927_min", + "total3257917790_sum", + ] + + # Decompose `cast(sum(coalesce(has_ordered, 0.0)) as double)/count(total)` + res = decompose_expression( + ast.BinaryOp( + left=ast.Cast( + expression=ast.Function( + name=ast.Name("sum"), + args=[ + ast.Function( + ast.Name("coalesce"), + args=[ast.Column(ast.Name("has_ordered")), ast.Number(0.0)], + ), + ], + ), + data_type=types.DoubleType(), + ), + right=ast.Function( + name=ast.Name("sum"), + args=[ast.Column(ast.Name("total"))], + ), + op=ast.BinaryOpKind.Divide, + ), + ) + assert str(res[0]) == "sum(has_ordered2766370626_sum) / sum(total3257917790_sum)" + assert [measure.alias_or_name.name for measure in res[1]] == [ + "has_ordered2766370626_sum", + "total3257917790_sum", + ] + + +@pytest.mark.asyncio +async def test_list_dimension_attributes(client_with_roads: AsyncClient) -> None: + """ + Test that listing dimension attributes for any node works. + """ + response = await client_with_roads.get( + "/nodes/default.regional_level_agg/dimensions/", + ) + assert response.status_code in (200, 201) + assert response.json() == [ + { + "filter_only": False, + "name": "default.regional_level_agg.order_day", + "node_display_name": "Regional Level Agg", + "node_name": "default.regional_level_agg", + "path": [], + "type": "int", + "properties": ["primary_key"], + }, + { + "filter_only": False, + "name": "default.regional_level_agg.order_month", + "node_display_name": "Regional Level Agg", + "node_name": "default.regional_level_agg", + "path": [], + "type": "int", + "properties": ["primary_key"], + }, + { + "filter_only": False, + "name": "default.regional_level_agg.order_year", + "node_display_name": "Regional Level Agg", + "node_name": "default.regional_level_agg", + "path": [], + "type": "int", + "properties": ["primary_key"], + }, + { + "filter_only": False, + "name": "default.regional_level_agg.state_name", + "node_display_name": "Regional Level Agg", + "node_name": "default.regional_level_agg", + "path": [], + "type": "string", + "properties": ["primary_key"], + }, + { + "filter_only": False, + "name": "default.regional_level_agg.us_region_id", + "node_display_name": "Regional Level Agg", + "node_name": "default.regional_level_agg", + "path": [], + "type": "int", + "properties": ["primary_key"], + }, + { + "filter_only": True, + "name": "default.repair_order.repair_order_id", + "node_display_name": "Repair Order", + "node_name": "default.repair_order", + "path": ["default.repair_orders"], + "type": "int", + "properties": ["primary_key"], + }, + { + "filter_only": True, + "name": "default.dispatcher.dispatcher_id", + "node_display_name": "Dispatcher", + "node_name": "default.dispatcher", + "path": ["default.repair_orders"], + "type": "int", + "properties": ["primary_key"], + }, + { + "filter_only": True, + "name": "default.repair_order.repair_order_id", + "node_display_name": "Repair Order", + "node_name": "default.repair_order", + "path": ["default.repair_order_details"], + "type": "int", + "properties": ["primary_key"], + }, + { + "filter_only": True, + "name": "default.contractor.contractor_id", + "node_display_name": "Contractor", + "node_name": "default.contractor", + "path": ["default.repair_type"], + "type": "int", + "properties": ["primary_key"], + }, + { + "filter_only": True, + "name": "default.us_state.state_short", + "node_display_name": "Us State", + "node_name": "default.us_state", + "path": [ + "default.contractors", + ], + "type": "string", + "properties": ["primary_key"], + }, + ] + + +@pytest.mark.asyncio +async def test_cycle_detection_dimensions_graph(client_with_roads: AsyncClient) -> None: + """ + Test that getting the dimensions graph detects cycles and does not continue with + infinite recursion. + """ + response = await client_with_roads.post( + "/nodes/transform", + json={ + "description": "Events", + "query": """ + SELECT + 1 AS event_id, + 2 AS user_id + """, + "mode": "published", + "name": "default.events", + "primary_key": ["event_id"], + }, + ) + response = await client_with_roads.post( + "/nodes/dimension", + json={ + "description": "User dimension", + "query": """ + SELECT + 1 AS user_id, + 2 AS birth_country + """, + "mode": "published", + "name": "default.user", + "primary_key": ["user_id"], + }, + ) + assert response.status_code == 201 + response = await client_with_roads.post( + "/nodes/dimension", + json={ + "description": "Country dimension", + "query": """ + SELECT + 1 AS country_id, + 2 AS user_id + """, + "mode": "published", + "name": "default.country", + "primary_key": ["country_id"], + }, + ) + assert response.status_code == 201 + + # Create dimension links that are in a cycle + response = await client_with_roads.post( + "/nodes/default.user/link", + json={ + "dimension_node": "default.country", + "join_type": "left", + "join_on": ("default.user.birth_country = default.country.country_id"), + }, + ) + assert response.status_code == 201 + response = await client_with_roads.post( + "/nodes/default.country/link", + json={ + "dimension_node": "default.user", + "join_type": "left", + "join_on": ("default.user.user_id = default.country.user_id"), + }, + ) + assert response.status_code == 201 + response = await client_with_roads.post( + "/nodes/default.events/link", + json={ + "dimension_node": "default.user", + "join_type": "left", + "join_on": ("default.events.user_id = default.user.user_id"), + }, + ) + assert response.status_code == 201 + + # Requesting dimensions for any of the above nodes should have a finite end + response = await client_with_roads.get("/nodes/default.user/dimensions") + assert [ + " -> ".join(dim["path"] + [""]) + dim["name"] for dim in response.json() + ] == [ + "default.user -> default.country.country_id", + "default.user -> default.country -> default.user -> default.country.country_id", + "default.user -> default.country.user_id", + "default.user -> default.country -> default.user -> default.country.user_id", + "default.user.birth_country", + "default.user -> default.country -> default.user.birth_country", + "default.user.user_id", + "default.user -> default.country -> default.user.user_id", + ] + response = await client_with_roads.get("/nodes/default.events/dimensions") + assert [ + " -> ".join(dim["path"] + [""]) + dim["name"] for dim in response.json() + ] == [ + "default.events -> default.user -> default.country.country_id", + "default.events -> default.user -> default.country.user_id", + "default.events.event_id", + "default.events -> default.user.birth_country", + "default.events -> default.user -> default.country -> default.user.birth_country", + "default.events -> default.user.user_id", + "default.events -> default.user -> default.country -> default.user.user_id", + ] + + +@pytest.mark.asyncio +async def test_set_column_partition(client_with_roads: AsyncClient): + """ + Test setting temporal and categorical partitions on node + """ + # Set hire_date to temporal + response = await client_with_roads.post( + "/nodes/default.hard_hat/columns/hire_date/partition", + json={ + "type_": "temporal", + "granularity": "hour", + "format": "yyyyMMddHH", + }, + ) + assert response.json() == { + "attributes": [], + "dimension": None, + "display_name": "Hire Date", + "name": "hire_date", + "partition": { + "expression": None, + "format": "yyyyMMddHH", + "type_": "temporal", + "granularity": "hour", + }, + "type": "timestamp", + } + + # Set state to categorical + response = await client_with_roads.post( + "/nodes/default.hard_hat/columns/state/partition", + json={ + "type_": "categorical", + }, + ) + assert response.json() == { + "attributes": [], + "dimension": None, + "display_name": "State", + "name": "state", + "partition": { + "expression": None, + "type_": "categorical", + "format": None, + "granularity": None, + }, + "type": "string", + } + + # Attempt to set country to temporal (missing granularity) + response = await client_with_roads.post( + "/nodes/default.hard_hat/columns/country/partition", + json={ + "type_": "temporal", + }, + ) + assert ( + response.json()["message"] + == "The granularity must be provided for temporal partitions. One of: " + "['SECOND', 'MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'QUARTER', " + "'YEAR']" + ) + + # Attempt to set country to temporal (missing format) + response = await client_with_roads.post( + "/nodes/default.hard_hat/columns/country/partition", + json={ + "type_": "temporal", + "granularity": "day", + }, + ) + assert ( + response.json()["message"] + == "The temporal partition column's datetime format must be provided." + ) + + # Set country to temporal + await client_with_roads.post( + "/nodes/default.hard_hat/columns/country/partition", + json={ + "type_": "temporal", + "granularity": "day", + "format": "yyyyMMdd", + }, + ) + + # Update country to categorical + response = await client_with_roads.post( + "/nodes/default.hard_hat/columns/country/partition", + json={ + "type_": "categorical", + "expression": "", + }, + ) + assert response.json() == { + "attributes": [], + "dimension": None, + "display_name": "Country", + "name": "country", + "partition": { + "expression": None, + "type_": "categorical", + "format": None, + "granularity": None, + }, + "type": "string", + } + + +@pytest.mark.asyncio +async def test_delete_recreate_for_all_nodes(client_with_roads: AsyncClient): + """ + Test deleting and recreating for all node types + """ + # Delete a source node + await client_with_roads.delete("/nodes/default.dispatchers") + # Recreating it should succeed + response = await client_with_roads.post( + "/nodes/source", + json={ + "columns": [ + {"name": "dispatcher_id", "type": "int"}, + {"name": "company_name", "type": "string"}, + {"name": "phone", "type": "string"}, + ], + "description": "Information on dispatchers", + "mode": "published", + "name": "default.dispatchers", + "catalog": "default", + "schema_": "roads", + "table": "dispatchers", + }, + ) + assert response.json()["version"] == "v2.0" + response = await client_with_roads.get("/history?node=default.dispatchers") + assert [activity["activity_type"] for activity in response.json()] == [ + "restore", + "update", + "delete", + "create", + ] + await client_with_roads.patch( + "/nodes/default.dispatcher", + json={"primary_key": ["dispatcher_id"]}, + ) + + # Delete a dimension node + await client_with_roads.delete("/nodes/default.us_state") + # Trying to create a transform node with the same name will fail + response = await client_with_roads.post( + "/nodes/transform", + json={ + "description": "US state transform", + "query": """SELECT + state_id, + state_name, + state_abbr AS state_short +FROM default.us_states s +LEFT JOIN default.us_region r +ON s.state_region = r.us_region_id""", + "mode": "published", + "name": "default.us_state", + "primary_key": ["state_id"], + }, + ) + assert response.json()["message"] == ( + "A node with name `default.us_state` of a `dimension` type existed " + "before. If you want to re-create it with a different type, you " + "need to remove all traces of the previous node with a hard delete call: " + "DELETE /nodes/{node_name}/hard" + ) + # Trying to create a dimension node with the same name but an updated query will succeed + response = await client_with_roads.post( + "/nodes/dimension", + json={ + "description": "US state", + "query": """SELECT + state_id, + state_name, + state_abbr AS state_short +FROM default.us_states s +LEFT JOIN default.us_region r +ON s.state_region = r.us_region_id""", + "mode": "published", + "name": "default.us_state", + "primary_key": ["state_id"], + }, + ) + node_data = response.json() + assert node_data["version"] == "v2.0" + response = await client_with_roads.get("/history?node=default.us_state") + assert [activity["activity_type"] for activity in response.json()] == [ + "restore", + "update", + "delete", + "set_attribute", + "create", + ] + + create_cube_payload = { + "metrics": [ + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + ], + "dimensions": [ + "default.hard_hat.country", + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + ], + "filters": ["default.hard_hat.state='AZ'"], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.repairs_cube", + } + await client_with_roads.post( + "/nodes/cube/", + json=create_cube_payload, + ) + await client_with_roads.delete("/nodes/default.repairs_cube") + await client_with_roads.post( + "/nodes/cube/", + json=create_cube_payload, + ) + response = await client_with_roads.get("/history?node=default.repairs_cube") + assert [activity["activity_type"] for activity in response.json()] == [ + "restore", + "delete", + "create", + ] + + +class TestCopyNode: + """Tests for the copy node API endpoint""" + + @pytest.fixture + def repairs_cube_payload(self): + """Repairs cube creation payload""" + return { + "metrics": [ + "default.num_repair_orders", + "default.avg_repair_price", + "default.total_repair_cost", + ], + "dimensions": [ + "default.hard_hat.state", + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + ], + "filters": ["default.hard_hat.state='AZ'"], + "description": "Cube of various metrics related to repairs", + "mode": "published", + "name": "default.repairs_cube", + } + + @pytest.fixture + def metric_with_required_dim_payload(self): + """Metric with required dimension""" + return { + "description": "Average length of employment per manager", + "query": "SELECT avg(NOW() - hire_date) FROM default.hard_hats", + "mode": "published", + "name": "default.avg_length_of_employment_per_manager", + "required_dimensions": ["manager"], + } + + @pytest.mark.asyncio + async def test_copy_node_failures(self, client_with_roads: AsyncClient): + """ + Test reaching various failure states when copying nodes + """ + response = await client_with_roads.post( + "/nodes/default.repair_order/copy?new_name=default.contractor", + ) + assert ( + response.json()["message"] + == "A node with name default.contractor already exists." + ) + + response = await client_with_roads.post( + "/nodes/default.repair_order/copy?new_name=default.blah.repair_order", + ) + assert ( + response.json()["message"] + == "node namespace `default.blah` does not exist." + ) + + # Test copying over deactivated node + await client_with_roads.delete("/nodes/default.contractor") + await client_with_roads.post( + "/nodes/default.repair_order/copy?new_name=default.contractor", + ) + copied = (await client_with_roads.get("/nodes/default.contractor")).json() + original = (await client_with_roads.get("/nodes/default.repair_order")).json() + for field in ["name", "node_id", "node_revision_id", "updated_at"]: + copied[field] = mock.ANY + copied_dimension_links = sorted( + copied["dimension_links"], + key=lambda li: li["dimension"]["name"], + ) + copied["dimension_links"] = mock.ANY + assert copied == original + assert copied_dimension_links == [ + { + "dimension": {"name": "default.dispatcher"}, + "foreign_keys": { + "default.contractor.dispatcher_id": ( + "default.dispatcher.dispatcher_id" + ), + }, + "join_cardinality": "many_to_one", + "join_sql": "default.contractor.dispatcher_id = " + "default.dispatcher.dispatcher_id", + "join_type": "inner", + "role": None, + }, + { + "dimension": {"name": "default.hard_hat"}, + "foreign_keys": { + "default.contractor.hard_hat_id": ("default.hard_hat.hard_hat_id"), + }, + "join_cardinality": "many_to_one", + "join_sql": "default.contractor.hard_hat_id = default.hard_hat.hard_hat_id", + "join_type": "inner", + "role": None, + }, + { + "dimension": {"name": "default.hard_hat_to_delete"}, + "foreign_keys": { + "default.contractor.hard_hat_id": ( + "default.hard_hat_to_delete.hard_hat_id" + ), + }, + "join_cardinality": "many_to_one", + "join_sql": "default.contractor.hard_hat_id = " + "default.hard_hat_to_delete.hard_hat_id", + "join_type": "left", + "role": None, + }, + { + "dimension": {"name": "default.municipality_dim"}, + "foreign_keys": { + "default.contractor.municipality_id": ( + "default.municipality_dim.municipality_id" + ), + }, + "join_cardinality": "many_to_one", + "join_sql": "default.contractor.municipality_id = " + "default.municipality_dim.municipality_id", + "join_type": "inner", + "role": None, + }, + ] + + @pytest.mark.asyncio + async def test_copy_nodes( + self, + client_with_roads: AsyncClient, + repairs_cube_payload, + metric_with_required_dim_payload, + ): + """ + Test copying all nodes in the roads database + """ + expected_dimension_links = { + "default.repair_orders": [ + { + "dimension": {"name": "default.dispatcher"}, + "foreign_keys": { + "default.repair_orders_copy.dispatcher_id": ( + "default.dispatcher.dispatcher_id" + ), + }, + "join_cardinality": "many_to_one", + "join_sql": "default.repair_orders_copy.dispatcher_id = " + "default.dispatcher.dispatcher_id", + "join_type": "inner", + "role": None, + }, + { + "dimension": {"name": "default.repair_order"}, + "foreign_keys": { + "default.repair_orders_copy.repair_order_id": ( + "default.repair_order.repair_order_id" + ), + }, + "join_cardinality": "many_to_one", + "join_sql": "default.repair_orders_copy.repair_order_id " + "= default.repair_order.repair_order_id", + "join_type": "inner", + "role": None, + }, + ], + "default.repair_order_details": [ + { + "dimension": {"name": "default.repair_order"}, + "join_type": "inner", + "join_sql": "default.repair_order_details_copy.repair_order_id " + "= default.repair_order.repair_order_id", + "join_cardinality": "many_to_one", + "role": None, + "foreign_keys": { + "default.repair_order_details_copy.repair_order_id": ( + "default.repair_order.repair_order_id" + ), + }, + }, + ], + "default.repair_type": [ + { + "dimension": {"name": "default.contractor"}, + "join_type": "inner", + "join_sql": "default.repair_type_copy.contractor_id = " + "default.contractor.contractor_id", + "join_cardinality": "many_to_one", + "role": None, + "foreign_keys": { + "default.repair_type_copy.contractor_id": ( + "default.contractor.contractor_id" + ), + }, + }, + ], + "default.repair_orders_fact": [ + { + "dimension": {"name": "default.dispatcher"}, + "join_type": "inner", + "join_sql": "default.repair_orders_fact_copy.dispatcher_id = " + "default.dispatcher.dispatcher_id", + "join_cardinality": "many_to_one", + "role": None, + "foreign_keys": { + "default.repair_orders_fact_copy.dispatcher_id": ( + "default.dispatcher.dispatcher_id" + ), + }, + }, + { + "dimension": {"name": "default.hard_hat"}, + "join_type": "inner", + "join_sql": "default.repair_orders_fact_copy.hard_hat_id = " + "default.hard_hat.hard_hat_id", + "join_cardinality": "many_to_one", + "role": None, + "foreign_keys": { + "default.repair_orders_fact_copy.hard_hat_id": ( + "default.hard_hat.hard_hat_id" + ), + }, + }, + { + "dimension": {"name": "default.hard_hat_to_delete"}, + "join_type": "left", + "join_sql": "default.repair_orders_fact_copy.hard_hat_id = " + "default.hard_hat_to_delete.hard_hat_id", + "join_cardinality": "many_to_one", + "role": None, + "foreign_keys": { + "default.repair_orders_fact_copy.hard_hat_id": ( + "default.hard_hat_to_delete.hard_hat_id" + ), + }, + }, + { + "dimension": {"name": "default.municipality_dim"}, + "join_type": "inner", + "join_sql": "default.repair_orders_fact_copy.municipality_id = " + "default.municipality_dim.municipality_id", + "join_cardinality": "many_to_one", + "role": None, + "foreign_keys": { + "default.repair_orders_fact_copy.municipality_id": ( + "default.municipality_dim.municipality_id" + ), + }, + }, + ], + "default.hard_hat": [ + { + "dimension": {"name": "default.us_state"}, + "join_type": "inner", + "join_sql": "default.hard_hat_copy.state = default.us_state.state_short", + "join_cardinality": "many_to_one", + "role": None, + "foreign_keys": { + "default.hard_hat_copy.state": "default.us_state.state_short", + }, + }, + ], + "default.repair_order": [ + { + "dimension": {"name": "default.dispatcher"}, + "join_type": "inner", + "join_sql": "default.repair_order_copy.dispatcher_id = " + "default.dispatcher.dispatcher_id", + "join_cardinality": "many_to_one", + "role": None, + "foreign_keys": { + "default.repair_order_copy.dispatcher_id": ( + "default.dispatcher.dispatcher_id" + ), + }, + }, + { + "dimension": {"name": "default.hard_hat"}, + "join_type": "inner", + "join_sql": "default.repair_order_copy.hard_hat_id = " + "default.hard_hat.hard_hat_id", + "join_cardinality": "many_to_one", + "role": None, + "foreign_keys": { + "default.repair_order_copy.hard_hat_id": ( + "default.hard_hat.hard_hat_id" + ), + }, + }, + { + "dimension": {"name": "default.hard_hat_to_delete"}, + "join_type": "left", + "join_sql": "default.repair_order_copy.hard_hat_id = " + "default.hard_hat_to_delete.hard_hat_id", + "join_cardinality": "many_to_one", + "role": None, + "foreign_keys": { + "default.repair_order_copy.hard_hat_id": ( + "default.hard_hat_to_delete.hard_hat_id" + ), + }, + }, + { + "dimension": {"name": "default.municipality_dim"}, + "join_type": "inner", + "join_sql": "default.repair_order_copy.municipality_id = " + "default.municipality_dim.municipality_id", + "join_cardinality": "many_to_one", + "role": None, + "foreign_keys": { + "default.repair_order_copy.municipality_id": ( + "default.municipality_dim.municipality_id" + ), + }, + }, + ], + } + await client_with_roads.post("/nodes/cube", json=repairs_cube_payload) + await client_with_roads.post( + "/nodes/metric", + json=metric_with_required_dim_payload, + ) + + # Copy all nodes to a node name with _copy appended + nodes = (await client_with_roads.get("/nodes")).json() + for node in sorted(nodes): + await client_with_roads.post(f"/nodes/{node}/copy?new_name={node}_copy") + + # Check that each node was successfully copied by comparing against the original + for node in nodes: + original = (await client_with_roads.get(f"/nodes/{node}")).json() + copied = (await client_with_roads.get(f"/nodes/{node}_copy")).json() + for field in ["name", "node_id", "node_revision_id", "updated_at"]: + copied[field] = mock.ANY + copied_dimension_links = sorted( + copied["dimension_links"], + key=lambda link: link["dimension"]["name"], + ) + copied["dimension_links"] = mock.ANY + original["dimension_links"] = sorted( + original["dimension_links"], + key=lambda link: link["dimension"]["name"], + ) + assert original == copied + + # Compare the dimension links, which should have updated join clauses + expected_link = expected_dimension_links.get(node) + if expected_link: + assert expected_link == copied_dimension_links + + # Metrics contain additional metadata, so compare the /metrics endpoint as well + if original["type"] == "metric": + metric_orig = (await client_with_roads.get(f"/metrics/{node}")).json() + metric_copied = ( + await client_with_roads.get(f"/metrics/{node}_copy") + ).json() + for field in ["id", "name", "updated_at"]: + metric_copied[field] = mock.ANY + assert metric_orig == metric_copied + + # Cubes contain additional metadata, so compare the /metrics endpoint as well + if original["type"] == "cube": + cube_orig = (await client_with_roads.get(f"/cubes/{node}")).json() + cube_copied = ( + await client_with_roads.get(f"/cubes/{node}_copy") + ).json() + for field in ["name", "node_id", "node_revision_id", "updated_at"]: + cube_copied[field] = mock.ANY + assert cube_orig == cube_copied + + # Check that the dimensions DAG for the node has been copied + # original_dimensions = [ + # dim["name"] + # for dim in ( + # await client_with_roads.get(f"/nodes/{node}/dimensions") + # ).json() + # ] + # copied_dimensions = [ + # dim["name"].replace(f"{node}_copy", node) + # for dim in ( + # await client_with_roads.get( + # f"/nodes/{node}_copy/dimensions", + # ) + # ).json() + # ] + # for copied in copied_dimensions: + # assert copied in original_dimensions diff --git a/datajunction-server/tests/api/nodes_update_test.py b/datajunction-server/tests/api/nodes_update_test.py new file mode 100644 index 000000000..ca08ce816 --- /dev/null +++ b/datajunction-server/tests/api/nodes_update_test.py @@ -0,0 +1,174 @@ +"""Tests for node updates""" + +from unittest import mock + +import pytest +from httpx import AsyncClient + +from datajunction_server.models.node import NodeStatus + + +@pytest.mark.asyncio +async def test_update_source_node( + module__client_with_roads: AsyncClient, +) -> None: + """ + Test updating a source node that has multiple layers of downstream effects + non-immediate children should be revalidated: + - default.regional_repair_efficiency should be affected (invalidated) + column changes should affect validity: + - any metric selecting from `quantity` should be invalid (now quantity_v2) + - any metric selecting from `price` should have updated types (now string) + """ + await module__client_with_roads.patch( + "/nodes/default.repair_order_details/", + json={ + "columns": [ + {"name": "repair_order_id", "type": "string"}, + {"name": "repair_type_id", "type": "int"}, + {"name": "price", "type": "string"}, + {"name": "quantity_v2", "type": "int"}, + {"name": "discount", "type": "float"}, + ], + }, + ) + affected_nodes = { + "default.regional_level_agg": NodeStatus.INVALID, # NodeStatus.INVALID + "default.national_level_agg": NodeStatus.INVALID, + "default.avg_repair_price": NodeStatus.INVALID, + "default.total_repair_cost": NodeStatus.INVALID, + "default.discounted_orders_rate": NodeStatus.INVALID, + "default.total_repair_order_discounts": NodeStatus.INVALID, + "default.avg_repair_order_discounts": NodeStatus.INVALID, + "default.regional_repair_efficiency": NodeStatus.INVALID, + } + + node_history_events = { + "default.national_level_agg": [ + { + "activity_type": "update", + "created_at": mock.ANY, + "details": { + "changes": {"updated_columns": []}, + "reason": "Caused by update of `default.repair_order_details` to " + "v2.0", + "upstream": { + "node": "default.repair_order_details", + "version": "v2.0", + }, + }, + "entity_name": "default.national_level_agg", + "entity_type": "node", + "id": mock.ANY, + "node": "default.national_level_agg", + "post": {"status": "invalid", "version": "v1.0"}, + "pre": {"status": "valid", "version": "v1.0"}, + "user": mock.ANY, + }, + ], + "default.regional_level_agg": [ + { + "activity_type": "update", + "created_at": mock.ANY, + "details": { + "changes": { + "updated_columns": [], + }, + "reason": "Caused by update of `default.repair_order_details` to v2.0", + "upstream": { + "node": "default.repair_order_details", + "version": "v2.0", + }, + }, + "entity_name": "default.regional_level_agg", + "entity_type": "node", + "id": mock.ANY, + "node": "default.regional_level_agg", + "post": {"status": "invalid", "version": "v1.0"}, + "pre": {"status": "valid", "version": "v1.0"}, + "user": mock.ANY, + }, + ], + "default.avg_repair_price": [ + { + "activity_type": "update", + "created_at": mock.ANY, + "details": { + "changes": {"updated_columns": []}, + "reason": "Caused by update of `default.repair_order_details` to " + "v2.0", + "upstream": { + "node": "default.repair_order_details", + "version": "v2.0", + }, + }, + "entity_name": "default.avg_repair_price", + "entity_type": "node", + "id": mock.ANY, + "node": "default.avg_repair_price", + "post": {"status": "invalid", "version": "v1.0"}, + "pre": {"status": "valid", "version": "v1.0"}, + "user": mock.ANY, + }, + ], + "default.regional_repair_efficiency": [ + { + "activity_type": "update", + "created_at": mock.ANY, + "details": { + "changes": { + "updated_columns": [], + }, + "reason": "Caused by update of `default.repair_order_details` to " + "v2.0", + "upstream": { + "node": "default.repair_order_details", + "version": "v2.0", + }, + }, + "entity_name": "default.regional_repair_efficiency", + "entity_type": "node", + "id": mock.ANY, + "node": "default.regional_repair_efficiency", + "post": {"status": "invalid", "version": "v1.0"}, + "pre": {"status": "valid", "version": "v1.0"}, + "user": mock.ANY, + }, + ], + } + + # check all affected nodes and verify that their statuses have been updated + for affected, expected_status in affected_nodes.items(): + response = await module__client_with_roads.get(f"/nodes/{affected}") + assert response.json()["status"] == expected_status + + # only nodes with a status change will have a history record + if expected_status == NodeStatus.INVALID: + response = await module__client_with_roads.get(f"/history?node={affected}") + if node_history_events.get(affected): + assert [ + event + for event in response.json() + if event["activity_type"] == "update" + ] == node_history_events.get(affected) + + await module__client_with_roads.patch( + "/nodes/default.national_level_agg/", + json={ + "query": "SELECT SUM(cast(rd.price AS float) * rd.quantity_v2) AS total_amount " + "FROM default.repair_order_details rd", + }, + ) + response = await module__client_with_roads.get("/nodes/default.national_level_agg") + data = response.json() + assert data["status"] == "valid" + assert data["columns"] == [ + { + "attributes": [], + "dimension": None, + "display_name": "Total Amount", + "name": "total_amount", + "partition": None, + "type": "double", + }, + ] diff --git a/datajunction-server/tests/api/notifications_test.py b/datajunction-server/tests/api/notifications_test.py new file mode 100644 index 000000000..0dbfe6da4 --- /dev/null +++ b/datajunction-server/tests/api/notifications_test.py @@ -0,0 +1,176 @@ +"""Tests for notifications router""" + +import unittest +from unittest import mock + +import pytest +from httpx import AsyncClient + +from datajunction_server.api.notifications import get_notifier +from datajunction_server.database.history import ActivityType, EntityType, History + + +class TestNotification(unittest.TestCase): + """Test sending notifications""" + + @mock.patch("datajunction_server.api.notifications._logger") + def test_notification(self, mock_logger): + """Test the get_notifier dependency""" + notify = get_notifier() + event = History( + id=1, + entity_name="bar", + entity_type=EntityType.NODE, + activity_type=ActivityType.CREATE, + ) + notify(event) + + mock_logger.debug.assert_any_call( + "Sending notification for event %s", + event, + ) + + +@pytest.mark.asyncio +async def test_notification_subscription( + module__client: AsyncClient, +) -> None: + """ + Test subscribing to notifications. + """ + response = await module__client.post( + "/notifications/subscribe", + json={ + "entity_name": "some_node_name", + "entity_type": EntityType.NODE, + "activity_types": [ActivityType.DELETE], + "alert_types": ["slack", "email"], + }, + ) + assert response.status_code == 201 + assert response.json() == { + "message": "Notification preferences successfully saved for some_node_name", + } + + +@pytest.mark.asyncio +async def test_notification_preferences( + module__client: AsyncClient, +) -> None: + """ + Test retrieving notification preferences. + """ + response = await module__client.post( + "/notifications/subscribe", + json={ + "entity_type": EntityType.NODE, + "entity_name": "some_node_name2", + "activity_types": [ActivityType.REFRESH], + "alert_types": ["slack", "email"], + }, + ) + assert response.status_code == 201 + response = await module__client.get( + "/notifications/", + params={"entity_name": "some_node_name2"}, + ) + assert response.status_code == 200 + assert len(response.json()) > 0 + assert response.json()[0]["entity_name"] == "some_node_name2" + assert response.json()[0]["username"] == "dj" + + +@pytest.mark.asyncio +async def test_notification_preferences_with_filters( + module__client: AsyncClient, +) -> None: + """ + Test retrieving notification preferences with filters. + """ + await module__client.post( + "/notifications/subscribe", + json={ + "entity_type": EntityType.NODE, + "entity_name": "name1", + "activity_types": [ActivityType.STATUS_CHANGE], + "alert_types": ["slack", "email"], + }, + ) + await module__client.post( + "/notifications/subscribe", + json={ + "entity_name": "name2", + "entity_type": EntityType.NODE, + "activity_types": [ActivityType.DELETE], + "alert_types": ["slack", "email"], + }, + ) + response = await module__client.get( + "/notifications/?entity_name=name1", + ) + assert response.status_code == 200 + assert len(response.json()) == 1 + assert response.json()[0]["entity_name"] == "name1" + + response = await module__client.get( + "/notifications/?entity_type=node", + ) + assert response.status_code == 200 + assert len(response.json()) > 1 + assert response.json()[0]["entity_type"] == "node" + + +@pytest.mark.asyncio +async def test_notification_unsubscribe( + module__client: AsyncClient, +) -> None: + """ + Test unsubscribing from notifications. + """ + # Subscribe + response = await module__client.post( + "/notifications/subscribe", + json={ + "entity_name": "some_node_name3", + "entity_type": EntityType.NODE, + "activity_types": [ActivityType.DELETE], + "alert_types": ["slack", "email"], + }, + ) + assert response.status_code == 201 + + # Unsubscribe + response = await module__client.delete( + "/notifications/unsubscribe", + params={ + "entity_type": EntityType.NODE, + "entity_name": "some_node_name3", + }, + ) + assert response.status_code == 200 + assert response.json() == { + "message": "Notification preferences successfully removed for some_node_name3", + } + + # Verify that the notification preference is actually removed + response = await module__client.get("/notifications/") + assert response.status_code == 200 + assert all(pref["entity_name"] != "some_node_name3" for pref in response.json()) + + +@pytest.mark.asyncio +async def test_notification_unsubscribe_not_found( + module__client: AsyncClient, +) -> None: + """ + Test notification preference not found when unsubscribing + """ + # Unsubscribe to a notification that doesn't exist + response = await module__client.delete( + "/notifications/unsubscribe", + params={ + "entity_type": EntityType.NODE, + "entity_name": "does_not_exist", + }, + ) + assert response.status_code == 404 diff --git a/datajunction-server/tests/api/routers_test.py b/datajunction-server/tests/api/routers_test.py new file mode 100644 index 000000000..06b2ac198 --- /dev/null +++ b/datajunction-server/tests/api/routers_test.py @@ -0,0 +1,35 @@ +""" +Tests for the custom API routers. +""" + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_api_router_trailing_slashes( + client_with_basic: AsyncClient, +) -> None: + """ + Test that the API router used by our endpoints will send both routes without + trailing slashes and those with trailing slashes to right place. + """ + response = await client_with_basic.get("/attributes/") + assert response.status_code in (200, 201) + assert len(response.json()) > 0 + + response = await client_with_basic.get("/attributes") + assert response.status_code in (200, 201) + assert len(response.json()) > 0 + + response = await client_with_basic.get("/namespaces/") + assert response.status_code in (200, 201) + assert len(response.json()) > 0 + + response = await client_with_basic.get("/namespaces") + assert response.status_code in (200, 201) + assert len(response.json()) > 0 + + response = await client_with_basic.get("/namespaces/basic?type_=source") + assert response.status_code in (200, 201) + assert len(response.json()) > 0 diff --git a/datajunction-server/tests/api/sql_test.py b/datajunction-server/tests/api/sql_test.py new file mode 100644 index 000000000..188b15a4d --- /dev/null +++ b/datajunction-server/tests/api/sql_test.py @@ -0,0 +1,3859 @@ +"""Tests for the /sql/ endpoint""" + +import duckdb +import pytest +from httpx import AsyncClient, Response +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database.column import Column +from datajunction_server.database.database import Database +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.queryrequest import QueryBuildType, QueryRequest +from datajunction_server.database.user import User +from datajunction_server.internal.access.authorization import validate_access +from datajunction_server.models import access +from datajunction_server.models.node_type import NodeType +from datajunction_server.sql.parsing.backends.antlr4 import parse +from datajunction_server.sql.parsing.types import IntegerType, StringType +from tests.sql.utils import compare_query_strings + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="Bad test setup") +async def test_sql( + session: AsyncSession, + client: AsyncClient, + current_user: User, +) -> None: + """ + Test ``GET /sql/{name}/``. + """ + database = Database(name="test", URI="blah://", tables=[]) + + source_node = Node( + name="default.my_table", + type=NodeType.SOURCE, + current_version="1", + created_by_id=current_user.id, + ) + source_node_rev = NodeRevision( + name=source_node.name, + node=source_node, + version="1", + schema_="rev", + table="my_table", + columns=[Column(name="one", type=StringType(), order=0)], + type=NodeType.SOURCE, + created_by_id=current_user.id, + ) + + node = Node( + name="default.a_metric", + type=NodeType.METRIC, + current_version="1", + created_by_id=current_user.id, + ) + node_revision = NodeRevision( + name=node.name, + node=node, + version="1", + query="SELECT COUNT(*) FROM default.my_table", + type=NodeType.METRIC, + created_by_id=current_user.id, + columns=[Column(name="col0", type=IntegerType(), order=0)], + ) + node_revision.parents = [source_node] + session.add(database) + session.add(node_revision) + session.add(source_node_rev) + await session.commit() + + response = (await client.get("/sql/default.a_metric/")).json() + assert compare_query_strings( + response["sql"], + "SELECT COUNT(*) default_DOT_a_metric \n FROM rev.my_table AS default_DOT_my_table\n", + ) + assert response["columns"] == [ + { + "column": "a_metric", + "name": "default_DOT_a_metric", + "node": "default", + "type": "bigint", + "semantic_type": None, + "semantic_entity": "default.a_metric", + }, + ] + assert response["dialect"] is None + + # Check that this query request has been saved + query_request = (await session.execute(select(QueryRequest))).scalars().all() + assert len(query_request) == 1 + assert query_request[0].nodes == ["default.a_metric@1"] + assert query_request[0].dimensions == [] + assert query_request[0].filters == [] + assert query_request[0].orderby == [] + assert query_request[0].limit is None + assert query_request[0].query_type == QueryBuildType.NODE + assert compare_query_strings(query_request[0].query, response["sql"]) + assert query_request[0].columns == response["columns"] + + +@pytest.fixture +def transform_node_sql_request(client_with_roads: AsyncClient): + """ + Request SQL for a transform node + GET `/sql/default.repair_orders_fact` + """ + + async def _make_request() -> Response: + transform_node_sql_params = { + "dimensions": [ + "default.dispatcher.company_name", + "default.hard_hat.state", + "default.hard_hat.hard_hat_id", + ], + "filters": ["default.hard_hat.state = 'CA'"], + "limit": 200, + "orderby": ["default.dispatcher.company_name ASC"], + } + response = await client_with_roads.get( + "/sql/default.repair_orders_fact", + params=transform_node_sql_params, + ) + return response + + return _make_request + + +@pytest.fixture +def measures_sql_request(client_with_roads: AsyncClient): + """ + Request measures SQL for some metrics + GET `/sql/measures` + """ + + async def _make_request() -> Response: + measures_sql_params = { + "metrics": ["default.num_repair_orders", "default.total_repair_cost"], + "dimensions": [ + "default.dispatcher.company_name", + "default.hard_hat.state", + ], + "filters": ["default.hard_hat.state = 'CA'"], + "include_all_columns": True, + } + response = await client_with_roads.get( + "/sql/measures/v2", + params=measures_sql_params, + ) + return response + + return _make_request + + +@pytest.fixture +def metrics_sql_request(client_with_roads: AsyncClient): + """ + Request metrics SQL for some metrics + GET `/sql` + """ + + async def _make_request() -> Response: + metrics_sql_params = { + "metrics": ["default.num_repair_orders", "default.total_repair_cost"], + "dimensions": [ + "default.dispatcher.company_name", + "default.hard_hat.state", + ], + "filters": ["default.hard_hat.state = 'CA'"], + "orderby": ["default.num_repair_orders DESC", "default.hard_hat.state"], + "limit": 50, + } + return await client_with_roads.get( + "/sql", + params=metrics_sql_params, + ) + + return _make_request + + +@pytest.fixture +def update_transform_node(client_with_roads: AsyncClient): + """ + Update transform node with a simplified query + """ + + async def _make_request() -> Response: + response = await client_with_roads.patch( + "/nodes/default.repair_orders_fact", + json={ + "query": """SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost + FROM + default.repair_orders repair_orders + JOIN + default.repair_order_details repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id""", + }, + ) + + response = await client_with_roads.post( + "/nodes/default.repair_orders_fact/link", + json={ + "dimension_node": "default.municipality_dim", + "join_type": "inner", + "join_on": ( + "default.repair_orders_fact.municipality_id = default.municipality_dim.municipality_id" + ), + }, + ) + + response = await client_with_roads.post( + "/nodes/default.repair_orders_fact/link", + json={ + "dimension_node": "default.hard_hat", + "join_type": "inner", + "join_on": ( + "default.repair_orders_fact.hard_hat_id = default.hard_hat.hard_hat_id" + ), + }, + ) + + response = await client_with_roads.post( + "/nodes/default.repair_orders_fact/link", + json={ + "dimension_node": "default.hard_hat_to_delete", + "join_type": "left", + "join_on": ( + "default.repair_orders_fact.hard_hat_id = default.hard_hat_to_delete.hard_hat_id" + ), + }, + ) + + response = await client_with_roads.post( + "/nodes/default.repair_orders_fact/link", + json={ + "dimension_node": "default.dispatcher", + "join_type": "inner", + "join_on": ( + "default.repair_orders_fact.dispatcher_id = default.dispatcher.dispatcher_id" + ), + }, + ) + return response + + return _make_request + + +async def get_query_requests(session: AsyncSession, query_type: QueryBuildType): + """ + Get all query requests of the specific query type + """ + return ( + ( + await session.execute( + select(QueryRequest) + .where( + QueryRequest.query_type == query_type, + ) + .order_by(QueryRequest.created_at.asc()), + ) + ) + .scalars() + .all() + ) + + +@pytest.mark.asyncio +async def test_saving_node_sql_requests( + session: AsyncSession, + client_with_roads: AsyncClient, + transform_node_sql_request, + update_transform_node, +) -> None: + """ + Test different scenarios involving saving and reusing cached query requests when + requesting node SQL for a transform. + """ + response = await transform_node_sql_request() + + # Check that this query request has been saved + query_request = await get_query_requests(session, QueryBuildType.NODE) + assert len(query_request) == 1 + assert query_request[0].nodes == ["default.repair_orders_fact@v1.0"] + assert query_request[0].parents == [ + "default.repair_order_details@v1.0", + "default.repair_orders@v1.0", + ] + assert query_request[0].dimensions == [ + "default.dispatcher.company_name@v1.0", + "default.hard_hat.state@v1.0", + "default.hard_hat.hard_hat_id@v1.0", + ] + assert query_request[0].filters == ["default.hard_hat.state@v1.0 = 'CA'"] + assert query_request[0].orderby == [ + "default.dispatcher.company_name@v1.0 ASC", + ] + assert query_request[0].limit == 200 + assert query_request[0].query_type == QueryBuildType.NODE + assert query_request[0].query.strip() == response.json()["sql"].strip() + assert query_request[0].columns == response.json()["columns"] + + # Requesting it again should reuse the saved request + await transform_node_sql_request() + query_requests = await get_query_requests(session, QueryBuildType.NODE) + assert len(query_requests) == 1 + + # Update the transform node to a query that invalidates the SQL request + response = await client_with_roads.patch( + "/nodes/default.repair_orders_fact", + json={ + "query": """SELECT + repair_orders.repair_order_id + FROM + default.repair_orders repair_orders""", + }, + ) + assert response.status_code == 200 + + # This should now trigger error messages when requesting SQL + response = await transform_node_sql_request() + assert ( + "are not available dimensions on default.repair_orders_fact" + in response.json()["message"] + ) + + # Update the transform node with a new query + response = await update_transform_node() + assert response.status_code == 201 + + response = (await transform_node_sql_request()).json() + + # Check that the node update triggered an updated query request to be saved. Note that + # default.repair_orders_fact's version gets bumped, but default.hard_hat will stay the same + query_request = await get_query_requests(session, QueryBuildType.NODE) + assert len(query_request) == 2 + assert query_request[1].nodes == ["default.repair_orders_fact@v3.0"] + assert query_request[1].parents == [ + "default.repair_order_details@v1.0", + "default.repair_orders@v1.0", + ] + assert query_request[1].dimensions == [ + "default.dispatcher.company_name@v1.0", + "default.hard_hat.state@v1.0", + "default.hard_hat.hard_hat_id@v1.0", + ] + assert query_request[1].filters == ["default.hard_hat.state@v1.0 = 'CA'"] + assert query_request[1].orderby == [ + "default.dispatcher.company_name@v1.0 ASC", + ] + assert query_request[1].limit == 200 + assert query_request[1].query_type == QueryBuildType.NODE + assert query_request[1].query.strip() == response["sql"].strip() + assert query_request[1].columns == response["columns"] + + # Update the dimension node default.hard_hat with a new query + response = await client_with_roads.patch( + "/nodes/default.hard_hat", + json={"query": "SELECT hard_hat_id, state FROM default.hard_hats"}, + ) + assert response.status_code == 200 + + # Request the same node query again + response = (await transform_node_sql_request()).json() + + # Check that the dimension node update triggered an updated query request to be saved. Now + # default.repair_orders_fact's version remains the same, but default.hard_hat gets bumped + query_request = await get_query_requests(session, QueryBuildType.NODE) + assert len(query_request) == 3 + assert query_request[2].nodes == ["default.repair_orders_fact@v3.0"] + assert query_request[2].parents == [ + "default.repair_order_details@v1.0", + "default.repair_orders@v1.0", + ] + assert query_request[2].dimensions == [ + "default.dispatcher.company_name@v1.0", + "default.hard_hat.state@v2.0", + "default.hard_hat.hard_hat_id@v2.0", + ] + assert query_request[2].filters == ["default.hard_hat.state@v2.0 = 'CA'"] + assert query_request[2].orderby == [ + "default.dispatcher.company_name@v1.0 ASC", + ] + assert query_request[2].limit == 200 + assert query_request[2].query_type == QueryBuildType.NODE + assert query_request[2].query.strip() == response["sql"].strip() + assert query_request[2].columns == response["columns"] + + # Remove a dimension node link to default.hard_hat + response = await client_with_roads.request( + "DELETE", + "/nodes/default.repair_orders_fact/link", + json={ + "dimension_node": "default.hard_hat", + }, + ) + assert response.status_code == 201 + + # Now requesting metrics SQL with default.hard_hat.state should fail + response = (await transform_node_sql_request()).json() + assert response["message"] == ( + "default.hard_hat.hard_hat_id, default.hard_hat.state are not available dimensions " + "on default.repair_orders_fact" + ) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="Not saving v2 measures SQL to cache for the time being") +async def test_saving_measures_sql_requests( + session: AsyncSession, + client_with_roads: AsyncClient, + measures_sql_request, + update_transform_node, +) -> None: + """ + Test saving query request while requesting measures SQL for a set of metrics + dimensions + + filters. It also checks that additional arguments like `include_all_columns` are recorded + in the query request key. + - Requesting metrics SQL (for a set of metrics + dimensions) + """ + response = (await measures_sql_request()).json() + + # Check that the measures SQL request was saved + query_requests = await get_query_requests(session, QueryBuildType.MEASURES) + assert len(query_requests) == 1 + assert query_requests[0].nodes == [ + "default.num_repair_orders@v1.0", + "default.total_repair_cost@v1.0", + ] + assert query_requests[0].dimensions == [ + "default.dispatcher.company_name@v1.0", + "default.hard_hat.state@v1.0", + ] + assert query_requests[0].filters == ["default.hard_hat.state@v1.0 = 'CA'"] + assert query_requests[0].orderby == [] + assert query_requests[0].limit is None + assert query_requests[0].query_type == QueryBuildType.MEASURES + assert query_requests[0].other_args == {"include_all_columns": True} + assert query_requests[0].query.strip() == response[0]["sql"].strip() + assert query_requests[0].columns == response["columns"] + + # Requesting it again should reuse the saved request + await measures_sql_request() + query_requests = await get_query_requests(session, QueryBuildType.MEASURES) + assert len(query_requests) == 1 + + # Update the underlying transform behind the metrics + response = await update_transform_node() + assert response.status_code == 200 + response = (await measures_sql_request()).json() + + # Check that the measures SQL request was saved + query_requests = await get_query_requests(session, QueryBuildType.MEASURES) + assert len(query_requests) == 2 + assert query_requests[1].nodes == [ + "default.num_repair_orders@v1.0", + "default.total_repair_cost@v1.0", + ] + assert query_requests[1].parents == [ + "default.repair_order_details@v1.0", + "default.repair_orders@v1.0", + "default.repair_orders_fact@v2.0", + ] + assert query_requests[1].dimensions == [ + "default.dispatcher.company_name@v1.0", + "default.hard_hat.state@v1.0", + ] + assert query_requests[1].filters == ["default.hard_hat.state@v1.0 = 'CA'"] + assert query_requests[1].orderby == [] + assert query_requests[1].limit is None + assert query_requests[1].query_type == QueryBuildType.MEASURES + assert query_requests[1].other_args == {"include_all_columns": True} + assert query_requests[1].query.strip() == response["sql"].strip() + assert query_requests[1].columns == response["columns"] + + # Remove a dimension node link to default.hard_hat + response = await client_with_roads.request( + "DELETE", + "/nodes/default.repair_orders_fact/link", + json={ + "dimension_node": "default.hard_hat", + }, + ) + assert response.status_code == 201 + + # And requesting measures SQL with default.hard_hat.state should fail + response = (await measures_sql_request()).json() + assert response["message"] == ( + "default.hard_hat.state are not available dimensions on default.num_repair_orders, default.total_repair_cost" + ) + + +@pytest.mark.asyncio +async def test_saving_metrics_sql_requests( + session: AsyncSession, + client_with_roads: AsyncClient, + metrics_sql_request, + update_transform_node, +) -> None: + """ + Requesting metrics SQL for a set of metrics + dimensions + filters + """ + response = (await metrics_sql_request()).json() + + # Check that the metrics SQL request was saved + query_request = await get_query_requests(session, QueryBuildType.METRICS) + assert len(query_request) == 1 + assert query_request[0].nodes == [ + "default.num_repair_orders@v1.0", + "default.total_repair_cost@v1.0", + ] + assert query_request[0].parents == [ + "default.repair_order_details@v1.0", + "default.repair_orders@v1.0", + "default.repair_orders_fact@v1.0", + ] + assert query_request[0].dimensions == [ + "default.dispatcher.company_name@v1.0", + "default.hard_hat.state@v1.0", + ] + assert query_request[0].filters == ["default.hard_hat.state@v1.0 = 'CA'"] + assert query_request[0].orderby == [ + "default.num_repair_orders@v1.0 DESC", + "default.hard_hat.state@v1.0", + ] + assert query_request[0].limit == 50 + assert query_request[0].engine_name is None + assert query_request[0].engine_version is None + assert query_request[0].other_args == {} + + assert query_request[0].query_type == QueryBuildType.METRICS + assert query_request[0].query.strip() == response["sql"].strip() + assert query_request[0].columns == response["columns"] + + # Requesting it again should reuse the cached values + await metrics_sql_request() + query_request = await get_query_requests(session, QueryBuildType.METRICS) + assert len(query_request) == 1 + + # Patch the underlying transform with a new query + await update_transform_node() + + # Now requesting metrics SQL should not use the cached entry + response = (await metrics_sql_request()).json() + + # Check that a new metrics SQL request was saved + query_request = await get_query_requests(session, QueryBuildType.METRICS) + assert len(query_request) == 2 + assert query_request[1].nodes == [ + "default.num_repair_orders@v1.0", + "default.total_repair_cost@v1.0", + ] + assert query_request[1].parents == [ + "default.repair_order_details@v1.0", + "default.repair_orders@v1.0", + "default.repair_orders_fact@v2.0", + ] + assert query_request[1].dimensions == [ + "default.dispatcher.company_name@v1.0", + "default.hard_hat.state@v1.0", + ] + assert query_request[1].filters == ["default.hard_hat.state@v1.0 = 'CA'"] + assert query_request[1].orderby == [ + "default.num_repair_orders@v1.0 DESC", + "default.hard_hat.state@v1.0", + ] + assert query_request[1].limit == 50 + assert query_request[1].engine_name is None + assert query_request[1].engine_version is None + assert query_request[1].other_args == {} + + assert query_request[1].query_type == QueryBuildType.METRICS + assert query_request[1].query.strip() == response["sql"].strip() + assert query_request[1].columns == response["columns"] + + # Request metrics SQL without dimensions, filters, limit or orderby + metrics_sql_params = { + "metrics": ["default.num_repair_orders", "default.total_repair_cost"], + } + response = ( + await client_with_roads.get( + "/sql", + params=metrics_sql_params, + ) + ).json() + + # Check the query request entry that was saved + query_request = await get_query_requests(session, QueryBuildType.METRICS) + assert len(query_request) == 3 + assert query_request[2].nodes == [ + "default.num_repair_orders@v1.0", + "default.total_repair_cost@v1.0", + ] + assert query_request[2].parents == [ + "default.repair_order_details@v1.0", + "default.repair_orders@v1.0", + "default.repair_orders_fact@v2.0", + ] + assert query_request[2].dimensions == [] + assert query_request[2].filters == [] + assert query_request[2].orderby == [] + assert query_request[2].limit is None + assert query_request[2].engine_name is None + assert query_request[2].engine_version is None + assert query_request[2].other_args == {} + + assert query_request[2].query_type == QueryBuildType.METRICS + assert query_request[2].query.strip() == response["sql"].strip() + assert query_request[2].columns == response["columns"] + + # Request it again and check that no new entry was created + await client_with_roads.get( + "/sql", + params=metrics_sql_params, + ) + query_request = await get_query_requests(session, QueryBuildType.METRICS) + assert len(query_request) == 3 + + +async def verify_node_sql( + custom_client: AsyncClient, + node_name: str, + dimensions, + filters, + expected_sql, + expected_columns, + expected_rows, +): + """ + Verifies node SQL generation. + """ + response = await custom_client.get( + f"/sql/{node_name}/", + params={"dimensions": dimensions, "filters": filters}, + ) + sql_data = response.json() + # Run the query against local duckdb file if it's part of the roads model + response = await custom_client.get( + f"/data/{node_name}/", + params={"dimensions": dimensions, "filters": filters}, + ) + data = response.json() + assert data["results"][0]["rows"] == expected_rows + assert str(parse(str(sql_data["sql"]))) == str(parse(str(expected_sql))) + assert sql_data["columns"] == expected_columns + + +@pytest.mark.asyncio +async def test_transform_sql_filter_joinable_dimension( + module__client_with_examples: AsyncClient, +): + """ + Test ``GET /sql/{node_name}/`` with various filters and dimensions. + """ + await verify_node_sql( + module__client_with_examples, + node_name="default.repair_orders_fact", + dimensions=["default.hard_hat.first_name", "default.hard_hat.last_name"], + filters=["default.hard_hat.state = 'NY'"], + expected_sql=""" + WITH + default_DOT_repair_orders_fact AS ( + SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_hard_hat AS ( + SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE default_DOT_hard_hats.state = 'NY' + ) + SELECT default_DOT_repair_orders_fact.repair_order_id default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact.municipality_id default_DOT_repair_orders_fact_DOT_municipality_id, + default_DOT_repair_orders_fact.hard_hat_id default_DOT_repair_orders_fact_DOT_hard_hat_id, + default_DOT_repair_orders_fact.dispatcher_id default_DOT_repair_orders_fact_DOT_dispatcher_id, + default_DOT_repair_orders_fact.order_date default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact.dispatched_date default_DOT_repair_orders_fact_DOT_dispatched_date, + default_DOT_repair_orders_fact.required_date default_DOT_repair_orders_fact_DOT_required_date, + default_DOT_repair_orders_fact.discount default_DOT_repair_orders_fact_DOT_discount, + default_DOT_repair_orders_fact.price default_DOT_repair_orders_fact_DOT_price, + default_DOT_repair_orders_fact.quantity default_DOT_repair_orders_fact_DOT_quantity, + default_DOT_repair_orders_fact.repair_type_id default_DOT_repair_orders_fact_DOT_repair_type_id, + default_DOT_repair_orders_fact.total_repair_cost default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_repair_orders_fact.time_to_dispatch default_DOT_repair_orders_fact_DOT_time_to_dispatch, + default_DOT_repair_orders_fact.dispatch_delay default_DOT_repair_orders_fact_DOT_dispatch_delay, + default_DOT_hard_hat.first_name default_DOT_hard_hat_DOT_first_name, + default_DOT_hard_hat.last_name default_DOT_hard_hat_DOT_last_name, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state + FROM default_DOT_repair_orders_fact INNER JOIN default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + """, + expected_columns=[ + { + "column": "repair_order_id", + "name": "default_DOT_repair_orders_fact_DOT_repair_order_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.repair_order_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "municipality_id", + "name": "default_DOT_repair_orders_fact_DOT_municipality_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.municipality_id", + "semantic_type": None, + "type": "string", + }, + { + "column": "hard_hat_id", + "name": "default_DOT_repair_orders_fact_DOT_hard_hat_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.hard_hat_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "dispatcher_id", + "name": "default_DOT_repair_orders_fact_DOT_dispatcher_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.dispatcher_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "order_date", + "name": "default_DOT_repair_orders_fact_DOT_order_date", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.order_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatched_date", + "name": "default_DOT_repair_orders_fact_DOT_dispatched_date", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.dispatched_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "required_date", + "name": "default_DOT_repair_orders_fact_DOT_required_date", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.required_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "discount", + "name": "default_DOT_repair_orders_fact_DOT_discount", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.discount", + "semantic_type": None, + "type": "float", + }, + { + "column": "price", + "name": "default_DOT_repair_orders_fact_DOT_price", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price", + "semantic_type": None, + "type": "float", + }, + { + "column": "quantity", + "name": "default_DOT_repair_orders_fact_DOT_quantity", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.quantity", + "semantic_type": None, + "type": "int", + }, + { + "column": "repair_type_id", + "name": "default_DOT_repair_orders_fact_DOT_repair_type_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.repair_type_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "total_repair_cost", + "name": "default_DOT_repair_orders_fact_DOT_total_repair_cost", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.total_repair_cost", + "semantic_type": None, + "type": "float", + }, + { + "column": "time_to_dispatch", + "name": "default_DOT_repair_orders_fact_DOT_time_to_dispatch", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.time_to_dispatch", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatch_delay", + "name": "default_DOT_repair_orders_fact_DOT_dispatch_delay", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.dispatch_delay", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "first_name", + "name": "default_DOT_hard_hat_DOT_first_name", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.first_name", + "semantic_type": None, + "type": "string", + }, + { + "column": "last_name", + "name": "default_DOT_hard_hat_DOT_last_name", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.last_name", + "semantic_type": None, + "type": "string", + }, + { + "column": "state", + "name": "default_DOT_hard_hat_DOT_state", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.state", + "semantic_type": None, + "type": "string", + }, + ], + expected_rows=[ + [ + 10021, + "Philadelphia", + 7, + 3, + "2007-05-10", + "2007-12-01", + "2009-08-27", + 0.009999999776482582, + 53374.0, + 1, + 1, + 53374.0, + 205, + -635, + "Boone", + "William", + "NY", + ], + ], + ) + + +@pytest.mark.asyncio +async def test_transform_sql_filter_dimension_pk_col( + module__client_with_examples: AsyncClient, +): + """ + Test ``GET /sql/{node_name}/`` with various filters and dimensions. + """ + await verify_node_sql( + module__client_with_examples, + node_name="default.repair_orders_fact", + dimensions=["default.hard_hat.hard_hat_id"], + filters=["default.hard_hat.hard_hat_id = 7"], + expected_sql=""" + WITH default_DOT_repair_orders_fact AS ( + SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + WHERE repair_orders.hard_hat_id = 7 + ) + SELECT default_DOT_repair_orders_fact.repair_order_id default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact.municipality_id default_DOT_repair_orders_fact_DOT_municipality_id, + default_DOT_repair_orders_fact.hard_hat_id default_DOT_hard_hat_DOT_hard_hat_id, + default_DOT_repair_orders_fact.dispatcher_id default_DOT_repair_orders_fact_DOT_dispatcher_id, + default_DOT_repair_orders_fact.order_date default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact.dispatched_date default_DOT_repair_orders_fact_DOT_dispatched_date, + default_DOT_repair_orders_fact.required_date default_DOT_repair_orders_fact_DOT_required_date, + default_DOT_repair_orders_fact.discount default_DOT_repair_orders_fact_DOT_discount, + default_DOT_repair_orders_fact.price default_DOT_repair_orders_fact_DOT_price, + default_DOT_repair_orders_fact.quantity default_DOT_repair_orders_fact_DOT_quantity, + default_DOT_repair_orders_fact.repair_type_id default_DOT_repair_orders_fact_DOT_repair_type_id, + default_DOT_repair_orders_fact.total_repair_cost default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_repair_orders_fact.time_to_dispatch default_DOT_repair_orders_fact_DOT_time_to_dispatch, + default_DOT_repair_orders_fact.dispatch_delay default_DOT_repair_orders_fact_DOT_dispatch_delay + FROM default_DOT_repair_orders_fact + """, + expected_columns=[ + { + "column": "repair_order_id", + "name": "default_DOT_repair_orders_fact_DOT_repair_order_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.repair_order_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "municipality_id", + "name": "default_DOT_repair_orders_fact_DOT_municipality_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.municipality_id", + "semantic_type": None, + "type": "string", + }, + { + "column": "hard_hat_id", + "name": "default_DOT_hard_hat_DOT_hard_hat_id", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.hard_hat_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "dispatcher_id", + "name": "default_DOT_repair_orders_fact_DOT_dispatcher_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.dispatcher_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "order_date", + "name": "default_DOT_repair_orders_fact_DOT_order_date", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.order_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatched_date", + "name": "default_DOT_repair_orders_fact_DOT_dispatched_date", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.dispatched_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "required_date", + "name": "default_DOT_repair_orders_fact_DOT_required_date", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.required_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "discount", + "name": "default_DOT_repair_orders_fact_DOT_discount", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.discount", + "semantic_type": None, + "type": "float", + }, + { + "column": "price", + "name": "default_DOT_repair_orders_fact_DOT_price", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price", + "semantic_type": None, + "type": "float", + }, + { + "column": "quantity", + "name": "default_DOT_repair_orders_fact_DOT_quantity", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.quantity", + "semantic_type": None, + "type": "int", + }, + { + "column": "repair_type_id", + "name": "default_DOT_repair_orders_fact_DOT_repair_type_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.repair_type_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "total_repair_cost", + "name": "default_DOT_repair_orders_fact_DOT_total_repair_cost", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.total_repair_cost", + "semantic_type": None, + "type": "float", + }, + { + "column": "time_to_dispatch", + "name": "default_DOT_repair_orders_fact_DOT_time_to_dispatch", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.time_to_dispatch", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatch_delay", + "name": "default_DOT_repair_orders_fact_DOT_dispatch_delay", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.dispatch_delay", + "semantic_type": None, + "type": "timestamp", + }, + ], + expected_rows=[ + [ + 10021, + "Philadelphia", + 7, + 3, + "2007-05-10", + "2007-12-01", + "2009-08-27", + 0.009999999776482582, + 53374.0, + 1, + 1, + 53374.0, + 205, + -635, + ], + ], + ) + + +@pytest.mark.asyncio +async def test_transform_sql_filter_direct_node( + module__client_with_examples: AsyncClient, +): + """ + Test ``GET /sql/{node_name}/`` with various filters and dimensions. + """ + await verify_node_sql( + module__client_with_examples, + node_name="default.repair_orders_fact", + dimensions=[], + filters=["default.repair_orders_fact.price > 97915"], + expected_sql="""WITH + default_DOT_repair_orders_fact AS ( + SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + WHERE repair_order_details.price > 97915 + ) + SELECT default_DOT_repair_orders_fact.repair_order_id default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact.municipality_id default_DOT_repair_orders_fact_DOT_municipality_id, + default_DOT_repair_orders_fact.hard_hat_id default_DOT_repair_orders_fact_DOT_hard_hat_id, + default_DOT_repair_orders_fact.dispatcher_id default_DOT_repair_orders_fact_DOT_dispatcher_id, + default_DOT_repair_orders_fact.order_date default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact.dispatched_date default_DOT_repair_orders_fact_DOT_dispatched_date, + default_DOT_repair_orders_fact.required_date default_DOT_repair_orders_fact_DOT_required_date, + default_DOT_repair_orders_fact.discount default_DOT_repair_orders_fact_DOT_discount, + default_DOT_repair_orders_fact.price default_DOT_repair_orders_fact_DOT_price, + default_DOT_repair_orders_fact.quantity default_DOT_repair_orders_fact_DOT_quantity, + default_DOT_repair_orders_fact.repair_type_id default_DOT_repair_orders_fact_DOT_repair_type_id, + default_DOT_repair_orders_fact.total_repair_cost default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_repair_orders_fact.time_to_dispatch default_DOT_repair_orders_fact_DOT_time_to_dispatch, + default_DOT_repair_orders_fact.dispatch_delay default_DOT_repair_orders_fact_DOT_dispatch_delay + FROM default_DOT_repair_orders_fact + """, + expected_columns=[ + { + "column": "repair_order_id", + "name": "default_DOT_repair_orders_fact_DOT_repair_order_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.repair_order_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "municipality_id", + "name": "default_DOT_repair_orders_fact_DOT_municipality_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.municipality_id", + "semantic_type": None, + "type": "string", + }, + { + "column": "hard_hat_id", + "name": "default_DOT_repair_orders_fact_DOT_hard_hat_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.hard_hat_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "dispatcher_id", + "name": "default_DOT_repair_orders_fact_DOT_dispatcher_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.dispatcher_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "order_date", + "name": "default_DOT_repair_orders_fact_DOT_order_date", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.order_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatched_date", + "name": "default_DOT_repair_orders_fact_DOT_dispatched_date", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.dispatched_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "required_date", + "name": "default_DOT_repair_orders_fact_DOT_required_date", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.required_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "discount", + "name": "default_DOT_repair_orders_fact_DOT_discount", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.discount", + "semantic_type": None, + "type": "float", + }, + { + "column": "price", + "name": "default_DOT_repair_orders_fact_DOT_price", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price", + "semantic_type": None, + "type": "float", + }, + { + "column": "quantity", + "name": "default_DOT_repair_orders_fact_DOT_quantity", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.quantity", + "semantic_type": None, + "type": "int", + }, + { + "column": "repair_type_id", + "name": "default_DOT_repair_orders_fact_DOT_repair_type_id", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.repair_type_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "total_repair_cost", + "name": "default_DOT_repair_orders_fact_DOT_total_repair_cost", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.total_repair_cost", + "semantic_type": None, + "type": "float", + }, + { + "column": "time_to_dispatch", + "name": "default_DOT_repair_orders_fact_DOT_time_to_dispatch", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.time_to_dispatch", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatch_delay", + "name": "default_DOT_repair_orders_fact_DOT_dispatch_delay", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.dispatch_delay", + "semantic_type": None, + "type": "timestamp", + }, + ], + expected_rows=[ + [ + 10019, + "Philadelphia", + 5, + 3, + "2007-05-16", + "2007-12-01", + "2009-09-06", + 0.009999999776482582, + 97916.0, + 1, + 2, + 97916.0, + 199, + -645, + ], + ], + ) + + +@pytest.mark.asyncio +async def test_source_node_query_with_filter_joinable_dimension( + module__client_with_examples: AsyncClient, +): + """ + Verify querying on source node with filter on joinable dimension + """ + await verify_node_sql( + custom_client=module__client_with_examples, + node_name="default.repair_orders", + dimensions=["default.hard_hat.state"], + filters=["default.hard_hat.state='NY'"], + expected_sql=""" + WITH default_DOT_repair_orders AS ( + SELECT default_DOT_repair_orders.repair_order_id, + default_DOT_repair_orders.municipality_id, + default_DOT_repair_orders.hard_hat_id, + default_DOT_repair_orders.order_date, + default_DOT_repair_orders.required_date, + default_DOT_repair_orders.dispatched_date, + default_DOT_repair_orders.dispatcher_id + FROM roads.repair_orders AS default_DOT_repair_orders + ), + default_DOT_repair_order AS ( + SELECT default_DOT_repair_orders.repair_order_id, + default_DOT_repair_orders.municipality_id, + default_DOT_repair_orders.hard_hat_id, + default_DOT_repair_orders.order_date, + default_DOT_repair_orders.required_date, + default_DOT_repair_orders.dispatched_date, + default_DOT_repair_orders.dispatcher_id + FROM roads.repair_orders AS default_DOT_repair_orders + ), + default_DOT_hard_hat AS ( + SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE default_DOT_hard_hats.state = 'NY' + ) + + SELECT default_DOT_repair_orders.repair_order_id default_DOT_repair_orders_DOT_repair_order_id, + default_DOT_repair_orders.municipality_id default_DOT_repair_orders_DOT_municipality_id, + default_DOT_repair_orders.hard_hat_id default_DOT_repair_orders_DOT_hard_hat_id, + default_DOT_repair_orders.order_date default_DOT_repair_orders_DOT_order_date, + default_DOT_repair_orders.required_date default_DOT_repair_orders_DOT_required_date, + default_DOT_repair_orders.dispatched_date default_DOT_repair_orders_DOT_dispatched_date, + default_DOT_repair_orders.dispatcher_id default_DOT_repair_orders_DOT_dispatcher_id, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state + FROM default_DOT_repair_orders INNER JOIN default_DOT_repair_order ON default_DOT_repair_orders.repair_order_id = default_DOT_repair_order.repair_order_id + INNER JOIN default_DOT_hard_hat ON default_DOT_repair_order.hard_hat_id = default_DOT_hard_hat.hard_hat_id + """, + expected_columns=[ + { + "column": "repair_order_id", + "name": "default_DOT_repair_orders_DOT_repair_order_id", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.repair_order_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "municipality_id", + "name": "default_DOT_repair_orders_DOT_municipality_id", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.municipality_id", + "semantic_type": None, + "type": "string", + }, + { + "column": "hard_hat_id", + "name": "default_DOT_repair_orders_DOT_hard_hat_id", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.hard_hat_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "order_date", + "name": "default_DOT_repair_orders_DOT_order_date", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.order_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "required_date", + "name": "default_DOT_repair_orders_DOT_required_date", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.required_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatched_date", + "name": "default_DOT_repair_orders_DOT_dispatched_date", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.dispatched_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatcher_id", + "name": "default_DOT_repair_orders_DOT_dispatcher_id", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.dispatcher_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "state", + "name": "default_DOT_hard_hat_DOT_state", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.state", + "semantic_type": None, + "type": "string", + }, + ], + expected_rows=[ + [ + 10021, + "Philadelphia", + 7, + "2007-05-10", + "2009-08-27", + "2007-12-01", + 3, + "NY", + ], + ], + ) + + +@pytest.mark.asyncio +async def test_source_node_sql_with_direct_filters( + module__client_with_examples: AsyncClient, +): + """ + Verify source node query generation with direct filters on the node. + """ + await verify_node_sql( + custom_client=module__client_with_examples, + node_name="default.repair_orders", + dimensions=[], + filters=["default.repair_orders.order_date='2009-08-14'"], + expected_sql=""" + WITH + default_DOT_repair_orders AS ( + SELECT + default_DOT_repair_orders.repair_order_id, + default_DOT_repair_orders.municipality_id, + default_DOT_repair_orders.hard_hat_id, + default_DOT_repair_orders.order_date, + default_DOT_repair_orders.required_date, + default_DOT_repair_orders.dispatched_date, + default_DOT_repair_orders.dispatcher_id + FROM ( + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM roads.repair_orders + WHERE order_date = '2009-08-14' + ) default_DOT_repair_orders + WHERE default_DOT_repair_orders.order_date = '2009-08-14' + ) + + SELECT + default_DOT_repair_orders.repair_order_id default_DOT_repair_orders_DOT_repair_order_id, + default_DOT_repair_orders.municipality_id default_DOT_repair_orders_DOT_municipality_id, + default_DOT_repair_orders.hard_hat_id default_DOT_repair_orders_DOT_hard_hat_id, + default_DOT_repair_orders.order_date default_DOT_repair_orders_DOT_order_date, + default_DOT_repair_orders.required_date default_DOT_repair_orders_DOT_required_date, + default_DOT_repair_orders.dispatched_date default_DOT_repair_orders_DOT_dispatched_date, + default_DOT_repair_orders.dispatcher_id default_DOT_repair_orders_DOT_dispatcher_id + FROM default_DOT_repair_orders + """, + expected_columns=[ + { + "column": "repair_order_id", + "name": "default_DOT_repair_orders_DOT_repair_order_id", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.repair_order_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "municipality_id", + "name": "default_DOT_repair_orders_DOT_municipality_id", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.municipality_id", + "semantic_type": None, + "type": "string", + }, + { + "column": "hard_hat_id", + "name": "default_DOT_repair_orders_DOT_hard_hat_id", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.hard_hat_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "order_date", + "name": "default_DOT_repair_orders_DOT_order_date", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.order_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "required_date", + "name": "default_DOT_repair_orders_DOT_required_date", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.required_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatched_date", + "name": "default_DOT_repair_orders_DOT_dispatched_date", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.dispatched_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatcher_id", + "name": "default_DOT_repair_orders_DOT_dispatcher_id", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.dispatcher_id", + "semantic_type": None, + "type": "int", + }, + ], + expected_rows=[], + ) + + +@pytest.mark.asyncio +async def test_dimension_node_sql_with_filters( + module__client_with_examples: AsyncClient, +): + """ + Verify dimension node query generation with direct filters on the node. + """ + await verify_node_sql( + custom_client=module__client_with_examples, + node_name="default.municipality", + dimensions=[], + filters=["default.municipality.state_id = 5"], + expected_sql=""" + WITH default_DOT_municipality AS ( + SELECT + default_DOT_municipality.municipality_id, + default_DOT_municipality.contact_name, + default_DOT_municipality.contact_title, + default_DOT_municipality.local_region, + default_DOT_municipality.phone, + default_DOT_municipality.state_id + FROM ( + SELECT + municipality_id, + contact_name, + contact_title, + local_region, + phone, + state_id + FROM roads.municipality + WHERE state_id = 5 + ) default_DOT_municipality + WHERE default_DOT_municipality.state_id = 5 + ) + SELECT + default_DOT_municipality.municipality_id default_DOT_municipality_DOT_municipality_id, + default_DOT_municipality.contact_name default_DOT_municipality_DOT_contact_name, + default_DOT_municipality.contact_title default_DOT_municipality_DOT_contact_title, + default_DOT_municipality.local_region default_DOT_municipality_DOT_local_region, + default_DOT_municipality.phone default_DOT_municipality_DOT_phone, + default_DOT_municipality.state_id default_DOT_municipality_DOT_state_id + FROM default_DOT_municipality + """, + expected_columns=[ + { + "column": "municipality_id", + "name": "default_DOT_municipality_DOT_municipality_id", + "node": "default.municipality", + "semantic_entity": "default.municipality.municipality_id", + "semantic_type": None, + "type": "string", + }, + { + "column": "contact_name", + "name": "default_DOT_municipality_DOT_contact_name", + "node": "default.municipality", + "semantic_entity": "default.municipality.contact_name", + "semantic_type": None, + "type": "string", + }, + { + "column": "contact_title", + "name": "default_DOT_municipality_DOT_contact_title", + "node": "default.municipality", + "semantic_entity": "default.municipality.contact_title", + "semantic_type": None, + "type": "string", + }, + { + "column": "local_region", + "name": "default_DOT_municipality_DOT_local_region", + "node": "default.municipality", + "semantic_entity": "default.municipality.local_region", + "semantic_type": None, + "type": "string", + }, + { + "column": "phone", + "name": "default_DOT_municipality_DOT_phone", + "node": "default.municipality", + "semantic_entity": "default.municipality.phone", + "semantic_type": None, + "type": "string", + }, + { + "column": "state_id", + "name": "default_DOT_municipality_DOT_state_id", + "node": "default.municipality", + "semantic_entity": "default.municipality.state_id", + "semantic_type": None, + "type": "int", + }, + ], + expected_rows=[ + [ + "Los Angeles", + "Hugh Moser", + "Administrative Assistant", + "Santa Monica", + "808-211-2323", + 5, + ], + [ + "San Diego", + "Ralph Helms", + "Senior Electrical Project Manager", + "Del Mar", + "491-813-2417", + 5, + ], + [ + "San Jose", + "Charles Carney", + "Municipal Accounting Manager", + "Santana Row", + "408-313-0698", + 5, + ], + ], + ) + + +@pytest.mark.asyncio +async def test_metric_with_node_level_and_nth_order_filters( + module__client_with_examples: AsyncClient, +): + """ + Verify metric SQL generation with filters on dimensions at the metric's + parent node level and filters on nth-order dimensions. + """ + await verify_node_sql( + custom_client=module__client_with_examples, + node_name="default.num_repair_orders", + dimensions=["default.hard_hat.state"], + filters=[ + "default.repair_orders_fact.dispatcher_id=1 OR " + "default.repair_orders_fact.dispatcher_id is not null", + "default.hard_hat.state='AZ'", + ], + expected_sql=""" + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id + WHERE + repair_orders.dispatcher_id = 1 OR repair_orders.dispatcher_id IS NOT NULL + ), default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE + default_DOT_hard_hats.state = 'AZ' + ), + default_DOT_repair_orders_fact_metrics AS ( + SELECT + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state, + count(default_DOT_repair_orders_fact.repair_order_id) default_DOT_num_repair_orders + FROM default_DOT_repair_orders_fact + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + GROUP BY default_DOT_hard_hat.state + ) + SELECT + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_state, + default_DOT_repair_orders_fact_metrics.default_DOT_num_repair_orders + FROM default_DOT_repair_orders_fact_metrics + """, + expected_columns=[ + { + "column": "state", + "name": "default_DOT_hard_hat_DOT_state", + "node": "default.hard_hat", + "type": "string", + "semantic_type": "dimension", + "semantic_entity": "default.hard_hat.state", + }, + { + "column": "default_DOT_num_repair_orders", + "name": "default_DOT_num_repair_orders", + "node": "default.num_repair_orders", + "type": "bigint", + "semantic_type": "metric", + "semantic_entity": "default.num_repair_orders.default_DOT_num_repair_orders", + }, + ], + expected_rows=[["AZ", 2]], + ) + + +@pytest.mark.asyncio +async def test_metric_with_nth_order_dimensions_filters( + module__client_with_examples: AsyncClient, +): + """ + Verify metric SQL generation that groups by nth-order dimensions and + filters on nth-order dimensions. + """ + + await verify_node_sql( + custom_client=module__client_with_examples, + node_name="default.num_repair_orders", + dimensions=[ + "default.hard_hat.city", + "default.hard_hat.last_name", + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + ], + filters=[ + "default.dispatcher.dispatcher_id=1", + "default.hard_hat.state != 'AZ'", + "default.dispatcher.phone = '4082021022'", + "default.repair_orders_fact.order_date >= '2020-01-01'", + ], + expected_sql=""" + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM ( + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM roads.repair_orders + WHERE dispatcher_id = 1 + ) repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id + WHERE + repair_orders.dispatcher_id = 1 AND repair_orders.order_date >= '2020-01-01' + ), default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE + default_DOT_hard_hats.state != 'AZ' + ), default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + WHERE + default_DOT_dispatchers.dispatcher_id = 1 + AND default_DOT_dispatchers.phone = '4082021022' + ), default_DOT_municipality_dim AS ( + SELECT + m.municipality_id AS municipality_id, + m.contact_name, + m.contact_title, + m.local_region, + m.state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM roads.municipality AS m + LEFT JOIN roads.municipality_municipality_type AS mmt + ON m.municipality_id = mmt.municipality_id + LEFT JOIN roads.municipality_type AS mt + ON mmt.municipality_type_id = mt.municipality_type_desc + ), default_DOT_repair_orders_fact_metrics AS ( + SELECT + default_DOT_hard_hat.city default_DOT_hard_hat_DOT_city, + default_DOT_hard_hat.last_name default_DOT_hard_hat_DOT_last_name, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region default_DOT_municipality_dim_DOT_local_region, + count(default_DOT_repair_orders_fact.repair_order_id) default_DOT_num_repair_orders + FROM default_DOT_repair_orders_fact + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher + ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_municipality_dim + ON default_DOT_repair_orders_fact.municipality_id = default_DOT_municipality_dim.municipality_id + GROUP BY + default_DOT_hard_hat.city, + default_DOT_hard_hat.last_name, + default_DOT_dispatcher.company_name, + default_DOT_municipality_dim.local_region + ) + SELECT + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_city, + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_last_name, + default_DOT_repair_orders_fact_metrics.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_metrics.default_DOT_municipality_dim_DOT_local_region, + default_DOT_repair_orders_fact_metrics.default_DOT_num_repair_orders + FROM default_DOT_repair_orders_fact_metrics + """, + expected_columns=[ + { + "name": "default_DOT_hard_hat_DOT_city", + "column": "city", + "node": "default.hard_hat", + "type": "string", + "semantic_type": "dimension", + "semantic_entity": "default.hard_hat.city", + }, + { + "name": "default_DOT_hard_hat_DOT_last_name", + "column": "last_name", + "node": "default.hard_hat", + "type": "string", + "semantic_type": "dimension", + "semantic_entity": "default.hard_hat.last_name", + }, + { + "name": "default_DOT_dispatcher_DOT_company_name", + "column": "company_name", + "node": "default.dispatcher", + "type": "string", + "semantic_type": "dimension", + "semantic_entity": "default.dispatcher.company_name", + }, + { + "name": "default_DOT_municipality_dim_DOT_local_region", + "column": "local_region", + "node": "default.municipality_dim", + "type": "string", + "semantic_type": "dimension", + "semantic_entity": "default.municipality_dim.local_region", + }, + { + "name": "default_DOT_num_repair_orders", + "column": "default_DOT_num_repair_orders", + "node": "default.num_repair_orders", + "type": "bigint", + "semantic_type": "metric", + "semantic_entity": "default.num_repair_orders.default_DOT_num_repair_orders", + }, + ], + expected_rows=[], + ) + + +@pytest.mark.asyncio +async def test_metric_with_second_order_dimensions( + module__client_with_examples: AsyncClient, +): + """ + Verify metric SQL generation with group by on second-order dimension. + """ + await verify_node_sql( + custom_client=module__client_with_examples, + node_name="default.avg_repair_price", + dimensions=["default.hard_hat.city"], + filters=[], + expected_sql=""" + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + ), default_DOT_repair_orders_fact_metrics AS ( + SELECT + default_DOT_hard_hat.city default_DOT_hard_hat_DOT_city, + avg(default_DOT_repair_orders_fact.price) default_DOT_avg_repair_price + FROM default_DOT_repair_orders_fact + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + GROUP BY default_DOT_hard_hat.city + ) + SELECT + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_city, + default_DOT_repair_orders_fact_metrics.default_DOT_avg_repair_price + FROM default_DOT_repair_orders_fact_metrics + """, + expected_columns=[ + { + "column": "city", + "name": "default_DOT_hard_hat_DOT_city", + "node": "default.hard_hat", + "type": "string", + "semantic_type": "dimension", + "semantic_entity": "default.hard_hat.city", + }, + { + "column": "default_DOT_avg_repair_price", + "name": "default_DOT_avg_repair_price", + "node": "default.avg_repair_price", + "type": "double", + "semantic_type": "metric", + "semantic_entity": "default.avg_repair_price.default_DOT_avg_repair_price", + }, + ], + expected_rows=[ + ["Jersey City", 54672.75], + ["Billerica", 76555.33333333333], + ["Southgate", 64190.6], + ["Phoenix", 65682.0], + ["Southampton", 54083.5], + ["Powder Springs", 65595.66666666667], + ["Middletown", 39301.5], + ["Muskogee", 70418.0], + ["Niagara Falls", 53374.0], + ], + ) + + +@pytest.mark.asyncio +async def test_metric_with_nth_order_dimensions( + module__client_with_examples: AsyncClient, +): + """ + Verify metric SQL generation with group by on nth-order dimension. + """ + await verify_node_sql( + custom_client=module__client_with_examples, + node_name="default.avg_repair_price", + dimensions=["default.hard_hat.city", "default.dispatcher.company_name"], + filters=[], + expected_sql=""" + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id +), default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats +), default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers +), +default_DOT_repair_orders_fact_metrics AS ( + SELECT + default_DOT_hard_hat.city default_DOT_hard_hat_DOT_city, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + avg(default_DOT_repair_orders_fact.price) default_DOT_avg_repair_price + FROM default_DOT_repair_orders_fact + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher + ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + GROUP BY + default_DOT_hard_hat.city, + default_DOT_dispatcher.company_name +) +SELECT + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_city, + default_DOT_repair_orders_fact_metrics.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_metrics.default_DOT_avg_repair_price +FROM default_DOT_repair_orders_fact_metrics + """, + expected_columns=[ + { + "column": "city", + "name": "default_DOT_hard_hat_DOT_city", + "node": "default.hard_hat", + "type": "string", + "semantic_type": "dimension", + "semantic_entity": "default.hard_hat.city", + }, + { + "column": "company_name", + "name": "default_DOT_dispatcher_DOT_company_name", + "node": "default.dispatcher", + "type": "string", + "semantic_type": "dimension", + "semantic_entity": "default.dispatcher.company_name", + }, + { + "column": "default_DOT_avg_repair_price", + "name": "default_DOT_avg_repair_price", + "node": "default.avg_repair_price", + "type": "double", + "semantic_type": "metric", + "semantic_entity": "default.avg_repair_price.default_DOT_avg_repair_price", + }, + ], + expected_rows=[ + ["Jersey City", "Federal Roads Group", 63708.0], + ["Billerica", "Pothole Pete", 67253.0], + ["Southgate", "Asphalts R Us", 57332.5], + ["Jersey City", "Pothole Pete", 51661.0], + ["Phoenix", "Asphalts R Us", 76463.0], + ["Billerica", "Asphalts R Us", 81206.5], + ["Southampton", "Asphalts R Us", 63918.0], + ["Southgate", "Federal Roads Group", 59499.5], + ["Southampton", "Federal Roads Group", 27222.0], + ["Southampton", "Pothole Pete", 62597.0], + ["Phoenix", "Federal Roads Group", 54901.0], + ["Powder Springs", "Asphalts R Us", 66929.5], + ["Middletown", "Federal Roads Group", 39301.5], + ["Muskogee", "Federal Roads Group", 70418.0], + ["Powder Springs", "Pothole Pete", 62928.0], + ["Niagara Falls", "Federal Roads Group", 53374.0], + ["Southgate", "Pothole Pete", 87289.0], + ], + ) + + +@pytest.mark.asyncio +async def test_metric_sql_without_dimensions_filters( + module__client_with_examples: AsyncClient, +): + """ + Verify metric SQL generation without group by dimensions or filters. + """ + await verify_node_sql( + custom_client=module__client_with_examples, + node_name="default.num_repair_orders", + dimensions=[], + filters=[], + expected_sql=""" + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_repair_orders_fact_metrics AS ( + SELECT + count(default_DOT_repair_orders_fact.repair_order_id) default_DOT_num_repair_orders + FROM default_DOT_repair_orders_fact + ) + SELECT + default_DOT_repair_orders_fact_metrics.default_DOT_num_repair_orders + FROM default_DOT_repair_orders_fact_metrics + """, + expected_columns=[ + { + "column": "default_DOT_num_repair_orders", + "name": "default_DOT_num_repair_orders", + "node": "default.num_repair_orders", + "type": "bigint", + "semantic_type": "metric", + "semantic_entity": "default.num_repair_orders.default_DOT_num_repair_orders", + }, + ], + expected_rows=[ + [25], + ], + ) + + +@pytest.mark.asyncio +async def test_source_sql_joinable_dimension_and_filter( + module__client_with_examples: AsyncClient, +): + """ + Verify source SQL generation with joinable dimension and filters. + """ + await verify_node_sql( + custom_client=module__client_with_examples, + node_name="default.repair_orders", + dimensions=["default.hard_hat.state"], + filters=["default.hard_hat.state='NY'"], + expected_sql=""" + WITH default_DOT_repair_orders AS ( + SELECT + default_DOT_repair_orders.repair_order_id, + default_DOT_repair_orders.municipality_id, + default_DOT_repair_orders.hard_hat_id, + default_DOT_repair_orders.order_date, + default_DOT_repair_orders.required_date, + default_DOT_repair_orders.dispatched_date, + default_DOT_repair_orders.dispatcher_id + FROM roads.repair_orders AS default_DOT_repair_orders + ), + default_DOT_repair_order AS ( + SELECT + default_DOT_repair_orders.repair_order_id, + default_DOT_repair_orders.municipality_id, + default_DOT_repair_orders.hard_hat_id, + default_DOT_repair_orders.order_date, + default_DOT_repair_orders.required_date, + default_DOT_repair_orders.dispatched_date, + default_DOT_repair_orders.dispatcher_id + FROM roads.repair_orders AS default_DOT_repair_orders + ), + default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE default_DOT_hard_hats.state = 'NY' + ) + SELECT default_DOT_repair_orders.repair_order_id default_DOT_repair_orders_DOT_repair_order_id, + default_DOT_repair_orders.municipality_id default_DOT_repair_orders_DOT_municipality_id, + default_DOT_repair_orders.hard_hat_id default_DOT_repair_orders_DOT_hard_hat_id, + default_DOT_repair_orders.order_date default_DOT_repair_orders_DOT_order_date, + default_DOT_repair_orders.required_date default_DOT_repair_orders_DOT_required_date, + default_DOT_repair_orders.dispatched_date default_DOT_repair_orders_DOT_dispatched_date, + default_DOT_repair_orders.dispatcher_id default_DOT_repair_orders_DOT_dispatcher_id, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state + FROM default_DOT_repair_orders + INNER JOIN default_DOT_repair_order + ON default_DOT_repair_orders.repair_order_id = default_DOT_repair_order.repair_order_id + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_order.hard_hat_id = default_DOT_hard_hat.hard_hat_id + """, + expected_columns=[ + { + "column": "repair_order_id", + "name": "default_DOT_repair_orders_DOT_repair_order_id", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.repair_order_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "municipality_id", + "name": "default_DOT_repair_orders_DOT_municipality_id", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.municipality_id", + "semantic_type": None, + "type": "string", + }, + { + "column": "hard_hat_id", + "name": "default_DOT_repair_orders_DOT_hard_hat_id", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.hard_hat_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "order_date", + "name": "default_DOT_repair_orders_DOT_order_date", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.order_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "required_date", + "name": "default_DOT_repair_orders_DOT_required_date", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.required_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatched_date", + "name": "default_DOT_repair_orders_DOT_dispatched_date", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.dispatched_date", + "semantic_type": None, + "type": "timestamp", + }, + { + "column": "dispatcher_id", + "name": "default_DOT_repair_orders_DOT_dispatcher_id", + "node": "default.repair_orders", + "semantic_entity": "default.repair_orders.dispatcher_id", + "semantic_type": None, + "type": "int", + }, + { + "column": "state", + "name": "default_DOT_hard_hat_DOT_state", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.state", + "semantic_type": None, + "type": "string", + }, + ], + expected_rows=[ + [ + 10021, + "Philadelphia", + 7, + "2007-05-10", + "2009-08-27", + "2007-12-01", + 3, + "NY", + ], + ], + ) + + +@pytest.mark.asyncio +async def test_metric_with_joinable_dimension_multiple_hops( + module__client_with_examples: AsyncClient, +): + """ + Verify metric SQL generation with joinable nth-order dimensions. + """ + await verify_node_sql( + custom_client=module__client_with_examples, + node_name="default.num_repair_orders", + dimensions=["default.us_state.state_short"], + filters=[], + expected_sql=""" + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + ), default_DOT_repair_orders_fact_metrics AS ( + SELECT + default_DOT_hard_hat.state default_DOT_us_state_DOT_state_short, + count(default_DOT_repair_orders_fact.repair_order_id) default_DOT_num_repair_orders + FROM default_DOT_repair_orders_fact + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + GROUP BY default_DOT_hard_hat.state + ) + SELECT + default_DOT_repair_orders_fact_metrics.default_DOT_us_state_DOT_state_short, + default_DOT_repair_orders_fact_metrics.default_DOT_num_repair_orders + FROM default_DOT_repair_orders_fact_metrics + """, + expected_columns=[ + { + "column": "state_short", + "name": "default_DOT_us_state_DOT_state_short", + "node": "default.us_state", + "type": "string", + "semantic_type": "dimension", + "semantic_entity": "default.us_state.state_short", + }, + { + "column": "default_DOT_num_repair_orders", + "name": "default_DOT_num_repair_orders", + "node": "default.num_repair_orders", + "type": "bigint", + "semantic_type": "metric", + "semantic_entity": "default.num_repair_orders.default_DOT_num_repair_orders", + }, + ], + expected_rows=[ + ["NJ", 4], + ["MA", 3], + ["MI", 5], + ["AZ", 2], + ["PA", 4], + ["GA", 3], + ["CT", 2], + ["OK", 1], + ["NY", 1], + ], + ) + + +@pytest.mark.asyncio +async def test_union_all( + module__client_with_examples: AsyncClient, +): + """ + Verify union all query works + """ + response = await module__client_with_examples.post( + "/nodes/transform", + json={ + "name": "default.union_all_test", + "description": "", + "display_name": "Union All Test", + "query": """ + ( + SELECT + 1234 AS farmer_id, + 2234 AS farm_id, + 'pear' AS fruit_name, + 4444 AS fruit_id, + 20 AS fruits_cnt + ) + UNION ALL + ( + SELECT + NULL AS farmer_id, + NULL AS farm_id, + NULL AS fruit_name, + NULL AS fruit_id, + NULL AS fruits_cnt + )""", + "mode": "published", + }, + ) + assert response.status_code == 201 + + response = await module__client_with_examples.get("/sql/default.union_all_test") + assert str(parse(response.json()["sql"])) == str( + parse( + """ + WITH default_DOT_union_all_test AS ( + (SELECT 1234 AS farmer_id, + 2234 AS farm_id, + 'pear' AS fruit_name, + 4444 AS fruit_id, + 20 AS fruits_cnt) + + UNION ALL + (SELECT NULL AS farmer_id, + NULL AS farm_id, + NULL AS fruit_name, + NULL AS fruit_id, + NULL AS fruits_cnt) + ) + + SELECT default_DOT_union_all_test.farmer_id default_DOT_union_all_test_DOT_farmer_id, + default_DOT_union_all_test.farm_id default_DOT_union_all_test_DOT_farm_id, + default_DOT_union_all_test.fruit_name default_DOT_union_all_test_DOT_fruit_name, + default_DOT_union_all_test.fruit_id default_DOT_union_all_test_DOT_fruit_id, + default_DOT_union_all_test.fruits_cnt default_DOT_union_all_test_DOT_fruits_cnt + FROM default_DOT_union_all_test + """, + ), + ) + + +@pytest.mark.asyncio +async def test_multiple_joins_to_same_node( + module__client_with_examples: AsyncClient, +): + """ + Verify that multiple joins to the same node still work + """ + response = await module__client_with_examples.post( + "/nodes/transform", + json={ + "name": "default.sowtime", + "description": "", + "display_name": "Sow Time", + "query": """ + SELECT + 'Davis' AS city_name, + 'fruit' AS fruit, + 3 AS month + """, + "mode": "published", + }, + ) + assert response.status_code == 201 + + response = await module__client_with_examples.post( + "/nodes/transform", + json={ + "name": "default.fruits", + "description": "", + "display_name": "Fruits", + "query": """ + SELECT + 1234 AS farmer_id, + 2234 AS farm_id, + 'pear' AS primary, + 'avocado' AS companion, + 'Davis' AS city_name + """, + "mode": "published", + }, + ) + assert response.status_code == 201 + + response = await module__client_with_examples.post( + "/nodes/transform", + json={ + "name": "default.multiple_join_same_node", + "description": "", + "display_name": "Multiple Joins to Same Node", + "query": """ + SELECT + f.farmer_id, + s_start.month AS primary_sow_start_month, + s_end.month AS companion_sow_start_month + FROM default.fruits AS f + LEFT JOIN default.sowtime AS s_start + ON f.city_name = s_start.city_name + AND f.primary = s_start.fruit + LEFT JOIN default.sowtime AS s_end + ON f.city_name = s_end.city_name + AND f.companion = s_end.fruit + """, + "mode": "published", + }, + ) + assert response.status_code == 201 + + response = await module__client_with_examples.get( + "/sql/default.multiple_join_same_node", + ) + assert str(parse(response.json()["sql"])) == str( + parse( + """ + WITH default_DOT_fruits AS ( + SELECT 1234 AS farmer_id, + 2234 AS farm_id, + 'pear' AS primary, + 'avocado' AS companion, + 'Davis' AS city_name + ), + default_DOT_sowtime AS ( + SELECT 'Davis' AS city_name, + 'fruit' AS fruit, + 3 AS month + ), + default_DOT_multiple_join_same_node AS ( + SELECT f.farmer_id, + s_start.month AS primary_sow_start_month, + s_end.month AS companion_sow_start_month + FROM default_DOT_fruits f LEFT JOIN default_DOT_sowtime s_start ON f.city_name = s_start.city_name AND f.primary = s_start.fruit + LEFT JOIN default_DOT_sowtime s_end ON f.city_name = s_end.city_name AND f.companion = s_end.fruit + ) + SELECT + default_DOT_multiple_join_same_node.farmer_id default_DOT_multiple_join_same_node_DOT_farmer_id, + default_DOT_multiple_join_same_node.primary_sow_start_month default_DOT_multiple_join_same_node_DOT_primary_sow_start_month, + default_DOT_multiple_join_same_node.companion_sow_start_month default_DOT_multiple_join_same_node_DOT_companion_sow_start_month + FROM default_DOT_multiple_join_same_node""", + ), + ) + + +@pytest.mark.asyncio +async def test_cross_join_unnest( + module__client_with_examples: AsyncClient, +): + """ + Verify cross join unnest on a joined in dimension works + """ + await module__client_with_examples.post( + "/nodes/basic.corrected_patches/columns/color_id/" + "?dimension=basic.paint_colors_trino&dimension_column=color_id", + ) + response = await module__client_with_examples.get( + "/sql/basic.avg_luminosity_patches/", + params={ + "filters": [], + "dimensions": [ + "basic.paint_colors_trino.color_id", + "basic.paint_colors_trino.color_name", + ], + }, + ) + expected = """ + SELECT + paint_colors_trino.color_id basic_DOT_avg_luminosity_patches_DOT_color_id, + basic_DOT_paint_colors_trino.color_name basic_DOT_avg_luminosity_patches_DOT_color_name, + AVG(basic_DOT_corrected_patches.luminosity) AS basic_DOT_avg_luminosity_patches_DOT_basic_DOT_avg_luminosity_patches + FROM ( + SELECT + CAST(basic_DOT_patches.color_id AS VARCHAR) color_id, + basic_DOT_patches.color_name, + basic_DOT_patches.garishness, + basic_DOT_patches.luminosity, + basic_DOT_patches.opacity + FROM basic.patches AS basic_DOT_patches + ) AS basic_DOT_corrected_patches + LEFT JOIN ( + SELECT + t.color_name color_name, + t.color_id + FROM ( + SELECT + basic_DOT_murals.id, + basic_DOT_murals.colors + FROM basic.murals AS basic_DOT_murals + ) murals + CROSS JOIN UNNEST(murals.colors) t( color_id, color_name) + ) AS basic_DOT_paint_colors_trino + ON basic_DOT_corrected_patches.color_id = basic_DOT_paint_colors_trino.color_id + GROUP BY + paint_colors_trino.color_id, + basic_DOT_paint_colors_trino.color_name + """ + query = response.json()["sql"] + compare_query_strings(query, expected) + + +@pytest.mark.asyncio +async def test_lateral_view_explode( + module__client_with_examples: AsyncClient, +): + """ + Verify lateral view explode on a joined in dimension works + """ + await module__client_with_examples.post( + "/nodes/basic.corrected_patches/columns/color_id/" + "?dimension=basic.paint_colors_spark&dimension_column=color_id", + ) + response = await module__client_with_examples.get( + "/sql/basic.avg_luminosity_patches/", + params={ + "filters": [], + "dimensions": [ + "basic.paint_colors_spark.color_id", + "basic.paint_colors_spark.color_name", + ], + "limit": 5, + }, + ) + expected = """ + SELECT AVG(basic_DOT_corrected_patches.luminosity) basic_DOT_avg_luminosity_patches, + basic_DOT_paint_colors_spark.color_id basic_DOT_paint_colors_spark_DOT_color_id, + basic_DOT_paint_colors_spark.color_name basic_DOT_paint_colors_spark_DOT_color_name + FROM (SELECT CAST(basic_DOT_patches.color_id AS STRING) color_id, + basic_DOT_patches.color_name, + basic_DOT_patches.opacity, + basic_DOT_patches.luminosity, + basic_DOT_patches.garishness + FROM basic.patches AS basic_DOT_patches) + AS basic_DOT_corrected_patches LEFT JOIN (SELECT color_id, + color_name + FROM (SELECT basic_DOT_murals.id, + EXPLODE(basic_DOT_murals.colors) AS ( color_id, color_name) + FROM basic.murals AS basic_DOT_murals + + )) + AS basic_DOT_paint_colors_spark ON basic_DOT_corrected_patches.color_id = basic_DOT_paint_colors_spark.color_id + GROUP BY basic_DOT_paint_colors_spark.color_id, basic_DOT_paint_colors_spark.color_name + + LIMIT 5 + """ + query = response.json()["sql"] + compare_query_strings(query, expected) + + +@pytest.mark.asyncio +async def test_get_sql_for_metrics_failures(module__client_with_examples: AsyncClient): + """ + Test failure modes when getting sql for multiple metrics. + """ + # Getting sql for no metrics fails appropriately + response = await module__client_with_examples.get( + "/sql/", + params={ + "metrics": [], + "dimensions": ["default.account_type.account_type_name"], + "filters": [], + }, + ) + assert response.status_code == 422 + data = response.json() + assert data == { + "message": "At least one metric is required", + "errors": [], + "warnings": [], + } + + # Getting sql for metric with no dimensions works + response = await module__client_with_examples.get( + "/sql/", + params={ + "metrics": ["default.number_of_account_types"], + "dimensions": [], + "filters": [], + }, + ) + assert response.status_code == 200 + + # Getting sql for metric with non-metric node + response = await module__client_with_examples.get( + "/sql/", + params={ + "metrics": ["default.repair_orders"], + "dimensions": [], + "filters": [], + }, + ) + assert response.status_code == 422 + assert response.json() == { + "message": "All nodes must be of metric type, but some are not: default.repair_orders (source) .", + "errors": [], + "warnings": [], + } + + +@pytest.mark.asyncio +async def test_get_sql_for_metrics_no_access(module__client_with_examples: AsyncClient): + """ + Test getting sql for multiple metrics. + """ + + def validate_access_override(): + def _validate_access(access_control: access.AccessControl): + if access_control.state == "direct": + access_control.approve_all() + else: + access_control.deny_all() + + return _validate_access + + module__client_with_examples.app.dependency_overrides[validate_access] = ( + validate_access_override + ) + + response = await module__client_with_examples.get( + "/sql/", + params={ + "metrics": ["default.discounted_orders_rate", "default.num_repair_orders"], + "dimensions": [ + "default.hard_hat.country", + "default.hard_hat.postal_code", + "default.hard_hat.city", + "default.hard_hat.state", + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + ], + "filters": ["default.hard_hat.city = 'Las Vegas'"], + "orderby": [], + "limit": 100, + }, + ) + data = response.json() + # assert "Authorization of User `dj` for this request failed.\n" in data["message"] + assert "The following requests were denied:\n" in data["message"] + assert "read:node/default.municipality_dim" in data["message"] + assert "read:node/default.dispatcher" in data["message"] + assert "read:node/default.repair_orders_fact" in data["message"] + assert "read:node/default.hard_hat" in data["message"] + assert data["errors"][0]["code"] == 500 + + module__client_with_examples.app.dependency_overrides[validate_access] = ( + validate_access + ) + + module__client_with_examples.app.dependency_overrides.clear() + + +@pytest.mark.asyncio +async def test_get_sql_for_metrics2(module__client_with_examples: AsyncClient): + """ + Test getting sql for multiple metrics. + """ + + response = await module__client_with_examples.get( + "/sql/", + params={ + "metrics": ["default.discounted_orders_rate", "default.num_repair_orders"], + "dimensions": [ + "default.hard_hat.country", + "default.hard_hat.postal_code", + "default.hard_hat.city", + "default.hard_hat.state", + "default.dispatcher.company_name", + "default.municipality_dim.local_region", + ], + "filters": [], + "orderby": [ + "default.hard_hat.country", + "default.num_repair_orders", + "default.dispatcher.company_name", + "default.discounted_orders_rate", + ], + "limit": 100, + }, + ) + data = response.json() + expected_sql = """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + ), default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), default_DOT_municipality_dim AS ( + SELECT + m.municipality_id AS municipality_id, + m.contact_name, + m.contact_title, + m.local_region, + m.state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM roads.municipality AS m + LEFT JOIN roads.municipality_municipality_type AS mmt + ON m.municipality_id = mmt.municipality_id + LEFT JOIN roads.municipality_type AS mt + ON mmt.municipality_type_id = mt.municipality_type_desc + ), + default_DOT_repair_orders_fact_metrics AS ( + SELECT + default_DOT_hard_hat.country default_DOT_hard_hat_DOT_country, + default_DOT_hard_hat.postal_code default_DOT_hard_hat_DOT_postal_code, + default_DOT_hard_hat.city default_DOT_hard_hat_DOT_city, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_municipality_dim.local_region default_DOT_municipality_dim_DOT_local_region, + CAST(sum(if(default_DOT_repair_orders_fact.discount > 0.0, 1, 0)) AS DOUBLE) / count(*) AS default_DOT_discounted_orders_rate, + count(default_DOT_repair_orders_fact.repair_order_id) default_DOT_num_repair_orders + FROM default_DOT_repair_orders_fact + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_dispatcher + ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_municipality_dim + ON default_DOT_repair_orders_fact.municipality_id = default_DOT_municipality_dim.municipality_id + GROUP BY + default_DOT_hard_hat.country, + default_DOT_hard_hat.postal_code, + default_DOT_hard_hat.city, + default_DOT_hard_hat.state, + default_DOT_dispatcher.company_name, + default_DOT_municipality_dim.local_region + ) + SELECT + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_country, + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_postal_code, + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_city, + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_state, + default_DOT_repair_orders_fact_metrics.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_metrics.default_DOT_municipality_dim_DOT_local_region, + default_DOT_repair_orders_fact_metrics.default_DOT_discounted_orders_rate, + default_DOT_repair_orders_fact_metrics.default_DOT_num_repair_orders + FROM default_DOT_repair_orders_fact_metrics + ORDER BY + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_country, + default_DOT_repair_orders_fact_metrics.default_DOT_num_repair_orders, + default_DOT_repair_orders_fact_metrics.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_metrics.default_DOT_discounted_orders_rate + LIMIT 100 + """ + assert str(parse(data["sql"])) == str(parse(expected_sql)) + assert data["columns"] == [ + { + "column": "country", + "name": "default_DOT_hard_hat_DOT_country", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.country", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "postal_code", + "name": "default_DOT_hard_hat_DOT_postal_code", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.postal_code", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "city", + "name": "default_DOT_hard_hat_DOT_city", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.city", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "state", + "name": "default_DOT_hard_hat_DOT_state", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.state", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "company_name", + "name": "default_DOT_dispatcher_DOT_company_name", + "node": "default.dispatcher", + "semantic_entity": "default.dispatcher.company_name", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "local_region", + "name": "default_DOT_municipality_dim_DOT_local_region", + "node": "default.municipality_dim", + "semantic_entity": "default.municipality_dim.local_region", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "default_DOT_discounted_orders_rate", + "name": "default_DOT_discounted_orders_rate", + "node": "default.discounted_orders_rate", + "semantic_entity": "default.discounted_orders_rate.default_DOT_discounted_orders_rate", + "semantic_type": "metric", + "type": "double", + }, + { + "column": "default_DOT_num_repair_orders", + "name": "default_DOT_num_repair_orders", + "node": "default.num_repair_orders", + "semantic_entity": "default.num_repair_orders.default_DOT_num_repair_orders", + "semantic_type": "metric", + "type": "bigint", + }, + ] + + +@pytest.mark.asyncio +async def test_get_sql_including_dimension_ids( + module__client_with_examples: AsyncClient, +): + """ + Test getting SQL when there are dimensions ids included + """ + response = await module__client_with_examples.get( + "/sql/", + params={ + "metrics": ["default.avg_repair_price", "default.total_repair_cost"], + "dimensions": [ + "default.dispatcher.company_name", + "default.dispatcher.dispatcher_id", + ], + "filters": [], + }, + ) + assert response.status_code == 200 + data = response.json() + expected = """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id +), default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers +), default_DOT_repair_orders_fact_metrics AS ( + SELECT + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_dispatcher.dispatcher_id default_DOT_dispatcher_DOT_dispatcher_id, + avg(default_DOT_repair_orders_fact.price) default_DOT_avg_repair_price, + sum(default_DOT_repair_orders_fact.total_repair_cost) default_DOT_total_repair_cost + FROM default_DOT_repair_orders_fact + INNER JOIN default_DOT_dispatcher + ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + GROUP BY + default_DOT_dispatcher.company_name, + default_DOT_dispatcher.dispatcher_id +) +SELECT + default_DOT_repair_orders_fact_metrics.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_metrics.default_DOT_dispatcher_DOT_dispatcher_id, + default_DOT_repair_orders_fact_metrics.default_DOT_avg_repair_price, + default_DOT_repair_orders_fact_metrics.default_DOT_total_repair_cost +FROM default_DOT_repair_orders_fact_metrics +""" + assert str(parse(str(data["sql"]))) == str(parse(str(expected))) + + response = await module__client_with_examples.get( + "/sql/", + params={ + "metrics": ["default.avg_repair_price", "default.total_repair_cost"], + "dimensions": [ + "default.hard_hat.hard_hat_id", + "default.hard_hat.first_name", + ], + "filters": [], + }, + ) + assert response.status_code == 200 + data = response.json() + assert str(parse(str(data["sql"]))) == str( + parse( + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id +), default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats +), default_DOT_repair_orders_fact_metrics AS ( + SELECT + default_DOT_repair_orders_fact.hard_hat_id default_DOT_hard_hat_DOT_hard_hat_id, + default_DOT_hard_hat.first_name default_DOT_hard_hat_DOT_first_name, + avg(default_DOT_repair_orders_fact.price) default_DOT_avg_repair_price, + sum(default_DOT_repair_orders_fact.total_repair_cost) default_DOT_total_repair_cost + FROM default_DOT_repair_orders_fact + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + GROUP BY + default_DOT_repair_orders_fact.hard_hat_id, + default_DOT_hard_hat.first_name +) +SELECT + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_hard_hat_id, + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_first_name, + default_DOT_repair_orders_fact_metrics.default_DOT_avg_repair_price, + default_DOT_repair_orders_fact_metrics.default_DOT_total_repair_cost +FROM default_DOT_repair_orders_fact_metrics + """, + ), + ) + + +@pytest.mark.asyncio +async def test_get_sql_including_dimensions_with_disambiguated_columns( + module__client_with_examples: AsyncClient, + duckdb_conn: duckdb.DuckDBPyConnection, +): + """ + Test getting SQL that includes dimensions with SQL that has to disambiguate projection columns with prefixes + """ + response = await module__client_with_examples.get( + "/sql/", + params={ + "metrics": ["default.total_repair_cost"], + "dimensions": [ + "default.municipality_dim.state_id", + "default.municipality_dim.municipality_type_id", + "default.municipality_dim.municipality_type_desc", + "default.municipality_dim.municipality_id", + ], + "filters": [], + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["columns"] == [ + { + "column": "state_id", + "name": "default_DOT_municipality_dim_DOT_state_id", + "node": "default.municipality_dim", + "semantic_entity": "default.municipality_dim.state_id", + "semantic_type": "dimension", + "type": "int", + }, + { + "column": "municipality_type_id", + "name": "default_DOT_municipality_dim_DOT_municipality_type_id", + "node": "default.municipality_dim", + "semantic_entity": "default.municipality_dim.municipality_type_id", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "municipality_type_desc", + "name": "default_DOT_municipality_dim_DOT_municipality_type_desc", + "node": "default.municipality_dim", + "semantic_entity": "default.municipality_dim.municipality_type_desc", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "municipality_id", + "name": "default_DOT_municipality_dim_DOT_municipality_id", + "node": "default.municipality_dim", + "semantic_entity": "default.municipality_dim.municipality_id", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "default_DOT_total_repair_cost", + "name": "default_DOT_total_repair_cost", + "node": "default.total_repair_cost", + "semantic_entity": "default.total_repair_cost.default_DOT_total_repair_cost", + "semantic_type": "metric", + "type": "double", + }, + ] + assert str(parse(data["sql"])) == str( + parse( + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id +), default_DOT_municipality_dim AS ( + SELECT + m.municipality_id AS municipality_id, + m.contact_name, + m.contact_title, + m.local_region, + m.state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM roads.municipality AS m + LEFT JOIN roads.municipality_municipality_type AS mmt + ON m.municipality_id = mmt.municipality_id + LEFT JOIN roads.municipality_type AS mt + ON mmt.municipality_type_id = mt.municipality_type_desc +), default_DOT_repair_orders_fact_metrics AS ( + SELECT + default_DOT_municipality_dim.state_id default_DOT_municipality_dim_DOT_state_id, + default_DOT_municipality_dim.municipality_type_id default_DOT_municipality_dim_DOT_municipality_type_id, + default_DOT_municipality_dim.municipality_type_desc default_DOT_municipality_dim_DOT_municipality_type_desc, + default_DOT_municipality_dim.municipality_id default_DOT_municipality_dim_DOT_municipality_id, + sum(default_DOT_repair_orders_fact.total_repair_cost) default_DOT_total_repair_cost + FROM default_DOT_repair_orders_fact + INNER JOIN default_DOT_municipality_dim + ON default_DOT_repair_orders_fact.municipality_id = default_DOT_municipality_dim.municipality_id + GROUP BY + default_DOT_municipality_dim.state_id, + default_DOT_municipality_dim.municipality_type_id, + default_DOT_municipality_dim.municipality_type_desc, + default_DOT_municipality_dim.municipality_id +) +SELECT + default_DOT_repair_orders_fact_metrics.default_DOT_municipality_dim_DOT_state_id, + default_DOT_repair_orders_fact_metrics.default_DOT_municipality_dim_DOT_municipality_type_id, + default_DOT_repair_orders_fact_metrics.default_DOT_municipality_dim_DOT_municipality_type_desc, + default_DOT_repair_orders_fact_metrics.default_DOT_municipality_dim_DOT_municipality_id, + default_DOT_repair_orders_fact_metrics.default_DOT_total_repair_cost +FROM default_DOT_repair_orders_fact_metrics + """, + ), + ) + result = duckdb_conn.sql(data["sql"]) + assert result.fetchall() == [ + (33, "A", None, "New York", 285627.0), + (44, "A", None, "Dallas", 18497.0), + (44, "A", None, "San Antonio", 76463.0), + (39, "B", None, "Philadelphia", 1135603.0), + ] + + response = await module__client_with_examples.get( + "/sql/", + params={ + "metrics": ["default.avg_repair_price", "default.total_repair_cost"], + "dimensions": [ + "default.hard_hat.hard_hat_id", + ], + "filters": [], + }, + ) + assert response.status_code == 200 + data = response.json() + expected = """WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id +), default_DOT_repair_orders_fact_metrics AS ( + SELECT + default_DOT_repair_orders_fact.hard_hat_id default_DOT_hard_hat_DOT_hard_hat_id, + avg(default_DOT_repair_orders_fact.price) default_DOT_avg_repair_price, + sum(default_DOT_repair_orders_fact.total_repair_cost) default_DOT_total_repair_cost + FROM default_DOT_repair_orders_fact + GROUP BY + default_DOT_repair_orders_fact.hard_hat_id +) +SELECT + default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_hard_hat_id, + default_DOT_repair_orders_fact_metrics.default_DOT_avg_repair_price, + default_DOT_repair_orders_fact_metrics.default_DOT_total_repair_cost +FROM default_DOT_repair_orders_fact_metrics +""" + + assert str(parse(data["sql"])) == str(parse(expected)) + + result = duckdb_conn.sql(data["sql"]) + assert result.fetchall() == [ + (1, 54672.75, 218691.0), + (2, 39301.5, 78603.0), + (3, 76555.33333333333, 229666.0), + (4, 54083.5, 216334.0), + (5, 64190.6, 320953.0), + (6, 65595.66666666667, 196787.0), + (7, 53374.0, 53374.0), + (8, 65682.0, 131364.0), + (9, 70418.0, 70418.0), + ] + + +@pytest.mark.asyncio +async def test_get_sql_for_metrics_filters_validate_dimensions( + module__client_with_examples: AsyncClient, +): + """ + Test that we extract the columns from filters to validate that they are from shared dimensions + """ + response = await module__client_with_examples.get( + "/sql/", + params={ + "metrics": ["foo.bar.num_repair_orders", "foo.bar.avg_repair_price"], + "dimensions": [ + "foo.bar.hard_hat.country", + ], + "filters": ["default.hard_hat.city = 'Las Vegas'"], + "limit": 10, + "ignore_errors": False, + }, + ) + data = response.json() + assert ( + "This dimension attribute cannot be joined in: default.hard_hat.city" + in data["message"] + ) + + +@pytest.mark.asyncio +async def test_get_sql_for_metrics_orderby_not_in_dimensions( + module__client_with_examples: AsyncClient, +): + """ + Test that we extract the columns from filters to validate that they are from shared dimensions + """ + response = await module__client_with_examples.get( + "/sql/", + params={ + "metrics": ["foo.bar.num_repair_orders", "foo.bar.avg_repair_price"], + "dimensions": [ + "foo.bar.hard_hat.country", + ], + "orderby": ["default.hard_hat.city"], + "limit": 10, + }, + ) + data = response.json() + assert data["message"] == ( + "Columns ['default.hard_hat.city'] in order by " + "clause must also be specified in the metrics or dimensions" + ) + + +@pytest.mark.asyncio +async def test_get_sql_for_metrics_orderby_not_in_dimensions_no_access( + module__client_with_examples: AsyncClient, +): + """ + Test that we extract the columns from filters to validate that they are from shared dimensions + """ + + def validate_access_override(): + def _validate_access(access_control: access.AccessControl): + for request in access_control.requests: + if ( + request.access_object.resource_type == access.ResourceType.NODE + and request.access_object.name + in ( + "foo.bar.avg_repair_price", + "default.hard_hat.city", + ) + ): + request.deny() + else: + request.approve() + + return _validate_access + + module__client_with_examples.app.dependency_overrides[validate_access] = ( + validate_access_override + ) + response = await module__client_with_examples.get( + "/sql/", + params={ + "metrics": ["foo.bar.num_repair_orders", "foo.bar.avg_repair_price"], + "dimensions": [ + "foo.bar.hard_hat.country", + ], + "orderby": ["default.hard_hat.city"], + "limit": 10, + }, + ) + data = response.json() + assert data["message"] == ( + "Columns ['default.hard_hat.city'] in order by " + "clause must also be specified in the metrics or dimensions" + ) + module__client_with_examples.app.dependency_overrides.clear() + + +@pytest.mark.asyncio +async def test_sql_structs(module__client_with_examples: AsyncClient): + """ + Create a transform with structs and verify that metric expressions that reference these + structs, along with grouping by dimensions that reference these structs will work when + building metrics SQL. + """ + await module__client_with_examples.post( + "/nodes/transform", + json={ + "name": "default.simple_agg", + "description": "simple agg", + "mode": "published", + "query": """SELECT + EXTRACT(YEAR FROM ro.relevant_dates.order_dt) AS order_year, + EXTRACT(MONTH FROM ro.relevant_dates.order_dt) AS order_month, + EXTRACT(DAY FROM ro.relevant_dates.order_dt) AS order_day, + SUM(DATEDIFF(ro.relevant_dates.dispatched_dt, ro.relevant_dates.order_dt)) AS dispatch_delay_sum, + COUNT(ro.repair_order_id) AS repair_orders_cnt +FROM ( + SELECT + repair_order_id, + STRUCT(required_date as required_dt, order_date as order_dt, dispatched_date as dispatched_dt) relevant_dates + FROM default.repair_orders +) AS ro +GROUP BY + EXTRACT(YEAR FROM ro.relevant_dates.order_dt), + EXTRACT(MONTH FROM ro.relevant_dates.order_dt), + EXTRACT(DAY FROM ro.relevant_dates.order_dt)""", + }, + ) + + await module__client_with_examples.post( + "/nodes/metric", + json={ + "name": "default.average_dispatch_delay", + "description": "average dispatch delay", + "mode": "published", + "query": """select SUM(D.dispatch_delay_sum)/SUM(D.repair_orders_cnt) from default.simple_agg D""", + }, + ) + dimension_attr = [ + { + "namespace": "system", + "name": "dimension", + }, + ] + for column in ["order_year", "order_month", "order_day"]: + await module__client_with_examples.post( + f"/nodes/default.simple_agg/columns/{column}/attributes/", + json=dimension_attr, + ) + sql_params = { + "metrics": ["default.average_dispatch_delay"], + "dimensions": [ + "default.simple_agg.order_year", + "default.simple_agg.order_month", + "default.simple_agg.order_day", + ], + "filters": ["default.simple_agg.order_year = 2020"], + } + + expected = """ + WITH default_DOT_simple_agg AS ( + SELECT + EXTRACT(YEAR, ro.relevant_dates.order_dt) AS order_year, + EXTRACT(MONTH, ro.relevant_dates.order_dt) AS order_month, + EXTRACT(DAY, ro.relevant_dates.order_dt) AS order_day, + SUM( + DATEDIFF( + ro.relevant_dates.dispatched_dt, + ro.relevant_dates.order_dt + ) + ) AS dispatch_delay_sum, + COUNT(ro.repair_order_id) AS repair_orders_cnt + FROM ( + SELECT + default_DOT_repair_orders.repair_order_id, + struct( + default_DOT_repair_orders.required_date AS required_dt, + default_DOT_repair_orders.order_date AS order_dt, + default_DOT_repair_orders.dispatched_date AS dispatched_dt + ) relevant_dates + FROM roads.repair_orders AS default_DOT_repair_orders + ) AS ro + WHERE EXTRACT(YEAR, ro.relevant_dates.order_dt) = 2020 + GROUP BY + EXTRACT(YEAR, ro.relevant_dates.order_dt), + EXTRACT(MONTH, ro.relevant_dates.order_dt), + EXTRACT(DAY, ro.relevant_dates.order_dt) + ), + default_DOT_simple_agg_metrics AS ( + SELECT + default_DOT_simple_agg.order_year default_DOT_simple_agg_DOT_order_year, + default_DOT_simple_agg.order_month default_DOT_simple_agg_DOT_order_month, + default_DOT_simple_agg.order_day default_DOT_simple_agg_DOT_order_day, + SUM(default_DOT_simple_agg.dispatch_delay_sum) / SUM(default_DOT_simple_agg.repair_orders_cnt) default_DOT_average_dispatch_delay + FROM default_DOT_simple_agg + GROUP BY default_DOT_simple_agg.order_year, default_DOT_simple_agg.order_month, default_DOT_simple_agg.order_day + ) + SELECT + default_DOT_simple_agg_metrics.default_DOT_simple_agg_DOT_order_year, + default_DOT_simple_agg_metrics.default_DOT_simple_agg_DOT_order_month, + default_DOT_simple_agg_metrics.default_DOT_simple_agg_DOT_order_day, + default_DOT_simple_agg_metrics.default_DOT_average_dispatch_delay + FROM default_DOT_simple_agg_metrics + """ + response = await module__client_with_examples.get("/sql", params=sql_params) + data = response.json() + assert str(parse(str(expected))) == str(parse(str(data["sql"]))) + + # Test the same query string but with `ro` as a CTE + await module__client_with_examples.patch( + "/nodes/transform", + json={ + "name": "default.simple_agg", + "query": """WITH ro as ( + SELECT + repair_order_id, + STRUCT(required_date as required_dt, order_date as order_dt, dispatched_date as dispatched_dt) relevant_dates + FROM default.repair_orders +) +SELECT + EXTRACT(YEAR FROM ro.relevant_dates.order_dt) AS order_year, + EXTRACT(MONTH FROM ro.relevant_dates.order_dt) AS order_month, + EXTRACT(DAY FROM ro.relevant_dates.order_dt) AS order_day, + SUM(DATEDIFF(ro.relevant_dates.dispatched_dt, ro.relevant_dates.order_dt)) AS dispatch_delay_sum, + COUNT(ro.repair_order_id) AS repair_orders_cnt +FROM ro +GROUP BY + EXTRACT(YEAR FROM ro.relevant_dates.order_dt), + EXTRACT(MONTH FROM ro.relevant_dates.order_dt), + EXTRACT(DAY FROM ro.relevant_dates.order_dt)""", + }, + ) + + response = await module__client_with_examples.get("/sql", params=sql_params) + data = response.json() + assert str(parse(str(expected))) == str(parse(str(data["sql"]))) + + +@pytest.mark.asyncio +async def test_filter_pushdowns( + module__client_with_examples: AsyncClient, +): + """ + Pushing down filters should use the column names and not the column aliases + """ + response = await module__client_with_examples.patch( + "/nodes/default.repair_orders_fact", + json={ + "query": "SELECT repair_orders.hard_hat_id AS hh_id " + "FROM default.repair_orders repair_orders", + }, + ) + assert response.status_code == 200 + + response = await module__client_with_examples.post( + "/nodes/default.repair_orders_fact/link", + json={ + "dimension_node": "default.hard_hat", + "join_type": "left", + "join_on": ( + "default.repair_orders_fact.hh_id = default.hard_hat.hard_hat_id" + ), + }, + ) + assert response.status_code == 201 + + response = await module__client_with_examples.get( + "/sql/default.repair_orders_fact", + params={ + "dimensions": ["default.hard_hat.hard_hat_id"], + "filters": [ + "default.hard_hat.hard_hat_id IN (123, 13) AND " + "default.hard_hat.hard_hat_id = 123 OR default.hard_hat.hard_hat_id = 13", + ], + }, + ) + assert str(parse(response.json()["sql"])) == str( + parse( + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT repair_orders.hard_hat_id AS hh_id + FROM roads.repair_orders AS repair_orders + WHERE repair_orders.hard_hat_id IN (123, 13) AND repair_orders.hard_hat_id = 123 OR repair_orders.hard_hat_id = 13 + ) + SELECT + default_DOT_repair_orders_fact.hh_id default_DOT_hard_hat_DOT_hard_hat_id + FROM default_DOT_repair_orders_fact + """, + ), + ) + + +@pytest.mark.asyncio +async def test_sql_use_materialized_table( + measures_sql_request, + module__client_with_examples: AsyncClient, +): + """ + Posting a materialized table for a dimension node should result in building SQL + that uses the materialized table whenever a dimension attribute on the node is + requested. + """ + availability_response = await module__client_with_examples.post( + "/data/default.hard_hat/availability", + json={ + "catalog": "default", + "schema_": "xyz", + "table": "hardhat", + "valid_through_ts": 20240601, + "max_temporal_partition": ["2024", "06", "01"], + "min_temporal_partition": ["2022", "01", "01"], + }, + ) + assert availability_response.status_code == 200 + response = (await measures_sql_request()).json() + assert "xyz.hardhat" in response[0]["sql"] + expected_sql = """WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id +), default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers +), default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hat.hard_hat_id, + default_DOT_hard_hat.last_name, + default_DOT_hard_hat.first_name, + default_DOT_hard_hat.title, + default_DOT_hard_hat.birth_date, + default_DOT_hard_hat.hire_date, + default_DOT_hard_hat.address, + default_DOT_hard_hat.city, + default_DOT_hard_hat.state, + default_DOT_hard_hat.postal_code, + default_DOT_hard_hat.country, + default_DOT_hard_hat.manager, + default_DOT_hard_hat.contractor_id + FROM ( + SELECT + hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id + FROM xyz.hardhat + WHERE + state = 'CA' + ) default_DOT_hard_hat + WHERE + default_DOT_hard_hat.state = 'CA' +) +SELECT + default_DOT_repair_orders_fact.repair_order_id default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact.municipality_id default_DOT_repair_orders_fact_DOT_municipality_id, + default_DOT_repair_orders_fact.hard_hat_id default_DOT_repair_orders_fact_DOT_hard_hat_id, + default_DOT_repair_orders_fact.dispatcher_id default_DOT_repair_orders_fact_DOT_dispatcher_id, + default_DOT_repair_orders_fact.order_date default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact.dispatched_date default_DOT_repair_orders_fact_DOT_dispatched_date, + default_DOT_repair_orders_fact.required_date default_DOT_repair_orders_fact_DOT_required_date, + default_DOT_repair_orders_fact.discount default_DOT_repair_orders_fact_DOT_discount, + default_DOT_repair_orders_fact.price default_DOT_repair_orders_fact_DOT_price, + default_DOT_repair_orders_fact.quantity default_DOT_repair_orders_fact_DOT_quantity, + default_DOT_repair_orders_fact.repair_type_id default_DOT_repair_orders_fact_DOT_repair_type_id, + default_DOT_repair_orders_fact.total_repair_cost default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_repair_orders_fact.time_to_dispatch default_DOT_repair_orders_fact_DOT_time_to_dispatch, + default_DOT_repair_orders_fact.dispatch_delay default_DOT_repair_orders_fact_DOT_dispatch_delay, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_hard_hat.state default_DOT_hard_hat_DOT_state +FROM default_DOT_repair_orders_fact +INNER JOIN default_DOT_dispatcher + ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id +INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + """ + assert str(parse(expected_sql)) == str(parse(response[0]["sql"])) + + +@pytest.mark.asyncio +async def test_filter_on_source_nodes( + module__client_with_examples: AsyncClient, +): + """ + Verify that filtering using dimensions that are available on a given node's upstream + source nodes works, even if these dimensions are not available on the node itself. + """ + # Create a dimension node: `default.event_date` + response = await module__client_with_examples.post( + "/nodes/dimension", + json={ + "name": "default.event_date", + "description": "", + "display_name": "Event Date", + "query": """ + SELECT + 20240101 AS dateint, + '2024-01-01' AS date + """, + "mode": "published", + "primary_key": ["dateint"], + }, + ) + assert response.status_code == 201 + + # Create a source node: `default.events` + response = await module__client_with_examples.post( + "/nodes/source", + json={ + "name": "default.events", + "description": "", + "display_name": "Events", + "catalog": "default", + "schema_": "example", + "table": "events", + "columns": [ + {"name": "event_id", "type": "int"}, + {"name": "event_date", "type": "int"}, + {"name": "user_id", "type": "int"}, + {"name": "duration_ms", "type": "int"}, + ], + "primary_key": ["event_id"], + "mode": "published", + }, + ) + assert response.status_code == 200 + + # Link `default.events` transform to the `default.event_date` dimension node on `dateint` + response = await module__client_with_examples.post( + "/nodes/default.events/link", + json={ + "dimension_node": "default.event_date", + "join_type": "left", + "join_on": "default.events.event_date = default.event_date.dateint", + }, + ) + assert response.status_code == 201 + + # Create a transform on `default.events` that aggregates it to the user level + response = await module__client_with_examples.post( + "/nodes/transform", + json={ + "name": "default.events_agg", + "description": "", + "display_name": "Events Agg", + "query": """ + SELECT + user_id, + SUM(duration_ms) AS duration_ms + FROM default.events + """, + "primary_key": ["user_id"], + "mode": "published", + }, + ) + assert response.status_code == 201 + + # Request the available dimensions for `default.events_agg` + response = await module__client_with_examples.get( + "/nodes/default.events_agg/dimensions", + ) + assert response.json() == [ + { + "name": "default.events_agg.user_id", + "node_display_name": "Events Agg", + "node_name": "default.events_agg", + "path": [], + "type": "int", + "filter_only": False, + "properties": ["primary_key"], + }, + { + "name": "default.event_date.dateint", + "node_display_name": "Event Date", + "node_name": "default.event_date", + "path": ["default.events"], + "type": "int", + "filter_only": True, + "properties": ["primary_key"], + }, + ] + + # Request SQL for default.events_agg with filters on `default.event_date` + response = await module__client_with_examples.get( + "/sql/default.events_agg", + params={ + "filters": ["default.event_date.dateint BETWEEN 20240101 AND 20240201"], + }, + ) + + # Check that the filters have propagated to the upstream nodes + assert str(parse(response.json()["sql"])) == str( + parse( + """ + WITH default_DOT_events_agg AS ( + SELECT default_DOT_events.user_id, + SUM(default_DOT_events.duration_ms) AS duration_ms + FROM ( + SELECT + event_id, + event_date, + user_id, + duration_ms + FROM example.events + WHERE event_date BETWEEN 20240101 AND 20240201 + ) default_DOT_events + ) + SELECT + default_DOT_events_agg.user_id default_DOT_events_agg_DOT_user_id, + default_DOT_events_agg.duration_ms default_DOT_events_agg_DOT_duration_ms + FROM default_DOT_events_agg + """, + ), + ) diff --git a/datajunction-server/tests/api/sql_v2_test.py b/datajunction-server/tests/api/sql_v2_test.py new file mode 100644 index 000000000..e743f2bb6 --- /dev/null +++ b/datajunction-server/tests/api/sql_v2_test.py @@ -0,0 +1,1515 @@ +"""Tests for all /sql endpoints that use node SQL build v2""" + +from unittest import mock + +import duckdb +import pytest +from httpx import AsyncClient + +from datajunction_server.sql.parsing.backends.antlr4 import parse + + +async def fix_dimension_links(module__client_with_roads: AsyncClient): + """ + Override some dimension links with inner join instead of left join. + """ + await module__client_with_roads.post( + "/nodes/default.repair_orders_fact/link", + json={ + "dimension_node": "default.hard_hat", + "join_type": "inner", + "join_on": ( + "default.repair_orders_fact.hard_hat_id = default.hard_hat.hard_hat_id" + ), + }, + ) + await module__client_with_roads.post( + "/nodes/default.repair_orders_fact/link", + json={ + "dimension_node": "default.dispatcher", + "join_type": "left", + "join_on": ( + "default.repair_orders_fact.dispatcher_id = default.dispatcher.dispatcher_id" + ), + }, + ) + await module__client_with_roads.post( + "/nodes/default.hard_hat/link", + json={ + "dimension_node": "default.us_state", + "join_type": "inner", + "join_on": ("default.hard_hat.state = default.us_state.state_short"), + }, + ) + + +@pytest.mark.parametrize( + "metrics, dimensions, filters, orderby, sql, columns, rows", + [ + # One metric with two measures + one local dimension. Both referenced measures should + # show up in the generated measures SQL + ( + ["default.total_repair_order_discounts"], + ["default.dispatcher.dispatcher_id"], + [], + [], + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * + repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = + repair_order_details.repair_order_id + ) + SELECT + default_DOT_repair_orders_fact.dispatcher_id + default_DOT_dispatcher_DOT_dispatcher_id, + default_DOT_repair_orders_fact.discount + default_DOT_repair_orders_fact_DOT_discount, + default_DOT_repair_orders_fact.price + default_DOT_repair_orders_fact_DOT_price + FROM default_DOT_repair_orders_fact + """, + [ + { + "column": "dispatcher_id", + "name": "default_DOT_dispatcher_DOT_dispatcher_id", + "node": "default.dispatcher", + "semantic_entity": "default.dispatcher.dispatcher_id", + "semantic_type": "dimension", + "type": "int", + }, + { + "column": "discount", + "name": "default_DOT_repair_orders_fact_DOT_discount", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.discount", + "semantic_type": "measure", + "type": "float", + }, + { + "column": "price", + "name": "default_DOT_repair_orders_fact_DOT_price", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price", + "semantic_type": "measure", + "type": "float", + }, + ], + [ + (3, 0.05000000074505806, 63708.0), + (1, 0.05000000074505806, 67253.0), + (2, 0.05000000074505806, 66808.0), + (1, 0.05000000074505806, 18497.0), + (2, 0.05000000074505806, 76463.0), + (2, 0.05000000074505806, 87858.0), + (2, 0.05000000074505806, 63918.0), + (3, 0.05000000074505806, 21083.0), + (2, 0.05000000074505806, 74555.0), + (3, 0.05000000074505806, 27222.0), + (1, 0.05000000074505806, 73600.0), + (3, 0.009999999776482582, 54901.0), + (1, 0.009999999776482582, 51594.0), + (2, 0.009999999776482582, 65114.0), + (3, 0.009999999776482582, 48919.0), + (3, 0.009999999776482582, 70418.0), + (3, 0.009999999776482582, 29684.0), + (1, 0.009999999776482582, 62928.0), + (3, 0.009999999776482582, 97916.0), + (1, 0.009999999776482582, 44120.0), + (3, 0.009999999776482582, 53374.0), + (1, 0.009999999776482582, 87289.0), + (1, 0.009999999776482582, 92366.0), + (2, 0.009999999776482582, 47857.0), + (2, 0.009999999776482582, 68745.0), + ], + ), + # # Two metrics with overlapping measures + one joinable dimension + ( + [ + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + ], + ["default.dispatcher.dispatcher_id"], + [], + [], + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ) + SELECT + default_DOT_repair_orders_fact.dispatcher_id + default_DOT_dispatcher_DOT_dispatcher_id, + default_DOT_repair_orders_fact.discount + default_DOT_repair_orders_fact_DOT_discount, + default_DOT_repair_orders_fact.price + default_DOT_repair_orders_fact_DOT_price + FROM default_DOT_repair_orders_fact + """, + [ + { + "column": "dispatcher_id", + "name": "default_DOT_dispatcher_DOT_dispatcher_id", + "node": "default.dispatcher", + "semantic_entity": "default.dispatcher.dispatcher_id", + "semantic_type": "dimension", + "type": "int", + }, + { + "column": "discount", + "name": "default_DOT_repair_orders_fact_DOT_discount", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.discount", + "semantic_type": "measure", + "type": "float", + }, + { + "column": "price", + "name": "default_DOT_repair_orders_fact_DOT_price", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price", + "semantic_type": "measure", + "type": "float", + }, + ], + [ + (3, 0.05000000074505806, 63708.0), + (1, 0.05000000074505806, 67253.0), + (2, 0.05000000074505806, 66808.0), + (1, 0.05000000074505806, 18497.0), + (2, 0.05000000074505806, 76463.0), + (2, 0.05000000074505806, 87858.0), + (2, 0.05000000074505806, 63918.0), + (3, 0.05000000074505806, 21083.0), + (2, 0.05000000074505806, 74555.0), + (3, 0.05000000074505806, 27222.0), + (1, 0.05000000074505806, 73600.0), + (3, 0.009999999776482582, 54901.0), + (1, 0.009999999776482582, 51594.0), + (2, 0.009999999776482582, 65114.0), + (3, 0.009999999776482582, 48919.0), + (3, 0.009999999776482582, 70418.0), + (3, 0.009999999776482582, 29684.0), + (1, 0.009999999776482582, 62928.0), + (3, 0.009999999776482582, 97916.0), + (1, 0.009999999776482582, 44120.0), + (3, 0.009999999776482582, 53374.0), + (1, 0.009999999776482582, 87289.0), + (1, 0.009999999776482582, 92366.0), + (2, 0.009999999776482582, 47857.0), + (2, 0.009999999776482582, 68745.0), + ], + ), + # Two metrics with different measures + two dimensions from different sources + ( + ["default.avg_time_to_dispatch", "default.total_repair_cost"], + [ + "default.us_state.state_name", + "default.dispatcher.company_name", + "default.hard_hat.last_name", + ], + [ + "default.us_state.state_name = 'New Jersey'", + "default.hard_hat.last_name IN ('Brian')", + ], + [], + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity + AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date + AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date + AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE default_DOT_hard_hats.last_name IN ('Brian') + ), + default_DOT_us_state AS ( + SELECT + s.state_id, + s.state_name, + s.state_abbr AS state_short, + s.state_region + FROM roads.us_states AS s + WHERE s.state_name = 'New Jersey' + ), + default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ) + SELECT + default_DOT_repair_orders_fact.total_repair_cost + default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_repair_orders_fact.time_to_dispatch + default_DOT_repair_orders_fact_DOT_time_to_dispatch, + default_DOT_us_state.state_name + default_DOT_us_state_DOT_state_name, + default_DOT_dispatcher.company_name + default_DOT_dispatcher_DOT_company_name, + default_DOT_hard_hat.last_name + default_DOT_hard_hat_DOT_last_name + FROM default_DOT_repair_orders_fact + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_us_state + ON default_DOT_hard_hat.state = default_DOT_us_state.state_short + LEFT JOIN default_DOT_dispatcher + ON default_DOT_repair_orders_fact.dispatcher_id =default_DOT_dispatcher.dispatcher_id + """, + [ + { + "column": "total_repair_cost", + "name": "default_DOT_repair_orders_fact_DOT_total_repair_cost", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.total_repair_cost", + "semantic_type": "measure", + "type": "float", + }, + { + "column": "time_to_dispatch", + "name": "default_DOT_repair_orders_fact_DOT_time_to_dispatch", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.time_to_dispatch", + "semantic_type": "measure", + "type": "timestamp", + }, + { + "column": "state_name", + "name": "default_DOT_us_state_DOT_state_name", + "node": "default.us_state", + "semantic_entity": "default.us_state.state_name", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "company_name", + "name": "default_DOT_dispatcher_DOT_company_name", + "node": "default.dispatcher", + "semantic_entity": "default.dispatcher.company_name", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "last_name", + "name": "default_DOT_hard_hat_DOT_last_name", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.last_name", + "semantic_type": "dimension", + "type": "string", + }, + ], + [ + (92366.0, 204, "New Jersey", "Pothole Pete", "Brian"), + (44120.0, 196, "New Jersey", "Pothole Pete", "Brian"), + (18497.0, 146, "New Jersey", "Pothole Pete", "Brian"), + (63708.0, 150, "New Jersey", "Federal Roads Group", "Brian"), + ], + ), + ( + ["default.avg_time_to_dispatch"], + ["default.dispatcher.company_name", "default.hard_hat.last_name"], + ["default.hard_hat.last_name IN ('Brian')"], + ["default.dispatcher.company_name"], + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_dispatcher AS ( + SELECT default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_hard_hat AS ( + SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE default_DOT_hard_hats.last_name IN ('Brian') + ) + SELECT + default_DOT_repair_orders_fact.time_to_dispatch default_DOT_repair_orders_fact_DOT_time_to_dispatch, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_hard_hat.last_name default_DOT_hard_hat_DOT_last_name + FROM default_DOT_repair_orders_fact + LEFT JOIN default_DOT_dispatcher ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + ORDER BY default_DOT_dispatcher.company_name + """, + [], + [ + (150, "Federal Roads Group", "Brian"), + (146, "Pothole Pete", "Brian"), + (196, "Pothole Pete", "Brian"), + (204, "Pothole Pete", "Brian"), + ], + ), + ( + ["default.avg_time_to_dispatch"], + ["default.dispatcher.company_name", "default.hard_hat.last_name"], + ["default.hard_hat.last_name IN ('Brian')"], + ["default.dispatcher.company_name DESC"], + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_dispatcher AS ( + SELECT default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_hard_hat AS ( + SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE default_DOT_hard_hats.last_name IN ('Brian') + ) + SELECT + default_DOT_repair_orders_fact.time_to_dispatch default_DOT_repair_orders_fact_DOT_time_to_dispatch, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_hard_hat.last_name default_DOT_hard_hat_DOT_last_name + FROM default_DOT_repair_orders_fact + LEFT JOIN default_DOT_dispatcher ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + ORDER BY default_DOT_dispatcher.company_name DESC + """, + [], + [ + (146, "Pothole Pete", "Brian"), + (196, "Pothole Pete", "Brian"), + (204, "Pothole Pete", "Brian"), + (150, "Federal Roads Group", "Brian"), + ], + ), + ], +) +@pytest.mark.asyncio +async def test_measures_sql_with_filters__v2( + metrics, + dimensions, + filters, + orderby, + sql, + columns, + rows, + module__client_with_roads: AsyncClient, + duckdb_conn: duckdb.DuckDBPyConnection, +): + """ + Test ``GET /sql/measures`` with various metrics, filters, and dimensions. + """ + await fix_dimension_links(module__client_with_roads) + sql_params = { + "metrics": metrics, + "dimensions": dimensions, + "filters": filters, + **({"orderby": orderby} if orderby else {}), + } + response = await module__client_with_roads.get( + "/sql/measures/v2", + params=sql_params, + ) + data = response.json() + translated_sql = data[0] + assert str(parse(str(sql))) == str(parse(str(translated_sql["sql"]))) + result = duckdb_conn.sql(translated_sql["sql"]) + assert set(result.fetchall()) == set(rows) + if columns: + assert translated_sql["columns"] == columns + + +@pytest.mark.parametrize( + "metrics, dimensions, filters, orderby, sql, columns, rows", + [ + # One metric with two measures + one local dimension. Both referenced measures should + # show up in the generated measures SQL + ( + ["default.total_repair_order_discounts"], + ["default.dispatcher.dispatcher_id"], + [], + [], + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_repair_orders_fact_built AS ( + SELECT + default_DOT_repair_orders_fact.dispatcher_id default_DOT_dispatcher_DOT_dispatcher_id, + default_DOT_repair_orders_fact.discount, + default_DOT_repair_orders_fact.price + FROM default_DOT_repair_orders_fact + ) + SELECT + default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_dispatcher_id, + SUM(price * discount) AS price_discount_sum_017d55a8 + FROM default_DOT_repair_orders_fact_built + GROUP BY default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_dispatcher_id + """, + [ + { + "column": "dispatcher_id", + "name": "default_DOT_dispatcher_DOT_dispatcher_id", + "node": "default.dispatcher", + "semantic_entity": "default.dispatcher.dispatcher_id", + "semantic_type": "dimension", + "type": "int", + }, + { + "column": "price_discount_sum_017d55a8", + "name": "price_discount_sum_017d55a8", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price_discount_sum_017d55a8", + "semantic_type": "measure", + "type": "double", + }, + ], + [ + (1, 11350.47006225586), + (2, 20297.260345458984), + (3, 9152.770111083984), + ], + ), + # Two metrics with overlapping measures + one joinable dimension + ( + [ + "default.total_repair_order_discounts", + "default.avg_repair_order_discounts", + ], + ["default.dispatcher.dispatcher_id"], + [], + [], + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_repair_orders_fact_built AS ( + SELECT + default_DOT_repair_orders_fact.dispatcher_id default_DOT_dispatcher_DOT_dispatcher_id, + default_DOT_repair_orders_fact.discount, + default_DOT_repair_orders_fact.price + FROM default_DOT_repair_orders_fact + ) + SELECT + default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_dispatcher_id, + SUM(price * discount) AS price_discount_sum_017d55a8, + COUNT(price * discount) AS price_discount_count_017d55a8 + FROM default_DOT_repair_orders_fact_built + GROUP BY default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_dispatcher_id + """, + [ + { + "column": "dispatcher_id", + "name": "default_DOT_dispatcher_DOT_dispatcher_id", + "node": "default.dispatcher", + "semantic_entity": "default.dispatcher.dispatcher_id", + "semantic_type": "dimension", + "type": "int", + }, + { + "column": "price_discount_sum_017d55a8", + "name": "price_discount_sum_017d55a8", + "node": mock.ANY, + "semantic_entity": mock.ANY, + "semantic_type": "measure", + "type": "double", + }, + { + "column": "price_discount_count_017d55a8", + "name": "price_discount_count_017d55a8", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.price_discount_count_017d55a8", + "semantic_type": "measure", + "type": "bigint", + }, + ], + [ + (2, 20297.260345458984, 8), + (1, 11350.47006225586, 8), + (3, 9152.770111083984, 9), + ], + ), + # Two metrics with different measures + two dimensions from different sources + ( + ["default.avg_time_to_dispatch", "default.total_repair_cost"], + [ + "default.us_state.state_name", + "default.dispatcher.company_name", + "default.hard_hat.last_name", + ], + [ + "default.us_state.state_name = 'New Jersey'", + "default.hard_hat.last_name IN ('Brian')", + ], + [], + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE default_DOT_hard_hats.last_name IN ('Brian') + ), + default_DOT_us_state AS ( + SELECT + s.state_id, + s.state_name, + s.state_abbr AS state_short, + s.state_region + FROM roads.us_states AS s + WHERE s.state_name = 'New Jersey' + ), + default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_repair_orders_fact_built AS ( + SELECT + default_DOT_repair_orders_fact.total_repair_cost, + default_DOT_repair_orders_fact.time_to_dispatch, + default_DOT_us_state.state_name default_DOT_us_state_DOT_state_name, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_hard_hat.last_name default_DOT_hard_hat_DOT_last_name + FROM default_DOT_repair_orders_fact + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_us_state + ON default_DOT_hard_hat.state = default_DOT_us_state.state_short + LEFT JOIN default_DOT_dispatcher + ON default_DOT_repair_orders_fact.dispatcher_id = + default_DOT_dispatcher.dispatcher_id + ) + SELECT + default_DOT_repair_orders_fact_built.default_DOT_us_state_DOT_state_name, + default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_last_name, + COUNT(CAST(time_to_dispatch AS INT)) AS time_to_dispatch_count_bf99afd6, + SUM(CAST(time_to_dispatch AS INT)) AS time_to_dispatch_sum_bf99afd6, + SUM(total_repair_cost) AS total_repair_cost_sum_9bdaf803 + FROM default_DOT_repair_orders_fact_built + GROUP BY + default_DOT_repair_orders_fact_built.default_DOT_us_state_DOT_state_name, + default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_last_name + """, + [ + { + "column": "state_name", + "name": "default_DOT_us_state_DOT_state_name", + "node": "default.us_state", + "semantic_entity": "default.us_state.state_name", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "company_name", + "name": "default_DOT_dispatcher_DOT_company_name", + "node": "default.dispatcher", + "semantic_entity": "default.dispatcher.company_name", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "last_name", + "name": "default_DOT_hard_hat_DOT_last_name", + "node": "default.hard_hat", + "semantic_entity": "default.hard_hat.last_name", + "semantic_type": "dimension", + "type": "string", + }, + { + "column": "time_to_dispatch_count_bf99afd6", + "name": "time_to_dispatch_count_bf99afd6", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.time_to_dispatch_count_bf99afd6", + "semantic_type": "measure", + "type": "bigint", + }, + { + "column": "time_to_dispatch_sum_bf99afd6", + "name": "time_to_dispatch_sum_bf99afd6", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.time_to_dispatch_sum_bf99afd6", + "semantic_type": "measure", + "type": "bigint", + }, + { + "column": "total_repair_cost_sum_9bdaf803", + "name": "total_repair_cost_sum_9bdaf803", + "node": "default.repair_orders_fact", + "semantic_entity": "default.repair_orders_fact.total_repair_cost_sum_9bdaf803", + "semantic_type": "measure", + "type": "double", + }, + ], + [ + ("New Jersey", "Federal Roads Group", "Brian", 1, 150, 63708.0), + ("New Jersey", "Pothole Pete", "Brian", 3, 546, 154983.0), + ], + ), + ( + ["default.avg_time_to_dispatch"], + ["default.dispatcher.company_name", "default.hard_hat.last_name"], + ["default.hard_hat.last_name IN ('Brian')"], + ["default.dispatcher.company_name"], + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE default_DOT_hard_hats.last_name IN ('Brian') + ), + default_DOT_repair_orders_fact_built AS ( + SELECT + default_DOT_repair_orders_fact.time_to_dispatch, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_hard_hat.last_name default_DOT_hard_hat_DOT_last_name + FROM default_DOT_repair_orders_fact + LEFT JOIN default_DOT_dispatcher + ON default_DOT_repair_orders_fact.dispatcher_id = + default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + ORDER BY default_DOT_dispatcher.company_name + ) + SELECT + default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_last_name, + COUNT(CAST(time_to_dispatch AS INT)) AS time_to_dispatch_count_bf99afd6, + SUM(CAST(time_to_dispatch AS INT)) AS time_to_dispatch_sum_bf99afd6 + FROM default_DOT_repair_orders_fact_built + GROUP BY + default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_last_name + """, + [], + [ + ("Federal Roads Group", "Brian", 1, 150), + ("Pothole Pete", "Brian", 3, 546), + ], + ), + ( + ["default.avg_time_to_dispatch"], + ["default.dispatcher.company_name", "default.hard_hat.last_name"], + ["default.hard_hat.last_name IN ('Brian')"], + ["default.dispatcher.company_name DESC"], + """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE default_DOT_hard_hats.last_name IN ('Brian') + ), + default_DOT_repair_orders_fact_built AS ( + SELECT + default_DOT_repair_orders_fact.time_to_dispatch, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_hard_hat.last_name default_DOT_hard_hat_DOT_last_name + FROM default_DOT_repair_orders_fact + LEFT JOIN default_DOT_dispatcher + ON default_DOT_repair_orders_fact.dispatcher_id = + default_DOT_dispatcher.dispatcher_id + INNER JOIN default_DOT_hard_hat + ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + ORDER BY default_DOT_dispatcher.company_name DESC + ) + SELECT + default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_last_name, + COUNT(CAST(time_to_dispatch AS INT)) AS time_to_dispatch_count_bf99afd6, + SUM(CAST(time_to_dispatch AS INT)) AS time_to_dispatch_sum_bf99afd6 + FROM default_DOT_repair_orders_fact_built + GROUP BY + default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_company_name, + default_DOT_repair_orders_fact_built.default_DOT_hard_hat_DOT_last_name + """, + [], + [ + ("Pothole Pete", "Brian", 3, 546), + ("Federal Roads Group", "Brian", 1, 150), + ], + ), + ], +) +@pytest.mark.asyncio +async def test_measures_sql_preaggregate( + metrics, + dimensions, + filters, + orderby, + sql, + columns, + rows, + module__client_with_roads: AsyncClient, + duckdb_conn: duckdb.DuckDBPyConnection, +): + """ + Test ``GET /sql/measures`` with various metrics, filters, and dimensions. + """ + await fix_dimension_links(module__client_with_roads) + sql_params = { + "metrics": metrics, + "dimensions": dimensions, + "filters": filters, + **({"orderby": orderby} if orderby else {}), + "preaggregate": True, + } + response = await module__client_with_roads.get( + "/sql/measures/v2", + params=sql_params, + ) + data = response.json() + translated_sql = data[0] + assert str(parse(str(sql))) == str(parse(str(translated_sql["sql"]))) + result = duckdb_conn.sql(translated_sql["sql"]) + assert set(result.fetchall()) == set(rows) + if columns: + assert translated_sql["columns"] == columns + + +@pytest.mark.asyncio +async def test_measures_sql_include_all_columns( + module__client_with_roads: AsyncClient, + duckdb_conn: duckdb.DuckDBPyConnection, +): + """ + Test ``GET /sql/measures/v2`` with include_all_columns set to true. + """ + await fix_dimension_links(module__client_with_roads) + + response = await module__client_with_roads.get( + "/sql/measures/v2", + params={ + "metrics": ["default.avg_time_to_dispatch"], + "dimensions": [ + "default.us_state.state_name", + "default.dispatcher.company_name", + "default.hard_hat.last_name", + ], + "filters": [ + "default.us_state.state_name = 'New Jersey'", + "default.hard_hat.last_name IN ('Brian')", + ], + "include_all_columns": True, + }, + ) + data = response.json() + translated_sql = data[0] + + expected_sql = """ + WITH default_DOT_repair_orders_fact AS ( + SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_hard_hat AS ( + SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + WHERE default_DOT_hard_hats.last_name IN ('Brian') + ), + default_DOT_us_state AS ( + SELECT s.state_id, + s.state_name, + s.state_abbr AS state_short, + s.state_region + FROM roads.us_states AS s + WHERE s.state_name = 'New Jersey' + ), + default_DOT_dispatcher AS ( + SELECT default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ) + SELECT default_DOT_repair_orders_fact.repair_order_id default_DOT_repair_orders_fact_DOT_repair_order_id, + default_DOT_repair_orders_fact.municipality_id default_DOT_repair_orders_fact_DOT_municipality_id, + default_DOT_repair_orders_fact.hard_hat_id default_DOT_repair_orders_fact_DOT_hard_hat_id, + default_DOT_repair_orders_fact.dispatcher_id default_DOT_repair_orders_fact_DOT_dispatcher_id, + default_DOT_repair_orders_fact.order_date default_DOT_repair_orders_fact_DOT_order_date, + default_DOT_repair_orders_fact.dispatched_date default_DOT_repair_orders_fact_DOT_dispatched_date, + default_DOT_repair_orders_fact.required_date default_DOT_repair_orders_fact_DOT_required_date, + default_DOT_repair_orders_fact.discount default_DOT_repair_orders_fact_DOT_discount, + default_DOT_repair_orders_fact.price default_DOT_repair_orders_fact_DOT_price, + default_DOT_repair_orders_fact.quantity default_DOT_repair_orders_fact_DOT_quantity, + default_DOT_repair_orders_fact.repair_type_id default_DOT_repair_orders_fact_DOT_repair_type_id, + default_DOT_repair_orders_fact.total_repair_cost default_DOT_repair_orders_fact_DOT_total_repair_cost, + default_DOT_repair_orders_fact.time_to_dispatch default_DOT_repair_orders_fact_DOT_time_to_dispatch, + default_DOT_repair_orders_fact.dispatch_delay default_DOT_repair_orders_fact_DOT_dispatch_delay, + default_DOT_us_state.state_name default_DOT_us_state_DOT_state_name, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name, + default_DOT_hard_hat.last_name default_DOT_hard_hat_DOT_last_name + FROM default_DOT_repair_orders_fact INNER JOIN default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + INNER JOIN default_DOT_us_state ON default_DOT_hard_hat.state = default_DOT_us_state.state_short + LEFT JOIN default_DOT_dispatcher ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + """ + assert str(parse(str(expected_sql))) == str(parse(str(translated_sql["sql"]))) + result = duckdb_conn.sql(translated_sql["sql"]) + assert len(result.fetchall()) == 4 + + +@pytest.mark.asyncio +async def test_measures_sql_errors( + module__client_with_roads: AsyncClient, +): + """ + Test ``GET /sql/measures/v2`` with include_all_columns set to true. + """ + await fix_dimension_links(module__client_with_roads) + + response = await module__client_with_roads.get( + "/sql/measures/v2", + params={ + "metrics": ["default.avg_time_to_dispatch"], + "dimensions": [ + "default.hard_hat.last_name", + ], + "filters": [ + "default.us_state.state_name = 'New Jersey'", + "default.hard_hat.last_name IN ('Brian')", + ], + "orderby": ["default.dispatcher.company_name"], + }, + ) + data = response.json() + assert data[0]["errors"] == [ + { + "code": 208, + "message": "['default.dispatcher.company_name'] is not a valid ORDER BY request", + "debug": { + "node_revision": "default.repair_orders_fact", + "filters": [ + "default.us_state.state_name = 'New Jersey'", + "default.hard_hat.last_name IN ('Brian')", + ], + "required_dimensions": [], + "dimensions": ["default.hard_hat.last_name"], + "orderby": ["default.dispatcher.company_name"], + "limit": None, + "ignore_errors": True, + "build_criteria": { + "timestamp": None, + "dialect": "spark", + "target_node_name": None, + }, + }, + "context": "", + }, + ] + + +@pytest.mark.asyncio +async def test_measures_sql_preagg_incompatible( + module__client_with_roads: AsyncClient, + duckdb_conn: duckdb.DuckDBPyConnection, +): + """ + Test ``GET /sql/measures`` with incompatible metrics vs compatible metrics. + """ + await fix_dimension_links(module__client_with_roads) + await module__client_with_roads.post( + "/nodes/metric", + json={ + "description": "A preagg incompatible metric", + "query": "SELECT COUNT(DISTINCT hard_hat_id) FROM default.repair_orders_fact", + "mode": "published", + "name": "default.number_of_hard_hats", + }, + ) + + response = await module__client_with_roads.get( + "/sql/measures/v2", + params={ + "metrics": ["default.avg_repair_price", "default.number_of_hard_hats"], + "dimensions": [ + "default.dispatcher.company_name", + ], + "filters": [], + "preaggregate": True, + }, + ) + data = response.json() + translated_sql = data[0] + assert translated_sql["grain"] == [] + expected_sql = """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ) + SELECT + default_DOT_repair_orders_fact.hard_hat_id default_DOT_repair_orders_fact_DOT_hard_hat_id, + default_DOT_repair_orders_fact.price default_DOT_repair_orders_fact_DOT_price, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name + FROM default_DOT_repair_orders_fact + LEFT JOIN default_DOT_dispatcher + ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + """ + assert str(parse(str(expected_sql))) == str(parse(str(translated_sql["sql"]))) + result = duckdb_conn.sql(translated_sql["sql"]) + assert set(result.fetchall()) == { + (3, 67253.0, "Pothole Pete"), + (6, 65114.0, "Asphalts R Us"), + (3, 87858.0, "Asphalts R Us"), + (1, 92366.0, "Pothole Pete"), + (1, 63708.0, "Federal Roads Group"), + (4, 73600.0, "Pothole Pete"), + (2, 48919.0, "Federal Roads Group"), + (5, 21083.0, "Federal Roads Group"), + (5, 47857.0, "Asphalts R Us"), + (4, 63918.0, "Asphalts R Us"), + (3, 74555.0, "Asphalts R Us"), + (2, 29684.0, "Federal Roads Group"), + (4, 51594.0, "Pothole Pete"), + (5, 87289.0, "Pothole Pete"), + (7, 53374.0, "Federal Roads Group"), + (8, 76463.0, "Asphalts R Us"), + (8, 54901.0, "Federal Roads Group"), + (6, 68745.0, "Asphalts R Us"), + (5, 66808.0, "Asphalts R Us"), + (4, 27222.0, "Federal Roads Group"), + (6, 62928.0, "Pothole Pete"), + (1, 18497.0, "Pothole Pete"), + (1, 44120.0, "Pothole Pete"), + (5, 97916.0, "Federal Roads Group"), + (9, 70418.0, "Federal Roads Group"), + } + + response = await module__client_with_roads.get( + "/sql/measures/v2", + params={ + "metrics": ["default.avg_repair_price", "default.num_repair_orders"], + "dimensions": [ + "default.dispatcher.company_name", + ], + "filters": [], + "preaggregate": True, + }, + ) + data = response.json() + translated_sql = data[0] + assert translated_sql["grain"] == ["default_DOT_dispatcher_DOT_company_name"] + expected_sql = """ + WITH default_DOT_repair_orders_fact AS ( + SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders + JOIN roads.repair_order_details AS repair_order_details + ON repair_orders.repair_order_id = repair_order_details.repair_order_id + ), + default_DOT_dispatcher AS ( + SELECT + default_DOT_dispatchers.dispatcher_id, + default_DOT_dispatchers.company_name, + default_DOT_dispatchers.phone + FROM roads.dispatchers AS default_DOT_dispatchers + ), + default_DOT_repair_orders_fact_built AS ( + SELECT + default_DOT_repair_orders_fact.repair_order_id, + default_DOT_repair_orders_fact.price, + default_DOT_dispatcher.company_name default_DOT_dispatcher_DOT_company_name + FROM default_DOT_repair_orders_fact LEFT JOIN default_DOT_dispatcher ON default_DOT_repair_orders_fact.dispatcher_id = default_DOT_dispatcher.dispatcher_id + ) + SELECT + default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_company_name, + COUNT(price) AS price_count_78a5eb43, + SUM(price) AS price_sum_78a5eb43, + COUNT(repair_order_id) AS repair_order_id_count_0b7dfba0 + FROM default_DOT_repair_orders_fact_built + GROUP BY default_DOT_repair_orders_fact_built.default_DOT_dispatcher_DOT_company_name + """ + assert str(parse(str(expected_sql))) == str(parse(str(translated_sql["sql"]))) + result = duckdb_conn.sql(translated_sql["sql"]) + assert set(result.fetchall()) == { + ("Pothole Pete", 8, 497647.0, 8), + ("Asphalts R Us", 8, 551318.0, 8), + ("Federal Roads Group", 9, 467225.0, 9), + } + + +@pytest.mark.asyncio +async def test_metrics_sql_different_parents( + module__client_with_roads: AsyncClient, + duckdb_conn: duckdb.DuckDBPyConnection, +): + """ + Test ``GET /sql`` for metrics from different parents. + """ + await fix_dimension_links(module__client_with_roads) + + response = await module__client_with_roads.get( + "/sql", + params={ + "metrics": [ + "default.avg_length_of_employment", + "default.total_repair_cost", + ], + "dimensions": [ + "default.hard_hat.first_name", + "default.hard_hat.last_name", + ], + "filters": [ + # "default.hard_hat.first_name like '%a%'", + ], + "orderby": ["default.hard_hat.last_name"], + "limit": 5, + }, + ) + data = response.json() + expected_sql = """WITH +default_DOT_hard_hat AS ( +SELECT default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats +), +default_DOT_repair_orders_fact AS ( +SELECT repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay + FROM roads.repair_orders AS repair_orders JOIN roads.repair_order_details AS repair_order_details ON repair_orders.repair_order_id = repair_order_details.repair_order_id +), +default_DOT_hard_hat_metrics AS ( +SELECT default_DOT_hard_hat.last_name default_DOT_hard_hat_DOT_last_name, + default_DOT_hard_hat.first_name default_DOT_hard_hat_DOT_first_name, + avg(CAST(NOW() AS DATE) - default_DOT_hard_hat.hire_date) default_DOT_avg_length_of_employment + FROM default_DOT_hard_hat + GROUP BY default_DOT_hard_hat.last_name, default_DOT_hard_hat.first_name +), +default_DOT_repair_orders_fact_metrics AS ( +SELECT default_DOT_hard_hat.first_name default_DOT_hard_hat_DOT_first_name, + default_DOT_hard_hat.last_name default_DOT_hard_hat_DOT_last_name, + sum(default_DOT_repair_orders_fact.total_repair_cost) default_DOT_total_repair_cost + FROM default_DOT_repair_orders_fact INNER JOIN default_DOT_hard_hat ON default_DOT_repair_orders_fact.hard_hat_id = default_DOT_hard_hat.hard_hat_id + GROUP BY default_DOT_hard_hat.first_name, default_DOT_hard_hat.last_name +) +SELECT default_DOT_hard_hat_metrics.default_DOT_hard_hat_DOT_last_name, + default_DOT_hard_hat_metrics.default_DOT_hard_hat_DOT_first_name, + default_DOT_hard_hat_metrics.default_DOT_avg_length_of_employment, + default_DOT_repair_orders_fact_metrics.default_DOT_total_repair_cost + FROM default_DOT_hard_hat_metrics FULL JOIN default_DOT_repair_orders_fact_metrics ON default_DOT_hard_hat_metrics.default_DOT_hard_hat_DOT_first_name = default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_first_name AND default_DOT_hard_hat_metrics.default_DOT_hard_hat_DOT_last_name = default_DOT_repair_orders_fact_metrics.default_DOT_hard_hat_DOT_last_name + ORDER BY default_DOT_hard_hat_metrics.default_DOT_hard_hat_DOT_last_name + LIMIT 5""" + assert str(parse(str(data["sql"]))) == str(parse(expected_sql)) + + response = await module__client_with_roads.get( + "/sql", + params={ + "metrics": [ + "default.avg_length_of_employment", + "default.total_repair_cost", + ], + "dimensions": [ + "default.hard_hat.first_name", + "default.hard_hat.last_name", + ], + "filters": [], + "orderby": "default.hard_hat.last_name", + "limit": 5, + }, + ) + data = response.json() + assert str(parse(str(data["sql"]))) == str(parse(expected_sql)) + + result = duckdb_conn.sql(data["sql"]) + assert result.fetchall() == [ + ("Alfred", "Clarke", mock.ANY, 196787.0), + ("Brian", "Perkins", mock.ANY, 218691.0), + ("Cathy", "Best", mock.ANY, 229666.0), + ("Donna", "Riley", mock.ANY, 320953.0), + ("Luka", "Henderson", mock.ANY, 131364.0), + ] + + +@pytest.mark.asyncio +async def test_measures_sql_local_dimensions( + module__client_with_roads: AsyncClient, + duckdb_conn: duckdb.DuckDBPyConnection, +): + """ + Test measures SQL for metrics that reference local dimensions + """ + await fix_dimension_links(module__client_with_roads) + + response = await module__client_with_roads.get( + "/sql/measures/v2", + params={ + "metrics": [ + "default.avg_length_of_employment", + ], + "dimensions": [ + "default.hard_hat.hire_date", + ], + "filters": [], + "preaggregate": True, + }, + ) + data = response.json() + expected_sql = """ + WITH default_DOT_hard_hat AS ( + SELECT + default_DOT_hard_hats.hard_hat_id, + default_DOT_hard_hats.last_name, + default_DOT_hard_hats.first_name, + default_DOT_hard_hats.title, + default_DOT_hard_hats.birth_date, + default_DOT_hard_hats.hire_date, + default_DOT_hard_hats.address, + default_DOT_hard_hats.city, + default_DOT_hard_hats.state, + default_DOT_hard_hats.postal_code, + default_DOT_hard_hats.country, + default_DOT_hard_hats.manager, + default_DOT_hard_hats.contractor_id + FROM roads.hard_hats AS default_DOT_hard_hats + ), + default_DOT_hard_hat_built AS ( + SELECT + default_DOT_hard_hat.hire_date default_DOT_hard_hat_DOT_hire_date + FROM default_DOT_hard_hat + ) + SELECT + default_DOT_hard_hat_built.default_DOT_hard_hat_DOT_hire_date, + COUNT(CAST(NOW() AS DATE) - default_DOT_hard_hat_DOT_hire_date) AS hire_date_count_9b06ca5d, + SUM(CAST(NOW() AS DATE) - default_DOT_hard_hat_DOT_hire_date) AS hire_date_sum_9b06ca5d + FROM default_DOT_hard_hat_built + GROUP BY + default_DOT_hard_hat_built.default_DOT_hard_hat_DOT_hire_date + """ + assert str(parse(str(expected_sql))) == str(parse(str(data[0]["sql"]))) + duckdb_conn.sql(data[0]["sql"]) diff --git a/datajunction-server/tests/api/tags_test.py b/datajunction-server/tests/api/tags_test.py new file mode 100644 index 000000000..3400a6f58 --- /dev/null +++ b/datajunction-server/tests/api/tags_test.py @@ -0,0 +1,387 @@ +""" +Tests for tags. +""" + +from unittest import mock + +import pytest +import pytest_asyncio +from httpx import AsyncClient +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database.base import Base + + +class TestTags: + """ + Test tags API endpoints. + """ + + @pytest_asyncio.fixture(autouse=True) + async def cleanup_tables(self, module__session: AsyncSession): + """ + Fixture to clean up tables after each test is run + """ + yield # Testing happens + # Teardown: remove any data from database (even data not created by this session) + for table in Base.metadata.tables.keys(): + await module__session.execute(text(f'TRUNCATE TABLE "{table}" CASCADE')) + await module__session.commit() + + async def create_tag(self, module__client: AsyncClient): + """ + Creates a tag. + """ + response = await module__client.post( + "/tags/", + json={ + "name": "sales_report", + "display_name": "Sales Report", + "description": "All metrics for sales", + "tag_type": "group", + "tag_metadata": {}, + }, + ) + return response + + async def create_another_tag(self, module__client: AsyncClient): + """ + Creates another tag + """ + response = await module__client.post( + "/tags/", + json={ + "name": "reports", + "display_name": "Reports", + "description": "Sales metrics", + "tag_type": "group", + "tag_metadata": {}, + }, + ) + return response + + @pytest.mark.asyncio + async def test_create_and_read_tag(self, module__client: AsyncClient) -> None: + """ + Test ``POST /tags`` and ``GET /tags/{name}`` + """ + response = await self.create_tag(module__client) + expected_tag_output = { + "tag_metadata": {}, + "display_name": "Sales Report", + "description": "All metrics for sales", + "name": "sales_report", + "tag_type": "group", + } + assert response.status_code == 201 + assert response.json() == expected_tag_output + + response = await module__client.post( + "/tags/", + json={ + "name": "sales_report2", + "display_name": "Sales Report2", + "tag_type": "group", + }, + ) + assert response.status_code == 201 + expected_tag_output2 = { + "tag_metadata": {}, + "display_name": "Sales Report2", + "description": None, + "name": "sales_report2", + "tag_type": "group", + } + assert response.json() == expected_tag_output2 + + response = await module__client.get("/tags/sales_report/") + assert response.status_code == 200 + assert response.json() == expected_tag_output + + response = await module__client.get("/tags/sales_report2/") + assert response.status_code == 200 + assert response.json() == expected_tag_output2 + + # Check history + response = await module__client.get("/history/tag/sales_report/") + assert response.json() == [ + { + "activity_type": "create", + "node": None, + "created_at": mock.ANY, + "details": {}, + "entity_name": "sales_report", + "entity_type": "tag", + "id": mock.ANY, + "post": {}, + "pre": {}, + "user": "dj", + }, + ] + + # Creating it again should raise an exception + response = await self.create_tag(module__client) + response_data = response.json() + assert ( + response_data["message"] == "A tag with name `sales_report` already exists!" + ) + + @pytest.mark.asyncio + async def test_update_tag(self, module__client: AsyncClient) -> None: + """ + Tests updating a tag. + """ + response = await self.create_tag(module__client) + assert response.status_code == 201 + + # Trying updating the tag + response = await module__client.patch( + "/tags/sales_report/", + json={ + "description": "Helpful sales metrics", + "tag_metadata": {"order": 1}, + "display_name": "Sales Metrics", + }, + ) + assert response.status_code == 200 + response_data = response.json() + assert response_data == { + "tag_metadata": {"order": 1}, + "display_name": "Sales Metrics", + "description": "Helpful sales metrics", + "name": "sales_report", + "tag_type": "group", + } + + # Trying updating the tag + response = await module__client.patch( + "/tags/sales_report/", + json={}, + ) + assert response.json() == { + "tag_metadata": {"order": 1}, + "display_name": "Sales Metrics", + "description": "Helpful sales metrics", + "name": "sales_report", + "tag_type": "group", + } + + # Check history + response = await module__client.get("/history/tag/sales_report/") + history = response.json() + assert [ + (activity["activity_type"], activity["entity_type"]) for activity in history + ] == [("update", "tag"), ("update", "tag"), ("create", "tag")] + + @pytest.mark.asyncio + async def test_list_tags(self, module__client: AsyncClient) -> None: + """ + Test ``GET /tags`` + """ + response = await self.create_tag(module__client) + assert response.status_code == 201 + + response = await module__client.get("/tags/") + assert response.status_code == 200 + response_data = response.json() + + assert response_data == [ + { + "description": "All metrics for sales", + "display_name": "Sales Report", + "name": "sales_report", + "tag_metadata": {}, + "tag_type": "group", + }, + ] + + await module__client.post( + "/tags/", + json={ + "name": "impressions_report", + "display_name": "Impressions Report", + "description": "Metrics for various types of impressions", + "tag_type": "group", + "tag_metadata": {}, + }, + ) + + await module__client.post( + "/tags/", + json={ + "name": "rotors", + "display_name": "Rotors", + "description": "Department of brakes", + "tag_type": "business_area", + "tag_metadata": {}, + }, + ) + + response = await module__client.get("/tags/?tag_type=group") + assert response.status_code == 200 + response_data = response.json() + assert response_data == [ + { + "description": "All metrics for sales", + "tag_metadata": {}, + "name": "sales_report", + "display_name": "Sales Report", + "tag_type": "group", + }, + { + "description": "Metrics for various types of impressions", + "tag_metadata": {}, + "name": "impressions_report", + "display_name": "Impressions Report", + "tag_type": "group", + }, + ] + + response = await module__client.get("/tags/?tag_type=business_area") + assert response.status_code == 200 + response_data = response.json() + assert response_data == [ + { + "name": "rotors", + "display_name": "Rotors", + "description": "Department of brakes", + "tag_type": "business_area", + "tag_metadata": {}, + }, + ] + + @pytest.mark.asyncio + async def test_add_tag_to_node(self, client_with_dbt: AsyncClient) -> None: + """ + Test ``POST /tags`` and ``GET /tags/{name}`` + """ + response = await self.create_tag(client_with_dbt) + assert response.status_code == 201 + await self.create_another_tag(client_with_dbt) + + # Trying tag a node with a nonexistent tag should fail + response = await client_with_dbt.post( + "/nodes/default.items_sold_count/tags?tag_names=random_tag", + ) + assert response.status_code == 404 + response_data = response.json() + assert response_data["message"] == "Tags not found: random_tag" + + # Trying tag a node with an existing tag should succeed + response = await client_with_dbt.post( + "/nodes/default.items_sold_count/tags/?tag_names=sales_report", + ) + assert response.status_code == 200 + response_data = response.json() + assert response_data["message"] == ( + "Node `default.items_sold_count` has been successfully " + "updated with the following tags: sales_report" + ) + + # Test finding all nodes for that tag + response = await client_with_dbt.get( + "/tags/sales_report/nodes/", + ) + assert response.status_code == 200 + response_data = response.json() + assert len(response_data) == 1 + assert response_data == [ + { + "display_name": "Items Sold Count", + "mode": "published", + "name": "default.items_sold_count", + "description": "Total units sold", + "edited_by": None, + "tags": None, + "status": "valid", + "type": "metric", + "updated_at": mock.ANY, + "version": "v1.0", + }, + ] + + # Tag a second node + response = await client_with_dbt.post( + "/nodes/default.total_profit/tags/?tag_names=sales_report&tag_names=reports", + ) + assert response.status_code == 200 + response_data = response.json() + assert ( + response_data["message"] + == "Node `default.total_profit` has been successfully " + "updated with the following tags: sales_report, reports" + ) + + # Check history + response = await client_with_dbt.get("/history?node=default.total_profit") + history = response.json() + assert [ + (activity["activity_type"], activity["entity_type"], activity["details"]) + for activity in history + ] == [ + ("tag", "node", {"tags": ["sales_report", "reports"]}), + ("create", "node", {}), + ] + + # Check finding nodes for tag + response = await client_with_dbt.get( + "/tags/sales_report/nodes/", + ) + assert response.status_code == 200 + response_data = response.json() + assert len(response_data) == 2 + assert response_data == [ + { + "display_name": "Items Sold Count", + "mode": "published", + "name": "default.items_sold_count", + "description": "Total units sold", + "status": "valid", + "edited_by": None, + "tags": None, + "type": "metric", + "updated_at": mock.ANY, + "version": "v1.0", + }, + { + "display_name": "Total Profit", + "mode": "published", + "name": "default.total_profit", + "description": "Total profit", + "status": "valid", + "edited_by": None, + "tags": None, + "type": "metric", + "updated_at": mock.ANY, + "version": "v1.0", + }, + ] + + # Check getting nodes for tag after deactivating a node + await client_with_dbt.delete("/nodes/default.total_profit") + response = await client_with_dbt.get( + "/tags/sales_report/nodes/", + ) + assert response.status_code == 200 + response_data = response.json() + assert len(response_data) == 1 + assert response_data[0]["name"] == "default.items_sold_count" + + # Check finding nodes for tag + response = await client_with_dbt.get( + "/tags/random_tag/nodes/", + ) + assert response.status_code == 404 + response_data = response.json() + assert ( + response_data["message"] == "A tag with name `random_tag` does not exist." + ) + + # Check finding nodes for tag + response = await client_with_dbt.get( + "/tags/sales_report/nodes/?node_type=transform", + ) + assert response.status_code == 200 + response_data = response.json() + assert response_data == [] diff --git a/datajunction-server/tests/api/users_test.py b/datajunction-server/tests/api/users_test.py new file mode 100644 index 000000000..763563ece --- /dev/null +++ b/datajunction-server/tests/api/users_test.py @@ -0,0 +1,73 @@ +""" +Tests for users API endpoints +""" + +import pytest +from httpx import AsyncClient + + +class TestUsers: + """ + Test users API endpoints. + """ + + @pytest.mark.asyncio + async def test_get_users(self, module__client_with_roads: AsyncClient) -> None: + """ + Test ``GET /users`` + """ + + response = await module__client_with_roads.get("/users?with_activity=true") + assert response.json() == [{"username": "dj", "count": 69}] + + response = await module__client_with_roads.get("/users") + assert response.json() == ["dj"] + + @pytest.mark.asyncio + async def test_list_nodes_by_user( + self, + module__client_with_roads: AsyncClient, + ) -> None: + """ + Test ``GET /users/{username}`` + """ + + response = await module__client_with_roads.get("/users/dj") + assert {(node["name"], node["type"]) for node in response.json()} == { + ("default.repair_orders", "source"), + ("default.repair_orders_view", "source"), + ("default.repair_order_details", "source"), + ("default.repair_type", "source"), + ("default.contractors", "source"), + ("default.municipality_municipality_type", "source"), + ("default.municipality_type", "source"), + ("default.municipality", "source"), + ("default.dispatchers", "source"), + ("default.hard_hats", "source"), + ("default.hard_hat_state", "source"), + ("default.us_states", "source"), + ("default.us_region", "source"), + ("default.repair_order", "dimension"), + ("default.contractor", "dimension"), + ("default.hard_hat", "dimension"), + ("default.hard_hat_2", "dimension"), + ("default.hard_hat_to_delete", "dimension"), + ("default.local_hard_hats", "dimension"), + ("default.local_hard_hats_1", "dimension"), + ("default.local_hard_hats_2", "dimension"), + ("default.us_state", "dimension"), + ("default.dispatcher", "dimension"), + ("default.municipality_dim", "dimension"), + ("default.regional_level_agg", "transform"), + ("default.national_level_agg", "transform"), + ("default.repair_orders_fact", "transform"), + ("default.regional_repair_efficiency", "metric"), + ("default.num_repair_orders", "metric"), + ("default.avg_repair_price", "metric"), + ("default.total_repair_cost", "metric"), + ("default.avg_length_of_employment", "metric"), + ("default.discounted_orders_rate", "metric"), + ("default.total_repair_order_discounts", "metric"), + ("default.avg_repair_order_discounts", "metric"), + ("default.avg_time_to_dispatch", "metric"), + } diff --git a/datajunction-server/tests/conftest.py b/datajunction-server/tests/conftest.py new file mode 100644 index 000000000..0bf3e240a --- /dev/null +++ b/datajunction-server/tests/conftest.py @@ -0,0 +1,1119 @@ +""" +Fixtures for testing. +""" + +import asyncio +import os +import re +from http.client import HTTPException +from typing import ( + Any, + AsyncGenerator, + Callable, + Collection, + Coroutine, + Dict, + Generator, + Iterator, + List, + Optional, +) +from unittest.mock import MagicMock, patch + +import duckdb +import httpx +import pytest +import pytest_asyncio +from cachelib.simple import SimpleCache +from fastapi import Request +from fastapi_cache import FastAPICache +from fastapi_cache.backends.inmemory import InMemoryBackend +from httpx import AsyncClient +from pytest_mock import MockerFixture +from sqlalchemy import StaticPool, insert +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from testcontainers.core.waiting_utils import wait_for_logs +from testcontainers.postgres import PostgresContainer + +from datajunction_server.api.main import app +from datajunction_server.config import Settings +from datajunction_server.database.base import Base +from datajunction_server.database.column import Column +from datajunction_server.database.engine import Engine +from datajunction_server.database.user import User +from datajunction_server.errors import DJQueryServiceClientEntityNotFound +from datajunction_server.internal.access.authorization import validate_access +from datajunction_server.models.access import AccessControl, ValidateAccessFn +from datajunction_server.models.materialization import MaterializationInfo +from datajunction_server.models.query import QueryCreate, QueryWithResults +from datajunction_server.models.user import OAuthProvider +from datajunction_server.service_clients import QueryServiceClient +from datajunction_server.typing import QueryState +from datajunction_server.utils import ( + get_query_service_client, + get_session, + get_settings, +) + +from .examples import COLUMN_MAPPINGS, EXAMPLES, QUERY_DATA_MAPPINGS, SERVICE_SETUP + + +EXAMPLE_TOKEN = ( + "eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..SxGbG0NRepMY4z9-2-ZZdg.ug" + "0FvJUoybiGGpUItL4VbM1O_oinX7dMBUM1V3OYjv30fddn9m9UrrXxv3ERIyKu2zVJ" + "xx1gSoM5k8petUHCjatFQqA-iqnvjloFKEuAmxLdCHKUDgfKzCIYtbkDcxtzXLuqlj" + "B0-ConD6tpjMjFxNrp2KD4vwaS0oGsDJGqXlMo0MOhe9lHMLraXzOQ6xDgDFHiFert" + "Fc0T_9jYkcpmVDPl9pgPf55R.sKF18rttq1OZ_EjZqw8Www" +) + + +@pytest.fixture(autouse=True) +def _init_cache() -> Generator[Any, Any, None]: + """ + Initialize FastAPI caching + """ + FastAPICache.init(InMemoryBackend()) + yield + FastAPICache.reset() + + +@pytest_asyncio.fixture +def settings( + mocker: MockerFixture, + postgres_container: PostgresContainer, +) -> Iterator[Settings]: + """ + Custom settings for unit tests. + """ + settings = Settings( + index=postgres_container.get_connection_url(), + repository="/path/to/repository", + results_backend=SimpleCache(default_timeout=0), + celery_broker=None, + redis_cache=None, + query_service="query_service:8001", + secret="a-fake-secretkey", + ) + + mocker.patch( + "datajunction_server.utils.get_settings", + return_value=settings, + ) + + yield settings + + +@pytest_asyncio.fixture +def settings_no_qs( + mocker: MockerFixture, + postgres_container: PostgresContainer, +) -> Iterator[Settings]: + """ + Custom settings for unit tests. + """ + settings = Settings( + index=postgres_container.get_connection_url(), + repository="/path/to/repository", + results_backend=SimpleCache(default_timeout=0), + celery_broker=None, + redis_cache=None, + query_service=None, + secret="a-fake-secretkey", + ) + + mocker.patch( + "datajunction_server.utils.get_settings", + return_value=settings, + ) + + yield settings + + +@pytest.fixture(scope="session") +def duckdb_conn() -> duckdb.DuckDBPyConnection: + """ + DuckDB connection fixture with mock roads data loaded + """ + with open( + os.path.join(os.path.dirname(__file__), "duckdb.sql"), + ) as mock_data: + with duckdb.connect( + ":memory:", + ) as conn: + conn.execute(mock_data.read()) + yield conn + + +@pytest.fixture(scope="session") +def postgres_container() -> PostgresContainer: + """ + Setup postgres container + """ + postgres = PostgresContainer( + image="postgres:latest", + username="dj", + password="dj", + dbname="dj", + port=5432, + driver="psycopg", + ) + with postgres: + wait_for_logs( + postgres, + r"UTC \[1\] LOG: database system is ready to accept connections", + 10, + ) + yield postgres + + +@pytest_asyncio.fixture +async def session( + postgres_container: PostgresContainer, +) -> AsyncGenerator[AsyncSession, None]: + """ + Create a Postgres session to test models. + """ + engine = create_async_engine( + url=postgres_container.get_connection_url(), + poolclass=StaticPool, + ) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + async_session_factory = async_sessionmaker( + bind=engine, + autocommit=False, + expire_on_commit=False, + ) + async with async_session_factory() as session: + yield session + + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) + + # for AsyncEngine created in function scope, close and + # clean-up pooled connections + await engine.dispose() + + +@pytest.fixture(scope="module") +def event_loop(): + """ + This fixture is OK because we are pinning the pytest_asyncio to 0.21.x. + When they fix https://github.com/pytest-dev/pytest-asyncio/issues/718 + we can remove the pytest_asyncio pin and remove this fixture. + """ + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + yield loop + loop.close() + + +@pytest.fixture +def query_service_client( + mocker: MockerFixture, + duckdb_conn: duckdb.DuckDBPyConnection, +) -> Iterator[QueryServiceClient]: + """ + Custom settings for unit tests. + """ + qs_client = QueryServiceClient(uri="query_service:8001") + + def mock_get_columns_for_table( + catalog: str, + schema: str, + table: str, + engine: Optional[Engine] = None, + request_headers: Optional[Dict[str, str]] = None, + ) -> List[Column]: + return COLUMN_MAPPINGS[f"{catalog}.{schema}.{table}"] + + mocker.patch.object( + qs_client, + "get_columns_for_table", + mock_get_columns_for_table, + ) + + def mock_submit_query( + query_create: QueryCreate, + request_headers: Optional[Dict[str, str]] = None, + ) -> QueryWithResults: + result = duckdb_conn.sql(query_create.submitted_query) + columns = [ + {"name": col, "type": str(type_).lower()} + for col, type_ in zip(result.columns, result.types) + ] + return QueryWithResults( + id="bd98d6be-e2d2-413e-94c7-96d9411ddee2", + submitted_query=query_create.submitted_query, + state=QueryState.FINISHED, + results=[ + { + "columns": columns, + "rows": result.fetchall(), + "sql": query_create.submitted_query, + }, + ], + errors=[], + ) + + mocker.patch.object( + qs_client, + "submit_query", + mock_submit_query, + ) + + def mock_create_view( + view_name: str, + query_create: QueryCreate, + request_headers: Optional[Dict[str, str]] = None, + ) -> str: + duckdb_conn.sql(query_create.submitted_query) + return f"View {view_name} created successfully." + + mocker.patch.object( + qs_client, + "create_view", + mock_create_view, + ) + + def mock_get_query( + query_id: str, + request_headers: Optional[Dict[str, str]] = None, + ) -> Collection[Collection[str]]: + if query_id == "foo-bar-baz": + raise DJQueryServiceClientEntityNotFound("Query foo-bar-baz not found.") + for _, response in QUERY_DATA_MAPPINGS.items(): + if response.id == query_id: + return response + raise RuntimeError(f"No mocked query exists for id {query_id}") + + mocker.patch.object( + qs_client, + "get_query", + mock_get_query, + ) + + mock_materialize = MagicMock() + mock_materialize.return_value = MaterializationInfo( + urls=["http://fake.url/job"], + output_tables=["common.a", "common.b"], + ) + mocker.patch.object( + qs_client, + "materialize", + mock_materialize, + ) + + mock_deactivate_materialization = MagicMock() + mock_deactivate_materialization.return_value = MaterializationInfo( + urls=["http://fake.url/job"], + output_tables=[], + ) + mocker.patch.object( + qs_client, + "deactivate_materialization", + mock_deactivate_materialization, + ) + + mock_get_materialization_info = MagicMock() + mock_get_materialization_info.return_value = MaterializationInfo( + urls=["http://fake.url/job"], + output_tables=["common.a", "common.b"], + ) + mocker.patch.object( + qs_client, + "get_materialization_info", + mock_get_materialization_info, + ) + + mock_run_backfill = MagicMock() + mock_run_backfill.return_value = MaterializationInfo( + urls=["http://fake.url/job"], + output_tables=[], + ) + mocker.patch.object( + qs_client, + "run_backfill", + mock_run_backfill, + ) + yield qs_client + + +@pytest_asyncio.fixture +async def client( + session: AsyncSession, + settings_no_qs: Settings, +) -> AsyncGenerator[AsyncClient, None]: + """ + Create a client for testing APIs. + """ + + def get_session_override() -> AsyncSession: + return session + + def get_settings_override() -> Settings: + return settings_no_qs + + def default_validate_access() -> ValidateAccessFn: + def _(access_control: AccessControl): + access_control.approve_all() + + return _ + + app.dependency_overrides[get_session] = get_session_override + app.dependency_overrides[get_settings] = get_settings_override + app.dependency_overrides[validate_access] = default_validate_access + + async with AsyncClient( + transport=httpx.ASGITransport(app=app), + base_url="http://test", + ) as test_client: + test_client.headers.update( + { + "Authorization": f"Bearer {EXAMPLE_TOKEN}", + }, + ) + test_client.app = app + yield test_client + + app.dependency_overrides.clear() + + +async def post_and_raise_if_error(client: AsyncClient, endpoint: str, json: dict): + """ + Post the payload to the client and raise if there's an error + """ + response = await client.post(endpoint, json=json) + if response.status_code not in (200, 201): + raise HTTPException(response.text) + + +async def post_and_dont_raise_if_error(client: AsyncClient, endpoint: str, json: dict): + """ + Post the payload to the client and don't raise if there's an error + """ + await client.post(endpoint, json=json) + + +async def load_examples_in_client( + client: AsyncClient, + examples_to_load: Optional[List[str]] = None, +): + """ + Load the DJ client with examples + """ + # Basic service setup always has to be done (i.e., create catalogs, engines, namespaces etc) + for endpoint, json in SERVICE_SETUP: + await post_and_dont_raise_if_error( + client=client, + endpoint="http://test" + endpoint, + json=json, # type: ignore + ) + + # Load only the selected examples if any are specified + if examples_to_load is not None: + for example_name in examples_to_load: + for endpoint, json in EXAMPLES[example_name]: # type: ignore + await post_and_raise_if_error( + client=client, + endpoint=endpoint, + json=json, # type: ignore + ) + return client + + # Load all examples if none are specified + for example_name, examples in EXAMPLES.items(): + for endpoint, json in examples: # type: ignore + await post_and_raise_if_error( + client=client, + endpoint=endpoint, + json=json, # type: ignore + ) + return client + + +@pytest_asyncio.fixture +async def client_example_loader( + client: AsyncClient, +) -> Callable[[list[str] | None], Coroutine[Any, Any, AsyncClient]]: + """ + Provides a callable fixture for loading examples into a DJ client. + """ + + async def _load_examples(examples_to_load: Optional[List[str]] = None): + return await load_examples_in_client(client, examples_to_load) + + return _load_examples + + +@pytest_asyncio.fixture +async def client_with_examples( + client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with all examples + """ + return await client_example_loader(None) + + +@pytest_asyncio.fixture +async def client_with_service_setup( + client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with just the service setup + """ + return await client_example_loader([]) + + +@pytest_asyncio.fixture +async def client_with_roads( + client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with roads examples + """ + return await client_example_loader(["ROADS"]) + + +@pytest_asyncio.fixture +async def client_with_namespaced_roads( + client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with namespaced roads examples + """ + return await client_example_loader(["NAMESPACED_ROADS"]) + + +@pytest_asyncio.fixture +async def client_with_basic( + client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with basic examples + """ + return await client_example_loader(["BASIC"]) + + +@pytest_asyncio.fixture +async def client_with_account_revenue( + client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with account revenue examples + """ + return await client_example_loader(["ACCOUNT_REVENUE"]) + + +@pytest_asyncio.fixture +async def client_with_event( + client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with event examples + """ + return await client_example_loader(["EVENT"]) + + +@pytest_asyncio.fixture +async def client_with_dbt( + client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with dbt examples + """ + return await client_example_loader(["DBT"]) + + +def compare_parse_trees(tree1, tree2): + """ + Recursively compare two ANTLR parse trees for equality. + """ + # Check if the node types are the same + if type(tree1) is not type(tree2): + return False + + # Check if the node texts are the same + if tree1.getText() is not tree2.getText(): + return False + + # Check if the number of child nodes is the same + if tree1.getChildCount() != tree2.getChildCount(): + return False + + # Recursively compare child nodes + for i in range(tree1.getChildCount()): + child1 = tree1.getChild(i) + child2 = tree2.getChild(i) + if not compare_parse_trees(child1, child2): + return False + + # If all checks passed, the trees are equal + return True + + +COMMENT = re.compile(r"(--.*)|(/\*[\s\S]*?\*/)") +TRAILING_ZEROES = re.compile(r"(\d+\.\d*?[1-9])0+|\b(\d+)\.0+\b") +DIFF_IGNORE = re.compile(r"[\';\s]+") + + +def compare_query_strings(str1, str2): + """ + Recursively compare two ANTLR parse trees for equality, ignoring certain elements. + """ + + str1 = DIFF_IGNORE.sub("", TRAILING_ZEROES.sub("", COMMENT.sub("", str1))).upper() + str2 = DIFF_IGNORE.sub("", TRAILING_ZEROES.sub("", COMMENT.sub("", str2))).upper() + + return str1 == str2 + + +@pytest.fixture +def compare_query_strings_fixture(): + """ + Fixture for comparing two query strings. + """ + return compare_query_strings + + +@pytest_asyncio.fixture +async def client_with_query_service_example_loader( + session: AsyncSession, + settings: Settings, + query_service_client: QueryServiceClient, + mocker: MockerFixture, +) -> Callable[[Optional[List[str]]], AsyncClient]: + """ + Provides a callable fixture for loading examples into a test client + fixture that additionally has a mocked query service. + """ + + def get_query_service_client_override( + request: Request = None, + ) -> QueryServiceClient: + return query_service_client + + def get_session_override() -> AsyncSession: + return session + + def get_settings_override() -> Settings: + return settings + + app.dependency_overrides[get_session] = get_session_override + app.dependency_overrides[get_settings] = get_settings_override + app.dependency_overrides[get_query_service_client] = ( + get_query_service_client_override + ) + + # The test client includes a signed and encrypted JWT in the authorization headers. + # Even though the user is mocked to always return a "dj" user, this allows for the + # JWT logic to be tested on all requests. + client = AsyncClient( + transport=httpx.ASGITransport(app=app), + base_url="http://test", + ) + client.headers.update( + { + "Authorization": f"Bearer {EXAMPLE_TOKEN}", + }, + ) + mocker.patch( + "datajunction_server.api.materializations.get_query_service_client", + get_query_service_client_override, + ) + + def _load_examples(examples_to_load: Optional[List[str]] = None): + return load_examples_in_client(client, examples_to_load) + + return _load_examples + + +@pytest_asyncio.fixture +async def client_with_query_service( + client_with_query_service_example_loader: Callable[ + [Optional[List[str]]], + AsyncClient, + ], +) -> AsyncClient: + """ + Client with query service and all examples loaded. + """ + return await client_with_query_service_example_loader(None) + + +def pytest_addoption(parser): + """ + Add flags that enable groups of tests + """ + parser.addoption( + "--tpcds", + action="store_true", + dest="tpcds", + default=False, + help="include tests for parsing TPC-DS queries", + ) + + parser.addoption( + "--auth", + action="store_true", + dest="auth", + default=False, + help="Run authentication tests", + ) + + +# +# Module scope fixtures +# +@pytest_asyncio.fixture(autouse=True, scope="module") +async def mock_user_dj(): + """ + Mock a DJ user for tests + """ + with patch( + "datajunction_server.internal.access.authentication.http.get_user", + return_value=User( + id=1, + username="dj", + oauth_provider=OAuthProvider.BASIC, + is_admin=False, + ), + ): + yield + + +@pytest_asyncio.fixture(scope="module") +async def module__client_example_loader( + module__client: AsyncClient, +) -> Callable[[list[str] | None], Coroutine[Any, Any, AsyncClient]]: + """ + Provides a callable fixture for loading examples into a DJ client. + """ + + async def _load_examples(examples_to_load: Optional[List[str]] = None): + return await load_examples_in_client(module__client, examples_to_load) + + return _load_examples + + +@pytest_asyncio.fixture(scope="module") +async def module__client( + module__session: AsyncSession, + module__settings: Settings, + module__query_service_client: QueryServiceClient, + module_mocker: MockerFixture, +) -> AsyncGenerator[AsyncClient, None]: + """ + Create a client for testing APIs. + """ + statement = insert(User).values( + username="dj", + email=None, + name=None, + oauth_provider="basic", + is_admin=False, + ) + await module__session.execute(statement) + + def get_query_service_client_override( + request: Request = None, + ) -> QueryServiceClient: + return module__query_service_client + + def get_session_override() -> AsyncSession: + return module__session + + def get_settings_override() -> Settings: + return module__settings + + def default_validate_access() -> ValidateAccessFn: + def _(access_control: AccessControl): + access_control.approve_all() + + return _ + + module_mocker.patch( + "datajunction_server.api.materializations.get_query_service_client", + get_query_service_client_override, + ) + + app.dependency_overrides[get_session] = get_session_override + app.dependency_overrides[get_settings] = get_settings_override + app.dependency_overrides[validate_access] = default_validate_access + app.dependency_overrides[get_query_service_client] = ( + get_query_service_client_override + ) + + async with AsyncClient( + transport=httpx.ASGITransport(app=app), + base_url="http://test", + ) as test_client: + test_client.headers.update( + { + "Authorization": f"Bearer {EXAMPLE_TOKEN}", + }, + ) + test_client.app = app + yield test_client + + app.dependency_overrides.clear() + + +@pytest_asyncio.fixture(scope="module") +async def module__session( + module__postgres_container: PostgresContainer, +) -> AsyncGenerator[AsyncSession, None]: + """ + Create a Postgres session to test models. + """ + engine = create_async_engine( + url=module__postgres_container.get_connection_url(), + poolclass=StaticPool, + ) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + async_session_factory = async_sessionmaker( + bind=engine, + autocommit=False, + expire_on_commit=False, + ) + async with async_session_factory() as session: + yield session + + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) + + # for AsyncEngine created in function scope, close and + # clean-up pooled connections + await engine.dispose() + + +@pytest_asyncio.fixture(scope="module") +def module__settings( + module_mocker: MockerFixture, + module__postgres_container: PostgresContainer, +) -> Iterator[Settings]: + """ + Custom settings for unit tests. + """ + settings = Settings( + index=module__postgres_container.get_connection_url(), + repository="/path/to/repository", + results_backend=SimpleCache(default_timeout=0), + celery_broker=None, + redis_cache=None, + query_service=None, + secret="a-fake-secretkey", + ) + + module_mocker.patch( + "datajunction_server.utils.get_settings", + return_value=settings, + ) + + yield settings + + +@pytest_asyncio.fixture(scope="module") +async def module__client_with_dimension_link( + module__client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with dbt examples + """ + return await module__client_example_loader(["DIMENSION_LINK"]) + + +@pytest_asyncio.fixture(scope="module") +async def module__client_with_roads( + module__client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with roads examples + """ + return await module__client_example_loader(["ROADS"]) + + +@pytest_asyncio.fixture(scope="module") +async def module__client_with_namespaced_roads( + module__client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with roads examples + """ + return await module__client_example_loader(["NAMESPACED_ROADS"]) + + +@pytest_asyncio.fixture(scope="module") +async def module__client_with_account_revenue( + module__client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with account revenue examples + """ + return await module__client_example_loader(["ACCOUNT_REVENUE"]) + + +@pytest_asyncio.fixture(scope="module") +async def module__client_with_roads_and_acc_revenue( + module__client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with roads examples + """ + return await module__client_example_loader(["ROADS", "ACCOUNT_REVENUE"]) + + +@pytest_asyncio.fixture(scope="module") +async def module__client_with_basic( + module__client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with account revenue examples + """ + return await module__client_example_loader(["BASIC"]) + + +@pytest_asyncio.fixture(scope="module") +async def module__client_with_both_basics( + module__client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with account revenue examples + """ + return await module__client_example_loader(["BASIC", "BASIC_IN_DIFFERENT_CATALOG"]) + + +@pytest_asyncio.fixture(scope="module") +async def module__client_with_examples( + module__client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with all examples + """ + return await module__client_example_loader(None) + + +@pytest.fixture(scope="module") +def module__postgres_container(request) -> PostgresContainer: + """ + Setup postgres container + """ + postgres = PostgresContainer( + image="postgres:latest", + username="dj", + password="dj", + dbname=request.module.__name__, + port=5432, + driver="psycopg", + ) + with postgres: + wait_for_logs( + postgres, + r"UTC \[1\] LOG: database system is ready to accept connections", + 10, + ) + yield postgres + + +@pytest.fixture(scope="module") +def module__query_service_client( + module_mocker: MockerFixture, + duckdb_conn: duckdb.DuckDBPyConnection, +) -> Iterator[QueryServiceClient]: + """ + Custom settings for unit tests. + """ + qs_client = QueryServiceClient(uri="query_service:8001") + + def mock_get_columns_for_table( + catalog: str, + schema: str, + table: str, + engine: Optional[Engine] = None, + request_headers: Optional[Dict[str, str]] = None, + ) -> List[Column]: + return COLUMN_MAPPINGS[f"{catalog}.{schema}.{table}"] + + module_mocker.patch.object( + qs_client, + "get_columns_for_table", + mock_get_columns_for_table, + ) + + def mock_submit_query( + query_create: QueryCreate, + request_headers: Optional[Dict[str, str]] = None, + ) -> QueryWithResults: + result = duckdb_conn.sql(query_create.submitted_query) + columns = [ + {"name": col, "type": str(type_).lower()} + for col, type_ in zip(result.columns, result.types) + ] + return QueryWithResults( + id="bd98d6be-e2d2-413e-94c7-96d9411ddee2", + submitted_query=query_create.submitted_query, + state=QueryState.FINISHED, + results=[ + { + "columns": columns, + "rows": result.fetchall(), + "sql": query_create.submitted_query, + }, + ], + errors=[], + ) + + module_mocker.patch.object( + qs_client, + "submit_query", + mock_submit_query, + ) + + def mock_create_view( + view_name: str, + query_create: QueryCreate, + request_headers: Optional[Dict[str, str]] = None, + ) -> str: + duckdb_conn.sql(query_create.submitted_query) + return f"View {view_name} created successfully." + + module_mocker.patch.object( + qs_client, + "create_view", + mock_create_view, + ) + + def mock_get_query( + query_id: str, + request_headers: Optional[Dict[str, str]] = None, + ) -> Collection[Collection[str]]: + if query_id == "foo-bar-baz": + raise DJQueryServiceClientEntityNotFound("Query foo-bar-baz not found.") + for _, response in QUERY_DATA_MAPPINGS.items(): + if response.id == query_id: + return response + raise RuntimeError(f"No mocked query exists for id {query_id}") + + module_mocker.patch.object( + qs_client, + "get_query", + mock_get_query, + ) + + mock_materialize = MagicMock() + mock_materialize.return_value = MaterializationInfo( + urls=["http://fake.url/job"], + output_tables=["common.a", "common.b"], + ) + module_mocker.patch.object( + qs_client, + "materialize", + mock_materialize, + ) + + mock_materialize_cube = MagicMock() + mock_materialize_cube.return_value = MaterializationInfo( + urls=["http://fake.url/job"], + output_tables=["common.a", "common.b"], + ) + module_mocker.patch.object( + qs_client, + "materialize_cube", + mock_materialize_cube, + ) + + mock_deactivate_materialization = MagicMock() + mock_deactivate_materialization.return_value = MaterializationInfo( + urls=["http://fake.url/job"], + output_tables=[], + ) + module_mocker.patch.object( + qs_client, + "deactivate_materialization", + mock_deactivate_materialization, + ) + + mock_get_materialization_info = MagicMock() + mock_get_materialization_info.return_value = MaterializationInfo( + urls=["http://fake.url/job"], + output_tables=["common.a", "common.b"], + ) + module_mocker.patch.object( + qs_client, + "get_materialization_info", + mock_get_materialization_info, + ) + + mock_run_backfill = MagicMock() + mock_run_backfill.return_value = MaterializationInfo( + urls=["http://fake.url/job"], + output_tables=[], + ) + module_mocker.patch.object( + qs_client, + "run_backfill", + mock_run_backfill, + ) + yield qs_client + + +@pytest_asyncio.fixture(scope="module") +async def module__client_with_all_examples( + module__client_example_loader: Callable[[Optional[List[str]]], AsyncClient], +) -> AsyncClient: + """ + Provides a DJ client fixture with all examples + """ + return await module__client_example_loader(None) + + +@pytest_asyncio.fixture(scope="module") +async def module__current_user(module__session: AsyncSession) -> User: + """ + A user fixture. + """ + + new_user = User( + username="datajunction", + password="datajunction", + email="dj@datajunction.io", + name="DJ", + oauth_provider=OAuthProvider.BASIC, + is_admin=False, + ) + existing_user = await module__session.get(User, new_user.id) + if not existing_user: + module__session.add(new_user) + await module__session.commit() + user = new_user + else: + user = existing_user + return user + + +@pytest_asyncio.fixture +async def current_user(session: AsyncSession) -> User: + """ + A user fixture. + """ + + new_user = User( + username="datajunction", + password="datajunction", + email="dj@datajunction.io", + name="DJ", + oauth_provider=OAuthProvider.BASIC, + is_admin=False, + ) + existing_user = await session.get(User, new_user.id) + if not existing_user: + session.add(new_user) + await session.commit() + user = new_user + else: + user = existing_user + return user diff --git a/datajunction-server/tests/construction/__init__.py b/datajunction-server/tests/construction/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/tests/construction/build_test.py b/datajunction-server/tests/construction/build_test.py new file mode 100644 index 000000000..9c4887f54 --- /dev/null +++ b/datajunction-server/tests/construction/build_test.py @@ -0,0 +1,382 @@ +"""tests for building nodes""" + +from unittest.mock import MagicMock, patch + +import pytest +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +import datajunction_server.sql.parsing.types as ct +from datajunction_server.construction.build import ( + build_materialized_cube_node, + build_metric_nodes, + build_temp_select, + get_default_criteria, +) +from datajunction_server.construction.build_v2 import QueryBuilder +from datajunction_server.database.attributetype import AttributeType, ColumnAttribute +from datajunction_server.database.column import Column +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.user import User +from datajunction_server.errors import DJException +from datajunction_server.models.engine import Dialect +from datajunction_server.models.node_type import NodeType +from datajunction_server.naming import amenable_name +from datajunction_server.sql.parsing.backends.antlr4 import ast, parse + + +@pytest.mark.asyncio +async def test_build_metric_with_dimensions_aggs(construction_session: AsyncSession): + """ + Test building metric with dimensions + """ + num_comments_mtc: Node = await Node.get_by_name( # type: ignore + construction_session, + "basic.num_comments", + ) + query = await build_metric_nodes( + construction_session, + [num_comments_mtc], + filters=[], + dimensions=["basic.dimension.users.country", "basic.dimension.users.gender"], + orderby=[], + ) + expected = """ + WITH basic_DOT_source_DOT_comments AS ( + SELECT + basic_DOT_source_DOT_comments.id, + basic_DOT_source_DOT_comments.user_id, + basic_DOT_source_DOT_comments.timestamp, + basic_DOT_source_DOT_comments.text + FROM basic.source.comments AS basic_DOT_source_DOT_comments + ), + basic_DOT_dimension_DOT_users AS ( + SELECT + basic_DOT_source_DOT_users.id, + basic_DOT_source_DOT_users.full_name, + basic_DOT_source_DOT_users.age, + basic_DOT_source_DOT_users.country, + basic_DOT_source_DOT_users.gender, + basic_DOT_source_DOT_users.preferred_language, + basic_DOT_source_DOT_users.secret_number + FROM basic.source.users AS basic_DOT_source_DOT_users + ), + basic_DOT_source_DOT_comments_metrics AS ( + SELECT + basic_DOT_dimension_DOT_users.country basic_DOT_dimension_DOT_users_DOT_country, + basic_DOT_dimension_DOT_users.gender basic_DOT_dimension_DOT_users_DOT_gender, + COUNT(1) AS basic_DOT_num_comments + FROM basic_DOT_source_DOT_comments + INNER JOIN basic_DOT_dimension_DOT_users + ON basic_DOT_source_DOT_comments.user_id = basic_DOT_dimension_DOT_users.id + GROUP BY + basic_DOT_dimension_DOT_users.country, basic_DOT_dimension_DOT_users.gender + ) + SELECT + basic_DOT_source_DOT_comments_metrics.basic_DOT_dimension_DOT_users_DOT_country, + basic_DOT_source_DOT_comments_metrics.basic_DOT_dimension_DOT_users_DOT_gender, + basic_DOT_source_DOT_comments_metrics.basic_DOT_num_comments + FROM basic_DOT_source_DOT_comments_metrics + """ + assert str(parse(str(query))) == str(parse(str(expected))) + + +@pytest.mark.asyncio +async def test_build_metric_with_required_dimensions( + construction_session: AsyncSession, +): + """ + Test building metric with bound dimensions + """ + num_comments_mtc: Node = await Node.get_by_name( # type: ignore + construction_session, + "basic.num_comments_bnd", + ) + + query = await build_metric_nodes( + construction_session, + [num_comments_mtc], + filters=[], + dimensions=["basic.dimension.users.country", "basic.dimension.users.gender"], + orderby=[], + ) + expected = """ + WITH basic_DOT_source_DOT_comments AS ( + SELECT + basic_DOT_source_DOT_comments.id, + basic_DOT_source_DOT_comments.user_id, + basic_DOT_source_DOT_comments.timestamp, + basic_DOT_source_DOT_comments.text + FROM basic.source.comments AS basic_DOT_source_DOT_comments + ), + basic_DOT_dimension_DOT_users AS ( + SELECT + basic_DOT_source_DOT_users.id, + basic_DOT_source_DOT_users.full_name, + basic_DOT_source_DOT_users.age, + basic_DOT_source_DOT_users.country, + basic_DOT_source_DOT_users.gender, + basic_DOT_source_DOT_users.preferred_language, + basic_DOT_source_DOT_users.secret_number + FROM basic.source.users AS basic_DOT_source_DOT_users + ), + basic_DOT_source_DOT_comments_metrics AS ( + SELECT + basic_DOT_dimension_DOT_users.country basic_DOT_dimension_DOT_users_DOT_country, + basic_DOT_dimension_DOT_users.gender basic_DOT_dimension_DOT_users_DOT_gender, + COUNT(1) AS basic_DOT_num_comments_bnd + FROM basic_DOT_source_DOT_comments + INNER JOIN basic_DOT_dimension_DOT_users + ON basic_DOT_source_DOT_comments.user_id = basic_DOT_dimension_DOT_users.id + GROUP BY basic_DOT_dimension_DOT_users.country, basic_DOT_dimension_DOT_users.gender + ) + SELECT + basic_DOT_source_DOT_comments_metrics.basic_DOT_dimension_DOT_users_DOT_country, + basic_DOT_source_DOT_comments_metrics.basic_DOT_dimension_DOT_users_DOT_gender, + basic_DOT_source_DOT_comments_metrics.basic_DOT_num_comments_bnd + FROM basic_DOT_source_DOT_comments_metrics + """ + assert str(parse(str(query))) == str(parse(str(expected))) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="Shouldn't be needed with complex dim links") +async def test_raise_on_build_without_required_dimension_column( + construction_session: AsyncSession, + current_user: User, +): + """ + Test building a node that has a dimension reference without a column and a compound PK + """ + primary_key: AttributeType = next( + await construction_session.execute( + select(AttributeType).filter(AttributeType.name == "primary_key"), + ), + )[0] + countries_dim_ref = Node( + name="basic.dimension.compound_countries", + type=NodeType.DIMENSION, + current_version="1", + created_by_id=current_user.id, + ) + NodeRevision( + name=countries_dim_ref.name, + type=countries_dim_ref.type, + node=countries_dim_ref, + version="1", + query=""" + SELECT country, + 'abcd' AS country_id2, + COUNT(1) AS user_cnt + FROM basic.dimension.users + GROUP BY country + """, + columns=[ + Column( + name="country", + type=ct.StringType(), + attributes=[ColumnAttribute(attribute_type=primary_key)], + order=0, + ), + Column( + name="country_id2", + type=ct.StringType(), + attributes=[ColumnAttribute(attribute_type=primary_key)], + order=1, + ), + Column(name="user_cnt", type=ct.IntegerType(), order=2), + ], + created_by_id=current_user.id, + ) + node_foo_ref = Node( + name="basic.foo", + type=NodeType.TRANSFORM, + current_version="1", + created_by_id=current_user.id, + ) + node_foo = NodeRevision( + name=node_foo_ref.name, + type=node_foo_ref.type, + node=node_foo_ref, + version="1", + query="""SELECT num_users, 'abcd' AS country_id FROM basic.transform.country_agg""", + columns=[ + Column( + name="num_users", + type=ct.IntegerType(), + order=0, + ), + Column( + name="country_id", + type=ct.StringType(), + dimension=countries_dim_ref, + order=1, + ), + ], + created_by_id=current_user.id, + ) + construction_session.add(node_foo) + + node_bar_ref = Node( + name="basic.bar", + type=NodeType.TRANSFORM, + current_version="1", + created_by_id=current_user.id, + ) + node_bar = NodeRevision( + name=node_bar_ref.name, + type=node_bar_ref.type, + node=node_bar_ref, + version="1", + query="SELECT SUM(num_users) AS num_users " + "FROM basic.foo GROUP BY basic.dimension.compound_countries.country", + columns=[ + Column(name="num_users", type=ct.IntegerType(), order=0), + ], + created_by_id=current_user.id, + ) + construction_session.add(node_bar) + await construction_session.commit() + with pytest.raises(DJException): + query_builder = await QueryBuilder.create(construction_session, node_bar) + ( + query_builder.add_dimension( + "basic.dimension.compound_countries.country_id2", + ).build() + ) + + +@pytest.mark.asyncio +async def test_build_metric_with_dimensions_filters(construction_session: AsyncSession): + """ + Test building metric with dimension filters + """ + num_comments_mtc: Node = await Node.get_by_name( # type: ignore + construction_session, + "basic.num_comments", + ) + query = await build_metric_nodes( + construction_session, + [num_comments_mtc], + filters=[ + "basic.dimension.users.age>=25", + "basic.dimension.users.age<50", + ], + dimensions=[], + orderby=[], + ) + expected = """ + WITH basic_DOT_source_DOT_comments AS ( + SELECT + basic_DOT_source_DOT_comments.id, + basic_DOT_source_DOT_comments.user_id, + basic_DOT_source_DOT_comments.timestamp, + basic_DOT_source_DOT_comments.text + FROM basic.source.comments AS basic_DOT_source_DOT_comments + ), + basic_DOT_dimension_DOT_users AS ( + SELECT basic_DOT_source_DOT_users.id, + basic_DOT_source_DOT_users.full_name, + basic_DOT_source_DOT_users.age, + basic_DOT_source_DOT_users.country, + basic_DOT_source_DOT_users.gender, + basic_DOT_source_DOT_users.preferred_language, + basic_DOT_source_DOT_users.secret_number + FROM basic.source.users AS basic_DOT_source_DOT_users + WHERE basic_DOT_source_DOT_users.age >= 25 AND basic_DOT_source_DOT_users.age < 50 + ), + basic_DOT_source_DOT_comments_metrics AS ( + SELECT + COUNT(1) AS basic_DOT_num_comments + FROM basic_DOT_source_DOT_comments + INNER JOIN basic_DOT_dimension_DOT_users + ON basic_DOT_source_DOT_comments.user_id = basic_DOT_dimension_DOT_users.id + ) + SELECT basic_DOT_source_DOT_comments_metrics.basic_DOT_num_comments + FROM basic_DOT_source_DOT_comments_metrics + """ + assert str(parse(str(query))) == str(parse(expected)) + + +def test_amenable_name(): + """testing for making an amenable name""" + assert amenable_name("hello.名") == "hello_DOT__UNK" + + +def test_get_default_criteria(): + """Test getting default criteria for a node revision""" + result = get_default_criteria(node=NodeRevision(type=NodeType.TRANSFORM)) + assert result.dialect == Dialect.SPARK + assert result.target_node_name is None + + +@patch("datajunction_server.construction.build.parse") +def test_build_temp_select(mock_parse): + """Test building a temporary select statement""" + mock_columns = [MagicMock(name="foo"), MagicMock(name="bar")] + mock_select = MagicMock(find_all=MagicMock(return_value=mock_columns)) + mock_parse().select = mock_select + test_select = build_temp_select(temp_query="SELECT * FROM foo") + assert test_select == mock_select + + +def test_build_materialized_cube_node(): + """Test building a materialized cube node""" + result = build_materialized_cube_node( + selected_metrics=[], + selected_dimensions=[MagicMock(name="dim1"), MagicMock(name="dim2")], + cube=NodeRevision( + name="foo", + type=NodeType.CUBE, + query="SELECT * FROM foo", + columns=[], + version="1", + materializations=[MagicMock()], + availability=MagicMock(table=MagicMock(name="foo")), + ), + filters=["filter1", "filter2"], + orderby=["order1", "order2"], + limit=10, + ) + assert result == ast.Query( + name=ast.DefaultName(name="", quote_style="", namespace=None), + alias=None, + as_=None, + semantic_entity=None, + semantic_type=None, + column_list=[], + _columns=[], + select=ast.Select( + alias=None, + as_=None, + semantic_entity=None, + semantic_type=None, + quantifier="", + projection=[], + from_=ast.From( + relations=[ + ast.Relation( + primary=ast.Table( + name=ast.Name(name="foo", quote_style="", namespace=None), + alias=None, + as_=None, + semantic_entity=None, + semantic_type=None, + column_list=[], + _columns=[], + ), + extensions=[], + ), + ], + ), + group_by=[], + having=None, + where=None, + lateral_views=[], + set_op=None, + limit=None, + organization=None, + hints=None, + ), + ctes=[], + ) diff --git a/datajunction-server/tests/construction/build_v2_test.py b/datajunction-server/tests/construction/build_v2_test.py new file mode 100644 index 000000000..1909b6272 --- /dev/null +++ b/datajunction-server/tests/construction/build_v2_test.py @@ -0,0 +1,1664 @@ +"""Tests for building nodes""" + +from typing import List, Tuple + +import pytest +import pytest_asyncio +from sqlalchemy.ext.asyncio import AsyncSession + +import datajunction_server.sql.parsing.types as ct +from datajunction_server.construction.build_v2 import ( + QueryBuilder, + combine_filter_conditions, + dimension_join_path, +) +from datajunction_server.database.attributetype import AttributeType, ColumnAttribute +from datajunction_server.database.column import Column +from datajunction_server.database.dimensionlink import DimensionLink, JoinType +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.user import User +from datajunction_server.errors import ( + DJQueryBuildError, + DJQueryBuildException, + ErrorCode, +) +from datajunction_server.models.node_type import NodeType +from datajunction_server.sql.parsing import ast +from datajunction_server.sql.parsing.backends.antlr4 import parse + + +async def create_source( + session: AsyncSession, + name: str, + display_name: str, + schema_: str, + table: str, + columns: List[Column], + current_user: User, + query: str = None, +) -> Tuple[Node, NodeRevision]: + """Create source node.""" + source_node = Node( + name=name, + display_name=display_name, + type=NodeType.SOURCE, + current_version="1", + created_by_id=current_user.id, + ) + source_node_revision = NodeRevision( + node=source_node, + name=name, + display_name=display_name, + type=NodeType.SOURCE, + version="1", + query=query, + schema_=schema_, + table=table, + columns=columns, + created_by_id=current_user.id, + ) + session.add(source_node_revision) + await session.commit() + await session.refresh(source_node, ["current"]) + return source_node, source_node_revision + + +async def create_node_with_query( + session: AsyncSession, + name: str, + display_name: str, + node_type: NodeType, + query: str, + columns: List[Column], + current_user: User, +) -> Tuple[Node, NodeRevision]: + """Create node with query.""" + node = Node( + name=name, + display_name=display_name, + type=node_type, + current_version="1", + created_by_id=current_user.id, + ) + node_revision = NodeRevision( + node=node, + name=name, + display_name=display_name, + type=node_type, + version="1", + query=query, + columns=columns, + created_by_id=current_user.id, + ) + session.add(node_revision) + await session.commit() + await session.refresh(node, ["current"]) + return node, node_revision + + +@pytest_asyncio.fixture +async def primary_key_attribute(session: AsyncSession) -> AttributeType: + """ + Primary key attribute entry + """ + attribute_type = AttributeType( + namespace="system", + name="primary_key", + description="Points to a column which is part of the primary key of the node", + uniqueness_scope=[], + allowed_node_types=[ + NodeType.SOURCE, + NodeType.TRANSFORM, + NodeType.DIMENSION, + ], + ) + session.add(attribute_type) + await session.commit() + await session.refresh(attribute_type) + return attribute_type + + +@pytest_asyncio.fixture +async def events(session: AsyncSession, current_user: User) -> Node: + """ + Events source node + """ + events_node, _ = await create_source( + session, + name="source.events", + display_name="Events", + schema_="test", + table="events", + columns=[ + Column(name="event_id", type=ct.BigIntType(), order=0), + Column(name="user_id", type=ct.BigIntType(), order=1), + Column(name="device_id", type=ct.BigIntType(), order=2), + Column(name="country_code", type=ct.StringType(), order=3), + Column(name="latency", type=ct.BigIntType(), order=3), + Column(name="utc_date", type=ct.BigIntType(), order=4), + ], + current_user=current_user, + query=( + "SELECT event_id, user_id, device_id, country_code, " + "latency, utc_date FROM test.events" + ), + ) + + return events_node + + +@pytest_asyncio.fixture +async def date_dim( + session: AsyncSession, + primary_key_attribute, + current_user: User, +) -> Node: + """ + Date dimension node + """ + date_node, _ = await create_node_with_query( + session, + name="shared.date", + display_name="Date", + node_type=NodeType.DIMENSION, + query="SELECT 1, 2, 3, 4 AS dateint", + columns=[ + Column( + name="dateint", + type=ct.BigIntType(), + order=0, + attributes=[ColumnAttribute(attribute_type=primary_key_attribute)], + ), + ], + current_user=current_user, + ) + return date_node + + +@pytest_asyncio.fixture +async def events_agg(session: AsyncSession, current_user: User) -> Node: + """ + Events aggregation transform node + """ + events_agg_node, _ = await create_node_with_query( + session, + name="agg.events", + display_name="Events Aggregated", + node_type=NodeType.TRANSFORM, + query=""" + SELECT + user_id, + utc_date, + device_id, + country_code, + SUM(latency) AS total_latency + FROM source.events + GROUP BY user_id, utc_date. device_id, country_code + """, + columns=[ + Column(name="user_id", type=ct.BigIntType(), order=1), + Column(name="utc_date", type=ct.BigIntType(), order=2), + Column(name="device_id", type=ct.BigIntType(), order=3), + Column(name="country_code", type=ct.StringType(), order=4), + Column(name="total_latency", type=ct.BigIntType(), order=5), + ], + current_user=current_user, + ) + return events_agg_node + + +@pytest_asyncio.fixture +async def events_agg_complex(session: AsyncSession, current_user: User) -> Node: + """ + Events aggregation transform node with CTEs + """ + events_agg_node, _ = await create_node_with_query( + session, + name="agg.events_complex", + display_name="Events Aggregated (Unnecessarily Complex)", + node_type=NodeType.TRANSFORM, + query=""" + WITH complexity AS ( + SELECT + user_id, + utc_date, + device_id, + country_code, + SUM(latency) AS total_latency + FROM source.events + GROUP BY user_id, utc_date, device_id, country_code + ) + SELECT + CAST(user_id AS BIGINT) user_id, + CAST(utc_date AS BIGINT) utc_date, + CAST(device_id AS BIGINT) device_id, + CAST(country_code AS STR) country_code, + CAST(total_latency AS BIGINT) total_latency + FROM complexity + """, + columns=[ + Column(name="user_id", type=ct.BigIntType(), order=1), + Column(name="utc_date", type=ct.BigIntType(), order=2), + Column(name="device_id", type=ct.BigIntType(), order=3), + Column(name="country_code", type=ct.StringType(), order=4), + Column(name="total_latency", type=ct.BigIntType(), order=5), + ], + current_user=current_user, + ) + return events_agg_node + + +@pytest_asyncio.fixture +async def devices( + session: AsyncSession, + primary_key_attribute: AttributeType, + current_user: User, +) -> Node: + """ + Devices source node + devices dimension node + """ + await create_source( + session, + name="source.devices", + display_name="Devices", + schema_="test", + table="devices", + columns=[ + Column(name="device_id", type=ct.BigIntType(), order=0), + Column(name="device_name", type=ct.BigIntType(), order=1), + Column(name="device_manufacturer", type=ct.StringType(), order=2), + ], + current_user=current_user, + query="SELECT device_id, device_name, device_manufacturer FROM test.devices", + ) + + devices_dim_node, _ = await create_node_with_query( + session, + name="shared.devices", + display_name="Devices", + node_type=NodeType.DIMENSION, + query=""" + SELECT + CAST(device_id AS INT) device_id, + CAST(device_name AS STR) device_name, + device_manufacturer + FROM source.devices + """, + columns=[ + Column( + name="device_id", + type=ct.BigIntType(), + order=0, + attributes=[ColumnAttribute(attribute_type=primary_key_attribute)], + ), + Column(name="device_name", type=ct.StringType(), order=1), + Column(name="device_manufacturer", type=ct.StringType(), order=2), + ], + current_user=current_user, + ) + return devices_dim_node + + +@pytest_asyncio.fixture +async def manufacturers_dim( + session: AsyncSession, + primary_key_attribute: AttributeType, + current_user: User, +) -> Node: + """ + Manufacturers source node + dimension node + """ + await create_source( + session, + name="source.manufacturers", + display_name="Manufacturers", + schema_="test", + table="manufacturers", + columns=[ + Column(name="manufacturer_name", type=ct.BigIntType(), order=0), + Column(name="company_name", type=ct.StringType(), order=1), + Column(name="created_on", type=ct.TimestampType(), order=2), + ], + current_user=current_user, + query="SELECT manufacturer_name, company_name, created_on FROM test.manufacturers", + ) + manufacturers_dim_node, _ = await create_node_with_query( + session, + name="shared.manufacturers", + display_name="Manufacturers", + node_type=NodeType.DIMENSION, + query=""" + SELECT + CAST(manufacturer_name AS STR) name, + CAST(company_name AS STR) company_name, + created_on AS created_at, + COUNT(DISTINCT devices.device_id) AS devices_produced + FROM source.manufacturers manufacturers + JOIN shared.devices devices + ON manufacturers.manufacturer_name = devices.device_manufacturer + """, + columns=[ + Column( + name="name", + type=ct.StringType(), + order=0, + attributes=[ColumnAttribute(attribute_type=primary_key_attribute)], + ), + Column(name="company_name", type=ct.StringType(), order=1), + Column(name="created_on", type=ct.TimestampType(), order=2), + ], + current_user=current_user, + ) + return manufacturers_dim_node + + +@pytest_asyncio.fixture +async def country_dim( + session: AsyncSession, + primary_key_attribute: AttributeType, + current_user: User, +) -> Node: + """ + Countries source node + dimension node & regions source + dim + """ + await create_source( + session, + name="source.countries", + display_name="Countries", + schema_="test", + table="countries", + columns=[ + Column(name="country_code", type=ct.StringType(), order=0), + Column(name="country_name", type=ct.StringType(), order=1), + Column(name="region_code", type=ct.IntegerType(), order=2), + Column(name="population", type=ct.IntegerType(), order=3), + ], + current_user=current_user, + query="SELECT country_code, country_name, region_code, population FROM test.countries", + ) + + await create_source( + session, + name="source.regions", + display_name="Regions", + schema_="test", + table="regions", + columns=[ + Column(name="region_code", type=ct.StringType(), order=0), + Column(name="region_name", type=ct.StringType(), order=1), + ], + current_user=current_user, + query="SELECT region_code, region_name FROM test.regions", + ) + + await create_node_with_query( + session, + name="shared.regions", + display_name="Regions Dimension", + node_type=NodeType.DIMENSION, + query=""" + SELECT + region_code, + region_name + FROM source.regions + """, + columns=[ + Column( + name="region_code", + type=ct.StringType(), + order=0, + attributes=[ColumnAttribute(attribute_type=primary_key_attribute)], + ), + Column(name="region_name", type=ct.StringType(), order=1), + ], + current_user=current_user, + ) + countries_dim_node, _ = await create_node_with_query( + session, + name="shared.countries", + display_name="Countries Dimension", + node_type=NodeType.DIMENSION, + query=""" + SELECT + country_code, + country_name, + region_code, + region_name, + population + FROM source.countries countries + JOIN shared.regions ON countries.region_code = shared.regions.region_code + """, + columns=[ + Column( + name="country_code", + type=ct.StringType(), + order=0, + attributes=[ColumnAttribute(attribute_type=primary_key_attribute)], + ), + Column(name="country_name", type=ct.StringType(), order=1), + Column(name="region_code", type=ct.StringType(), order=2), + Column(name="region_name", type=ct.StringType(), order=3), + Column(name="population", type=ct.IntegerType(), order=4), + ], + current_user=current_user, + ) + return countries_dim_node + + +@pytest_asyncio.fixture +async def events_agg_countries_link( + session: AsyncSession, + events_agg: Node, + country_dim: Node, +) -> Node: + """ + Link between agg.events and shared.countries + """ + link = DimensionLink( + node_revision=events_agg.current, + dimension=country_dim, + join_sql=f"{events_agg.name}.country_code = {country_dim.name}.country_code", + join_type=JoinType.INNER, + ) + session.add(link) + await session.commit() + await session.refresh(link) + return link + + +@pytest_asyncio.fixture +async def events_devices_link( + session: AsyncSession, + events: Node, + devices: Node, +) -> Node: + """ + Link between source.events and shared.devices + """ + link = DimensionLink( + node_revision=events.current, + dimension=devices, + join_sql=f"{devices.name}.device_id = {events.name}.device_id", + join_type=JoinType.INNER, + ) + session.add(link) + await session.commit() + await session.refresh(link) + return link + + +@pytest_asyncio.fixture +async def events_agg_devices_link( + session: AsyncSession, + events_agg: Node, + devices: Node, + manufacturers_dim: Node, +) -> Node: + """ + Link between agg.events and shared.devices + """ + link = DimensionLink( + node_revision=events_agg.current, + dimension=devices, + join_sql=f"{devices.name}.device_id = {events_agg.name}.device_id", + join_type=JoinType.INNER, + ) + session.add(link) + await session.commit() + await session.refresh(link) + + link2 = DimensionLink( + node_revision=devices.current, + dimension=manufacturers_dim, + join_sql=f"{manufacturers_dim.name}.name = {devices.name}.device_manufacturer", + join_type=JoinType.INNER, + ) + session.add(link2) + await session.commit() + await session.refresh(link2) + await session.refresh(devices, ["current"]) + await session.refresh(events_agg, ["current"]) + return link + + +@pytest_asyncio.fixture +async def events_agg_complex_devices_link( + session: AsyncSession, + events_agg_complex: Node, + devices: Node, +) -> Node: + """ + Link between agg.events and shared.devices + """ + link = DimensionLink( + node_revision=events_agg_complex.current, + dimension=devices, + join_sql=f"{devices.name}.device_id = {events_agg_complex.name}.device_id", + join_type=JoinType.INNER, + ) + session.add(link) + await session.commit() + await session.refresh(link) + await session.refresh(events_agg_complex, ["current"]) + return link + + +@pytest_asyncio.fixture +async def events_agg_date_dim_link( + session: AsyncSession, + events_agg: Node, + date_dim: Node, +) -> Node: + """ + Link between agg.events and shared.date + """ + link = DimensionLink( + node_revision=events_agg.current, + dimension=date_dim, + join_sql=f"{events_agg.name}.utc_date = {date_dim.name}.dateint", + join_type=JoinType.INNER, + ) + session.add(link) + await session.commit() + await session.refresh(link) + return link + + +@pytest.mark.asyncio +async def test_dimension_join_path( + session: AsyncSession, + events: Node, + events_agg: Node, + events_agg_devices_link: Node, +): + """ + Test finding a join path between the dimension attribute and the node. + """ + path = await dimension_join_path( + session, + events_agg.current, + "shared.devices.device_manufacturer", + ) + assert [link.dimension.name for link in path] == ["shared.devices"] # type: ignore + + path = await dimension_join_path( + session, + events_agg.current, + "shared.manufacturers.name", + ) + assert [link.dimension.name for link in path] == [ # type: ignore + "shared.devices", + "shared.manufacturers", + ] + + path = await dimension_join_path( + session, + events.current, + "shared.manufacturers.name", + ) + assert path is None + + path = await dimension_join_path( + session, + events.current, + "source.events.country_code", + ) + assert path == [] + + path = await dimension_join_path( + session, + events_agg.current, + "agg.events.country_code", + ) + assert path == [] + + +@pytest.mark.asyncio +async def test_build_source_node( + session: AsyncSession, + events: Node, +): + """ + Test building a source node + """ + query_builder = await QueryBuilder.create( + session, + events.current, + ) + query_ast = await query_builder.build() + assert ( + str(query_ast).strip() + == str( + parse( + """ + SELECT + event_id, + user_id, + device_id, + country_code, + latency, + utc_date + FROM test.events + """, + ), + ).strip() + ) + + +@pytest.mark.asyncio +async def test_build_source_node_with_direct_filter( + session: AsyncSession, + events: Node, +): + """ + Test building a source node with a filter on an immediate column on the source node. + """ + query_builder = await QueryBuilder.create( + session, + events.current, + ) + query_ast = await query_builder.build() + assert ( + str(query_ast).strip() + == str( + parse( + """ + SELECT + event_id, + user_id, + device_id, + country_code, + latency, + utc_date + FROM test.events + """, + ), + ).strip() + ) + + query_ast = await query_builder.add_filters( + ["source.events.utc_date = 20210101"], + ).build() + expected = """ + WITH source_DOT_events AS ( + SELECT + source_DOT_events.event_id, + source_DOT_events.user_id, + source_DOT_events.device_id, + source_DOT_events.country_code, + source_DOT_events.latency, + source_DOT_events.utc_date + FROM ( + SELECT + event_id, + user_id, + device_id, + country_code, + latency, + utc_date + FROM test.events + WHERE utc_date = 20210101 + ) source_DOT_events + WHERE source_DOT_events.utc_date = 20210101 + ) + SELECT + source_DOT_events.event_id, + source_DOT_events.user_id, + source_DOT_events.device_id, + source_DOT_events.country_code, + source_DOT_events.latency, + source_DOT_events.utc_date + FROM source_DOT_events + """ + assert str(query_ast).strip() == str(parse(expected)).strip() + + +@pytest.mark.asyncio +async def test_build_source_with_pushdown_filters( + session: AsyncSession, + events: Node, + devices: Node, + events_devices_link: DimensionLink, +): + """ + Test building a source node with a dimension attribute filter that can be + pushed down to an immediate column on the source node. + """ + query_builder = await QueryBuilder.create( + session, + events.current, + ) + query_ast = await ( + query_builder.filter_by("shared.devices.device_id = 111") + .filter_by("shared.devices.device_id = 222") + .add_dimension("shared.devices.device_id") + .build() + ) + + expected = """ + WITH source_DOT_events AS ( + SELECT + source_DOT_events.event_id, + source_DOT_events.user_id, + source_DOT_events.device_id, + source_DOT_events.country_code, + source_DOT_events.latency, + source_DOT_events.utc_date + FROM ( + SELECT + event_id, + user_id, + device_id, + country_code, + latency, + utc_date + FROM test.events + WHERE device_id = 111 AND device_id = 222 + ) source_DOT_events + WHERE + source_DOT_events.device_id = 111 AND source_DOT_events.device_id = 222 + ) + SELECT + source_DOT_events.event_id, + source_DOT_events.user_id, + source_DOT_events.device_id shared_DOT_devices_DOT_device_id, + source_DOT_events.country_code, + source_DOT_events.latency, + source_DOT_events.utc_date + FROM source_DOT_events + """ + assert str(query_ast).strip() == str(parse(expected)).strip() + + +@pytest.mark.asyncio +async def test_build_source_with_join_filters( + session: AsyncSession, + events: Node, + devices: Node, + events_devices_link: DimensionLink, +): + """ + Test building a source node with a dimension attribute filter that + requires a join to a dimension node. + """ + query_builder = await QueryBuilder.create( + session, + events.current, + ) + query_ast = await ( + query_builder.filter_by("shared.devices.device_id = 111") + .filter_by("shared.devices.device_name = 'iPhone'") + .add_dimension("shared.devices.device_id") + .add_dimension("shared.devices.device_name") + .add_dimension("shared.devices.device_manufacturer") + .build() + ) + expected = """ + WITH + source_DOT_events AS ( + SELECT source_DOT_events.event_id, + source_DOT_events.user_id, + source_DOT_events.device_id, + source_DOT_events.country_code, + source_DOT_events.latency, + source_DOT_events.utc_date + FROM ( + SELECT + event_id, + user_id, + device_id, + country_code, + latency, + utc_date + FROM test.events + WHERE device_id = 111 + ) source_DOT_events + WHERE source_DOT_events.device_id = 111 + ), + shared_DOT_devices AS ( + SELECT + CAST(source_DOT_devices.device_id AS INT) device_id, + CAST(source_DOT_devices.device_name AS STRING) device_name, + source_DOT_devices.device_manufacturer + FROM test.devices AS source_DOT_devices + WHERE + CAST(source_DOT_devices.device_id AS INT) = 111 + AND CAST(source_DOT_devices.device_name AS STRING) = 'iPhone' + ) + SELECT source_DOT_events.event_id, + source_DOT_events.user_id, + source_DOT_events.device_id, + source_DOT_events.country_code, + source_DOT_events.latency, + source_DOT_events.utc_date, + shared_DOT_devices.device_name shared_DOT_devices_DOT_device_name, + shared_DOT_devices.device_manufacturer shared_DOT_devices_DOT_device_manufacturer, + shared_DOT_devices.device_id shared_DOT_devices_DOT_device_id + FROM source_DOT_events + INNER JOIN shared_DOT_devices + ON shared_DOT_devices.device_id = source_DOT_events.device_id + """ + assert str(query_ast).strip() == str(parse(expected)).strip() + + +@pytest.mark.asyncio +async def test_build_dimension_node( + session: AsyncSession, + devices: Node, +): + """ + Test building a dimension node + """ + query_builder = await QueryBuilder.create( + session, + devices.current, + ) + query_ast = await query_builder.build() + expected = """ + WITH shared_DOT_devices AS ( + SELECT + CAST(source_DOT_devices.device_id AS INT) device_id, + CAST(source_DOT_devices.device_name AS STRING) device_name, + source_DOT_devices.device_manufacturer + FROM test.devices AS source_DOT_devices + ) + SELECT + shared_DOT_devices.device_id, + shared_DOT_devices.device_name, + shared_DOT_devices.device_manufacturer + FROM shared_DOT_devices + """ + assert str(query_ast).strip() == str(parse(expected)).strip() + + +@pytest.mark.asyncio +async def test_build_dimension_node_with_direct_and_pushdown_filter( + session: AsyncSession, + events: Node, + devices: Node, + events_agg_devices_link: DimensionLink, +): + """ + Test building a dimension node with a direct filter and a pushdown filter (the result + in this case is the same query) + """ + expected = """ + WITH shared_DOT_devices AS ( + SELECT + CAST(source_DOT_devices.device_id AS INT) device_id, + CAST(source_DOT_devices.device_name AS STRING) device_name, + source_DOT_devices.device_manufacturer + FROM test.devices AS source_DOT_devices + WHERE source_DOT_devices.device_manufacturer = 'Apple' + ) + SELECT + shared_DOT_devices.device_id, + shared_DOT_devices.device_name, + shared_DOT_devices.device_manufacturer + FROM shared_DOT_devices + """ + # Direct filter + query_builder = await QueryBuilder.create( + session, + devices.current, + ) + query_ast = await query_builder.filter_by( + "shared.devices.device_manufacturer = 'Apple'", + ).build() + assert str(query_ast).strip() == str(parse(expected)).strip() + + # Pushdown filter + query_builder = await QueryBuilder.create( + session, + devices.current, + ) + query_ast = await ( + query_builder.filter_by("shared.manufacturers.name = 'Apple'") + ).build() + assert str(query_ast).strip() == str(parse(expected)).strip() + + +@pytest.mark.asyncio +async def test_build_transform_with_pushdown_dimensions_filters( + session: AsyncSession, + events: Node, + events_agg: Node, + devices: Node, + events_agg_devices_link: DimensionLink, + manufacturers_dim: Node, +): + """ + Test building a transform node with filters and dimensions that can be pushed down + on to the transform's columns directly. + """ + # await session.refresh(events_agg.current, ["dimension_links"]) + query_builder = await QueryBuilder.create( + session, + events_agg.current, + ) + query_ast = await ( + query_builder.filter_by("shared.devices.device_id = 111") + .filter_by("shared.devices.device_id = 222") + .add_dimension("shared.devices.device_id") + .build() + ) + expected = """ + WITH agg_DOT_events AS ( + SELECT + source_DOT_events.user_id, + source_DOT_events.utc_date, + source_DOT_events.device_id, + source_DOT_events.country_code, + SUM(source_DOT_events.latency) AS total_latency + FROM test.events AS source_DOT_events + WHERE + source_DOT_events.device_id = 111 AND source_DOT_events.device_id = 222 + GROUP BY + source_DOT_events.user_id, + source_DOT_events.device_id, + source_DOT_events.country_code + ) + SELECT + agg_DOT_events.user_id, + agg_DOT_events.utc_date, + agg_DOT_events.device_id shared_DOT_devices_DOT_device_id, + agg_DOT_events.country_code, + agg_DOT_events.total_latency + FROM agg_DOT_events + """ + assert str(query_ast).strip() == str(parse(expected)).strip() + + +@pytest.mark.asyncio +async def test_build_transform_with_deeper_pushdown_dimensions_filters( + session: AsyncSession, + events: Node, + events_agg: Node, + events_devices_link: DimensionLink, + devices: Node, + events_agg_devices_link: DimensionLink, + manufacturers_dim: Node, +): + """ + Test building a transform node with filters and dimensions that can be pushed down + both onto the transform's columns and onto its upstream source node's columns. + """ + await session.refresh(events_agg.current, ["dimension_links"]) + query_builder = await QueryBuilder.create( + session, + events_agg.current, + ) + query_ast = await ( + query_builder.filter_by("shared.devices.device_id = 111") + .filter_by("shared.devices.device_id = 222") + .add_dimension("shared.devices.device_id") + .build() + ) + expected = """ + WITH agg_DOT_events AS ( + SELECT + source_DOT_events.user_id, + source_DOT_events.utc_date, + source_DOT_events.device_id, + source_DOT_events.country_code, + SUM(source_DOT_events.latency) AS total_latency + FROM ( + SELECT + event_id, + user_id, + device_id, + country_code, + latency, + utc_date + FROM test.events + WHERE device_id = 111 AND device_id = 222 + ) source_DOT_events + WHERE + source_DOT_events.device_id = 111 AND source_DOT_events.device_id = 222 + GROUP BY + source_DOT_events.user_id, + source_DOT_events.device_id, + source_DOT_events.country_code + ) + SELECT + agg_DOT_events.user_id, + agg_DOT_events.utc_date, + agg_DOT_events.device_id shared_DOT_devices_DOT_device_id, + agg_DOT_events.country_code, + agg_DOT_events.total_latency + FROM agg_DOT_events + """ + assert str(query_ast).strip() == str(parse(expected)).strip() + + +@pytest.mark.asyncio +async def test_build_transform_w_cte_and_pushdown_dimensions_filters( + session: AsyncSession, + events: Node, + events_agg_complex: Node, + events_devices_link: DimensionLink, + devices: Node, + events_agg_complex_devices_link: DimensionLink, + manufacturers_dim: Node, +): + """ + Test building a transform node that has CTEs in the node query, built with + filters and dimensions that can be pushed down, both immediately on the transform and + at the upstream source node level. + """ + await session.refresh(events_agg_complex.current, ["dimension_links"]) + query_builder = await QueryBuilder.create( + session, + events_agg_complex.current, + ) + query_ast = await ( + query_builder.filter_by("shared.devices.device_id = 111") + .filter_by("shared.devices.device_id = 222") + .add_dimension("shared.devices.device_id") + .build() + ) + expected = """ + WITH agg_DOT_events_complex AS ( + SELECT + CAST(complexity.user_id AS BIGINT) user_id, + CAST(complexity.utc_date AS BIGINT) utc_date, + CAST(complexity.device_id AS BIGINT) device_id, + CAST(complexity.country_code AS STRING) country_code, + CAST(complexity.total_latency AS BIGINT) total_latency + FROM ( + SELECT + source_DOT_events.user_id, + source_DOT_events.utc_date, + source_DOT_events.device_id, + source_DOT_events.country_code, + SUM(source_DOT_events.latency) AS total_latency + FROM ( + SELECT + event_id, + user_id, + device_id, + country_code, + latency, + utc_date + FROM test.events + WHERE device_id = 111 AND device_id = 222 + ) source_DOT_events + GROUP BY + source_DOT_events.user_id, + source_DOT_events.utc_date, + source_DOT_events.device_id, + source_DOT_events.country_code + ) AS complexity + WHERE CAST(complexity.device_id AS BIGINT) = 111 AND CAST(complexity.device_id AS BIGINT) = 222 + ) + SELECT + agg_DOT_events_complex.user_id, + agg_DOT_events_complex.utc_date, + agg_DOT_events_complex.device_id shared_DOT_devices_DOT_device_id, + agg_DOT_events_complex.country_code, + agg_DOT_events_complex.total_latency + FROM agg_DOT_events_complex + """ + assert str(query_ast).strip() == str(parse(expected)).strip() + + +@pytest.mark.asyncio +async def test_build_transform_with_join_dimensions_filters( + session: AsyncSession, + events: Node, + events_agg: Node, + devices: Node, + events_agg_devices_link: DimensionLink, + manufacturers_dim: Node, +): + """ + Test building a transform node with filters and dimensions that require a join + """ + query_builder = await QueryBuilder.create( + session, + events_agg.current, + ) + query_ast = await ( + query_builder.filter_by("shared.devices.device_name = 'iOS'") + .filter_by("shared.devices.device_id = 222") + .add_dimension("shared.devices.device_manufacturer") + .add_dimension("shared.devices.device_id") + .build() + ) + expected = """ + WITH agg_DOT_events AS ( + SELECT + source_DOT_events.user_id, + source_DOT_events.utc_date, + source_DOT_events.device_id, + source_DOT_events.country_code, + SUM(source_DOT_events.latency) AS total_latency + FROM test.events AS source_DOT_events + WHERE source_DOT_events.device_id = 222 + GROUP BY + source_DOT_events.user_id, + source_DOT_events.device_id, + source_DOT_events.country_code + ), + shared_DOT_devices AS ( + SELECT CAST(source_DOT_devices.device_id AS INT) device_id, + CAST(source_DOT_devices.device_name AS STRING) device_name, + source_DOT_devices.device_manufacturer + FROM test.devices AS source_DOT_devices + WHERE + CAST(source_DOT_devices.device_name AS STRING) = 'iOS' + AND CAST(source_DOT_devices.device_id AS INT) = 222 + ) + SELECT + agg_DOT_events.user_id, + agg_DOT_events.utc_date, + agg_DOT_events.device_id, + agg_DOT_events.country_code, + agg_DOT_events.total_latency, + shared_DOT_devices.device_manufacturer shared_DOT_devices_DOT_device_manufacturer, + shared_DOT_devices.device_id shared_DOT_devices_DOT_device_id, + shared_DOT_devices.device_name shared_DOT_devices_DOT_device_name + FROM agg_DOT_events + INNER JOIN shared_DOT_devices + ON shared_DOT_devices.device_id = agg_DOT_events.device_id + """ + assert str(query_ast).strip() == str(parse(expected)).strip() + + +@pytest.mark.asyncio +async def test_build_transform_with_multijoin_dimensions_filters( + session: AsyncSession, + events: Node, + events_agg: Node, + devices: Node, + events_agg_devices_link: DimensionLink, + manufacturers_dim: Node, + country_dim: Node, +): + """ + Test building a transform node with filters and dimensions that require + multiple joins (multiple hops in the dimensions graph). This tests the join type + where dimension nodes themselves have a query that references an existing CTE + in the query. + """ + query_builder = await QueryBuilder.create( + session, + events_agg.current, + ) + query_ast = await ( + query_builder.filter_by("shared.manufacturers.company_name = 'Apple'") + .filter_by("shared.devices.device_id = 123") + .filter_by("shared.devices.device_manufacturer = 'Something'") + .add_dimension("shared.devices.device_manufacturer") + .build() + ) + expected = """ + WITH + agg_DOT_events AS ( + SELECT + source_DOT_events.user_id, + source_DOT_events.utc_date, + source_DOT_events.device_id, + source_DOT_events.country_code, + SUM(source_DOT_events.latency) AS total_latency + FROM test.events AS source_DOT_events + WHERE source_DOT_events.device_id = 123 + GROUP BY + source_DOT_events.user_id, + source_DOT_events.device_id, + source_DOT_events.country_code + ), + shared_DOT_devices AS ( + SELECT + CAST(source_DOT_devices.device_id AS INT) device_id, + CAST(source_DOT_devices.device_name AS STRING) device_name, + source_DOT_devices.device_manufacturer + FROM test.devices AS source_DOT_devices + WHERE + CAST(source_DOT_devices.device_id AS INT) = 123 + AND source_DOT_devices.device_manufacturer = 'Something' + ), + shared_DOT_manufacturers AS ( + SELECT + CAST(manufacturers.manufacturer_name AS STRING) name, + CAST(manufacturers.company_name AS STRING) company_name, + manufacturers.created_on AS created_at, + COUNT( DISTINCT devices.device_id) AS devices_produced + FROM test.manufacturers AS manufacturers + JOIN shared_DOT_devices devices + ON manufacturers.manufacturer_name = + devices.device_manufacturer + WHERE CAST(manufacturers.company_name AS STRING) = 'Apple' + ) + SELECT + agg_DOT_events.user_id, + agg_DOT_events.utc_date, + agg_DOT_events.device_id, + agg_DOT_events.country_code, + agg_DOT_events.total_latency, + shared_DOT_devices.device_manufacturer shared_DOT_devices_DOT_device_manufacturer, + shared_DOT_devices.device_id shared_DOT_devices_DOT_device_id, + shared_DOT_manufacturers.company_name shared_DOT_manufacturers_DOT_company_name + FROM agg_DOT_events + INNER JOIN shared_DOT_devices + ON shared_DOT_devices.device_id = agg_DOT_events.device_id + INNER JOIN shared_DOT_manufacturers + ON shared_DOT_manufacturers.name = shared_DOT_devices.device_manufacturer + """ + assert str(query_ast).strip() == str(parse(expected)).strip() + + +@pytest.mark.asyncio +async def test_build_fail_no_join_path_found( + session: AsyncSession, + events: Node, + events_agg: Node, + country_dim: Node, +): + """ + Test failed node building due to not being able to find a join path to the dimension + """ + with pytest.raises(DJQueryBuildException) as exc_info: + query_builder = await QueryBuilder.create( + session, + events_agg.current, + ) + await ( + query_builder.raise_errors() + .filter_by("shared.countries.region_name = 'APAC'") + .add_dimension("shared.countries.region_name") + .build() + ) + assert ( + "This dimension attribute cannot be joined in: shared.countries.region_name. " + "Please make sure that shared.countries is linked to agg.events" + ) in str(exc_info.value) + + # Setting ignore errors will save them to the errors list on the query builder + # object but will not raise a build exception + query_builder = await QueryBuilder.create( + session, + events_agg.current, + ) + query_ast = await ( + query_builder.ignore_errors() + .filter_by("shared.countries.region_name = 'APAC'") + .add_dimension("shared.countries.region_name") + .build() + ) + assert ( + DJQueryBuildError( + code=ErrorCode.INVALID_DIMENSION_JOIN, + message="This dimension attribute cannot be joined in: shared.countries.region_name. " + "Please make sure that shared.countries is linked to agg.events", + debug=None, + context=str(query_builder), + ) + in query_builder.errors + ) + assert query_builder.final_ast == query_ast + + +@pytest.mark.asyncio +async def test_query_builder( + session: AsyncSession, + events: Node, + events_agg: Node, + country_dim: Node, +): + """ + Test failed node building due to not being able to find a join path to the dimension + """ + query_builder = ( + ( + await QueryBuilder.create( + session, + events_agg.current, + ) + ) + .filter_by("shared.countries.region_name = 'APAC'") + .filter_by("shared.countries.region_name = 'APAC'") + .add_dimension("shared.countries.region_name") + .add_dimension("shared.countries.region_name") + .order_by(["shared.countries.region_name DESC"]) + .order_by("shared.countries.region_name DESC") + .order_by("shared.countries.region_name ASC") + .limit(100) + ) + assert query_builder.filters == ["shared.countries.region_name = 'APAC'"] + assert query_builder.dimensions == ["shared.countries.region_name"] + assert query_builder._orderby == [ + "shared.countries.region_name DESC", + "shared.countries.region_name ASC", + ] + assert query_builder._limit == 100 + assert not query_builder.include_dimensions_in_groupby + + +@pytest.mark.asyncio +async def test_build_transform_sql_without_materialized_tables( + session: AsyncSession, + events: Node, + events_agg: Node, + devices: Node, + events_agg_devices_link: DimensionLink, + manufacturers_dim: Node, + country_dim: Node, + events_agg_countries_link: DimensionLink, +): + """ + Test building a transform node with filters and dimensions that forces skipping the materialized + tables for the dependent nodes. + """ + query_builder = await QueryBuilder.create( + session, + events_agg.current, + use_materialized=False, + ) + query_ast = await ( + query_builder.filter_by("shared.manufacturers.company_name = 'Apple'") + .filter_by("shared.manufacturers.created_at > 20240101") + .filter_by("shared.countries.region_name = 'APAC'") + .add_dimension("shared.devices.device_manufacturer") + .add_dimension("shared.countries.region_name") + .build() + ) + expected = """ + WITH + source_DOT_events AS ( + SELECT event_id, + user_id, + device_id, + country_code, + latency, + utc_date + FROM test.events + ), + agg_DOT_events AS ( + SELECT source_DOT_events.user_id, + source_DOT_events.utc_date, + source_DOT_events.device_id, + source_DOT_events.country_code, + SUM(source_DOT_events.latency) AS total_latency + FROM source_DOT_events + GROUP BY source_DOT_events.user_id, source_DOT_events.device_id, source_DOT_events.country_code + ), + source_DOT_devices AS ( + SELECT device_id, + device_name, + device_manufacturer + FROM test.devices + ), + shared_DOT_devices AS ( + SELECT CAST(source_DOT_devices.device_id AS INT) device_id, + CAST(source_DOT_devices.device_name AS STRING) device_name, + source_DOT_devices.device_manufacturer + FROM source_DOT_devices + ), + source_DOT_countries AS ( + SELECT country_code, + country_name, + region_code, + population + FROM test.countries + ), + source_DOT_regions AS ( + SELECT region_code, + region_name + FROM test.regions + ), + shared_DOT_regions AS ( + SELECT source_DOT_regions.region_code, + source_DOT_regions.region_name + FROM source_DOT_regions + ), + shared_DOT_countries AS ( + SELECT countries.country_code, + countries.country_name, + shared_DOT_regions.region_code, + shared_DOT_regions.region_name, + countries.population + FROM source_DOT_countries countries JOIN shared_DOT_regions ON countries.region_code = shared_DOT_regions.region_code + WHERE shared_DOT_regions.region_name = 'APAC' + ), + source_DOT_manufacturers AS ( + SELECT manufacturer_name, + company_name, + created_on + FROM test.manufacturers + ), + shared_DOT_manufacturers AS ( + SELECT CAST(manufacturers.manufacturer_name AS STRING) name, + CAST(manufacturers.company_name AS STRING) company_name, + manufacturers.created_on AS created_at, + COUNT( DISTINCT devices.device_id) AS devices_produced + FROM source_DOT_manufacturers manufacturers JOIN shared_DOT_devices devices ON manufacturers.manufacturer_name = devices.device_manufacturer + WHERE CAST(manufacturers.company_name AS STRING) = 'Apple' AND manufacturers.created_on > 20240101 + ) + + SELECT agg_DOT_events.user_id, + agg_DOT_events.utc_date, + agg_DOT_events.device_id, + agg_DOT_events.country_code, + agg_DOT_events.total_latency, + shared_DOT_devices.device_manufacturer shared_DOT_devices_DOT_device_manufacturer, + shared_DOT_countries.region_name shared_DOT_countries_DOT_region_name, + shared_DOT_manufacturers.company_name shared_DOT_manufacturers_DOT_company_name, + shared_DOT_manufacturers.created_at shared_DOT_manufacturers_DOT_created_at + FROM agg_DOT_events INNER JOIN shared_DOT_devices ON shared_DOT_devices.device_id = agg_DOT_events.device_id + INNER JOIN shared_DOT_countries ON agg_DOT_events.country_code = shared_DOT_countries.country_code + INNER JOIN shared_DOT_manufacturers ON shared_DOT_manufacturers.name = shared_DOT_devices.device_manufacturer + """ + assert str(query_ast).strip() == str(parse(expected)).strip() + + query_builder = await QueryBuilder.create( + session, + events_agg.current, + use_materialized=True, + ) + query_ast = await ( + query_builder.filter_by("shared.manufacturers.company_name = 'Apple'") + .filter_by("shared.manufacturers.created_at > 20240101") + .filter_by("shared.countries.region_name = 'APAC'") + .add_dimension("shared.devices.device_manufacturer") + .add_dimension("shared.countries.region_name") + .build() + ) + assert str(query_ast).strip() != str(parse(expected)).strip() + + +@pytest.mark.asyncio +async def test_build_transform_with_multijoin_dimensions_with_extra_ctes( + session: AsyncSession, + events: Node, + events_agg: Node, + devices: Node, + events_agg_devices_link: DimensionLink, + manufacturers_dim: Node, + country_dim: Node, + events_agg_countries_link: DimensionLink, +): + """ + Test building a transform node with filters and dimensions that require + multiple joins (multiple hops in the dimensions graph). This tests the join type + where dimension nodes themselves have a query that brings in an additional node that + is not already a CTE on the query. + """ + query_builder = await QueryBuilder.create(session, events_agg.current) + query_ast = await ( + query_builder.filter_by("shared.manufacturers.company_name = 'Apple'") + .filter_by("shared.manufacturers.created_at > 20240101") + .filter_by("shared.countries.region_name = 'APAC'") + .add_dimension("shared.devices.device_manufacturer") + .add_dimension("shared.countries.region_name") + .build() + ) + expected = """ + WITH + agg_DOT_events AS ( + SELECT + source_DOT_events.user_id, + source_DOT_events.utc_date, + source_DOT_events.device_id, + source_DOT_events.country_code, + SUM(source_DOT_events.latency) AS total_latency + FROM test.events AS source_DOT_events + GROUP BY + source_DOT_events.user_id, + source_DOT_events.device_id, + source_DOT_events.country_code + ), + shared_DOT_devices AS ( + SELECT CAST(source_DOT_devices.device_id AS INT) device_id, + CAST(source_DOT_devices.device_name AS STRING) device_name, + source_DOT_devices.device_manufacturer + FROM test.devices AS source_DOT_devices + ), + shared_DOT_regions AS ( + SELECT + source_DOT_regions.region_code, + source_DOT_regions.region_name + FROM test.regions AS source_DOT_regions + ), + shared_DOT_countries AS ( + SELECT countries.country_code, + countries.country_name, + shared_DOT_regions.region_code, + shared_DOT_regions.region_name, + countries.population + FROM test.countries AS countries + JOIN shared_DOT_regions + ON countries.region_code = shared_DOT_regions.region_code + WHERE + shared_DOT_regions.region_name = 'APAC' + ), + shared_DOT_manufacturers AS ( + SELECT + CAST(manufacturers.manufacturer_name AS STRING) name, + CAST(manufacturers.company_name AS STRING) company_name, + manufacturers.created_on AS created_at, + COUNT( DISTINCT devices.device_id) AS devices_produced + FROM test.manufacturers AS manufacturers + JOIN shared_DOT_devices devices ON manufacturers.manufacturer_name = + devices.device_manufacturer + WHERE + CAST(manufacturers.company_name AS STRING) = 'Apple' + AND manufacturers.created_on > 20240101 + ) + + SELECT agg_DOT_events.user_id, + agg_DOT_events.utc_date, + agg_DOT_events.device_id, + agg_DOT_events.country_code, + agg_DOT_events.total_latency, + shared_DOT_devices.device_manufacturer shared_DOT_devices_DOT_device_manufacturer, + shared_DOT_countries.region_name shared_DOT_countries_DOT_region_name, + shared_DOT_manufacturers.company_name shared_DOT_manufacturers_DOT_company_name, + shared_DOT_manufacturers.created_at shared_DOT_manufacturers_DOT_created_at + FROM agg_DOT_events + INNER JOIN shared_DOT_devices + ON shared_DOT_devices.device_id = agg_DOT_events.device_id + INNER JOIN shared_DOT_countries + ON agg_DOT_events.country_code = shared_DOT_countries.country_code + INNER JOIN shared_DOT_manufacturers + ON shared_DOT_manufacturers.name = shared_DOT_devices.device_manufacturer + """ + assert str(query_ast).strip() == str(parse(expected)).strip() + + +@pytest.mark.asyncio +async def test_build_with_source_filters( + session: AsyncSession, + events: Node, + events_agg: Node, + date_dim: Node, + events_agg_date_dim_link: DimensionLink, +): + """ + Test build node with filters on source + """ + query_builder = await QueryBuilder.create( + session, + events_agg.current, + ) + query_ast = await query_builder.filter_by("shared.date.dateint = 20250101").build() + expected = """ + WITH + agg_DOT_events AS ( + SELECT source_DOT_events.user_id, + source_DOT_events.utc_date, + source_DOT_events.device_id, + source_DOT_events.country_code, + SUM(source_DOT_events.latency) AS total_latency + FROM test.events AS source_DOT_events + WHERE source_DOT_events.utc_date = 20250101 + GROUP BY + source_DOT_events.user_id, + source_DOT_events.device_id, + source_DOT_events.country_code + ) + + SELECT agg_DOT_events.user_id, + agg_DOT_events.utc_date, + agg_DOT_events.device_id, + agg_DOT_events.country_code, + agg_DOT_events.total_latency + FROM agg_DOT_events + """ + assert str(query_ast).strip() == str(parse(expected)).strip() + + +def test_combine_filter_conditions(): + """ + Tests combining filter conditions + """ + assert combine_filter_conditions(None) is None + assert combine_filter_conditions(None, None) is None + assert ( + str( + combine_filter_conditions( + None, + ast.BinaryOp( + op=ast.BinaryOpKind.Eq, + left=ast.Column(name=ast.Name("abc")), + right=ast.String("'one'"), + ), + ), + ) + == "abc = 'one'" + ) + assert ( + str( + combine_filter_conditions( + None, + ast.BinaryOp( + op=ast.BinaryOpKind.Eq, + left=ast.Column(name=ast.Name("abc")), + right=ast.String("'one'"), + ), + ast.BinaryOp( + op=ast.BinaryOpKind.Eq, + left=ast.Column(name=ast.Name("def")), + right=ast.String("'two'"), + ), + ), + ) + == "abc = 'one' AND def = 'two'" + ) + assert ( + str( + combine_filter_conditions( + ast.BinaryOp( + op=ast.BinaryOpKind.Eq, + left=ast.Column(name=ast.Name("abc")), + right=ast.String("'one'"), + ), + ast.BinaryOp( + op=ast.BinaryOpKind.Eq, + left=ast.Column(name=ast.Name("def")), + right=ast.String("'two'"), + ), + ), + ) + == "abc = 'one' AND def = 'two'" + ) diff --git a/datajunction-server/tests/construction/compile_test.py b/datajunction-server/tests/construction/compile_test.py new file mode 100644 index 000000000..fff1f69bd --- /dev/null +++ b/datajunction-server/tests/construction/compile_test.py @@ -0,0 +1,218 @@ +""" +Tests for compiling nodes +""" + +import pytest +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.errors import DJException +from datajunction_server.sql.parsing.ast import CompileContext +from datajunction_server.sql.parsing.backends.antlr4 import parse +from datajunction_server.sql.parsing.backends.exceptions import DJParseException + + +@pytest.mark.asyncio +async def test_get_table_node_is_none(construction_session: AsyncSession): + """ + Test a nonexistent table node with compound exception ignore + """ + + query = parse("select x from purchases") + ctx = CompileContext( + session=construction_session, + exception=DJException(), + ) + await query.compile(ctx) + + assert "No node `purchases`" in str(ctx.exception.errors) + + +@pytest.mark.asyncio +async def test_missing_references(construction_session: AsyncSession): + """ + Test getting dependencies from a query that has dangling references + """ + query = parse("select a, b, c from does_not_exist") + exception = DJException() + context = CompileContext( + session=construction_session, + exception=exception, + ) + _, missing_references = await query.extract_dependencies(context) + assert missing_references + + +@pytest.mark.asyncio +async def test_catching_dangling_refs_in_extract_dependencies( + construction_session: AsyncSession, +): + """ + Test getting dependencies from a query that has dangling references when set not to raise + """ + query = parse("select a, b, c from does_not_exist") + exception = DJException() + context = CompileContext( + session=construction_session, + exception=exception, + ) + _, danglers = await query.extract_dependencies(context) + assert "does_not_exist" in danglers + + +@pytest.mark.asyncio +async def test_raising_on_extract_from_node_with_no_query(): + """ + Test parsing an empty query fails + """ + with pytest.raises(DJParseException) as exc_info: + parse(None) + assert "Empty query provided!" in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_raise_on_unnamed_subquery_in_implicit_join( + construction_session: AsyncSession, +): + """ + Test raising on an unnamed subquery in an implicit join + """ + query = parse( + "SELECT country FROM basic.transform.country_agg, " + "(SELECT country FROM basic.transform.country_agg)", + ) + + context = CompileContext( + session=construction_session, + exception=DJException(), + ) + await query.extract_dependencies(context) + assert ( + "Column `country` found in multiple tables. Consider using fully qualified name." + in str( + context.exception.errors, + ) + ) + + +@pytest.mark.asyncio +async def test_raise_on_ambiguous_column(construction_session: AsyncSession): + """ + Test raising on ambiguous column + """ + query = parse( + "SELECT country FROM basic.transform.country_agg a " + "LEFT JOIN basic.dimension.countries b on a.country = b.country", + ) + context = CompileContext( + session=construction_session, + exception=DJException(), + ) + await query.compile(context) + assert ( + "Column `country` found in multiple tables. Consider using fully qualified name." + in str( + context.exception.errors, + ) + ) + + +@pytest.mark.asyncio +async def test_compile_node(construction_session: AsyncSession): + """ + Test compiling a node + """ + node_a = Node(name="A", current_version="1") + node_a_rev = NodeRevision( + node=node_a, + version="1", + query="SELECT country FROM basic.transform.country_agg", + ) + query_ast = parse(node_a_rev.query) + ctx = CompileContext(session=construction_session, exception=DJException()) + await query_ast.compile(ctx) + + +@pytest.mark.asyncio +async def test_raise_on_compile_node_with_no_query(construction_session: AsyncSession): + """ + Test raising when compiling a node that has no query + """ + node_a = Node(name="A", current_version="1") + node_a_rev = NodeRevision(node=node_a, version="1") + + with pytest.raises(DJException) as exc_info: + query_ast = parse(node_a_rev.query) + ctx = CompileContext(session=construction_session, exception=DJException()) + await query_ast.compile(ctx) + + assert "Empty query provided" in str(exc_info.value) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="DJ should not validate query correctness") +async def test_raise_on_unjoinable_automatic_dimension_groupby( + construction_session: AsyncSession, +): + """ + Test raising where a dimension node is automatically detected but unjoinable + """ + node_a = Node(name="A", current_version="1") + node_a_rev = NodeRevision( + node=node_a, + version="1", + query=( + "SELECT country FROM basic.transform.country_agg " + "GROUP BY basic.dimension.countries.country" + ), + ) + + query_ast = parse(node_a_rev.query) + ctx = CompileContext(session=construction_session, exception=DJException()) + await query_ast.compile(ctx) + + assert ( + "Column `basic.dimension.countries.country` does not exist on any valid table." + in str( + ctx.exception.errors, + ) + ) + + +@pytest.mark.asyncio +async def test_raise_on_having_without_a_groupby(construction_session: AsyncSession): + """ + Test raising when using a having without a groupby + """ + node_a = Node(name="A", current_version="1") + node_a_rev = NodeRevision( + node=node_a, + version="1", + query=("SELECT country FROM basic.transform.country_agg HAVING country='US'"), + ) + + query_ast = parse(node_a_rev.query) + ctx = CompileContext(session=construction_session, exception=DJException()) + await query_ast.compile(ctx) + + assert "HAVING without a GROUP BY is not allowed" in str(ctx.exception.errors) + + +@pytest.mark.asyncio +async def test_having(construction_session: AsyncSession): + """ + Test using having + """ + node_a = Node(name="A", current_version="1") + node_a_rev = NodeRevision( + node=node_a, + version="1", + query=( + "SELECT order_date, status FROM dbt.source.jaffle_shop.orders " + "GROUP BY dbt.dimension.customers.id " + "HAVING dbt.dimension.customers.id=1" + ), + ) + query_ast = parse(node_a_rev.query) + ctx = CompileContext(session=construction_session, exception=DJException()) + await query_ast.compile(ctx) diff --git a/datajunction-server/tests/construction/conftest.py b/datajunction-server/tests/construction/conftest.py new file mode 100644 index 000000000..27320cec3 --- /dev/null +++ b/datajunction-server/tests/construction/conftest.py @@ -0,0 +1,610 @@ +"""fixtures for testing construction""" + +from typing import Dict, List, Optional, Tuple + +import pytest +import pytest_asyncio +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database.attributetype import AttributeType, ColumnAttribute +from datajunction_server.database.column import Column +from datajunction_server.database.database import Database +from datajunction_server.database.dimensionlink import DimensionLink, JoinType +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.user import User +from datajunction_server.models.node_type import NodeType +from datajunction_server.sql.parsing.types import ( + DateType, + FloatType, + IntegerType, + MapType, + StringType, + TimestampType, +) + +BUILD_NODE_NAMES: List[str] = [ + "basic.source.users", + "basic.source.comments", + "basic.dimension.users", + "dbt.source.jaffle_shop.orders", + "dbt.dimension.customers", + "dbt.source.jaffle_shop.customers", + "basic.dimension.countries", + "basic.transform.country_agg", + "basic.num_comments", + "basic.num_users", + "dbt.transform.customer_agg", +] + +BUILD_EXPECTATION_PARAMETERS: List[Tuple[str, Optional[int]]] = list( + zip(BUILD_NODE_NAMES * 3, [None] * len(BUILD_NODE_NAMES)), +) + + +@pytest.fixture +def build_expectation() -> Dict[str, Dict[Optional[int], Tuple[bool, str]]]: + """map node names with database ids to what their build results should be""" + return { + """basic.source.users""": { + None: ( + True, + """ + SELECT + id, + full_name, + names_map, + user_metadata, + age, + country, + gender, + preferred_language, + secret_number + FROM basic.source.users + """, + ), + }, + """basic.source.comments""": { + None: ( + True, + """ + SELECT + id, + user_id, + timestamp, + text + FROM basic.source.comments + """, + ), + }, + """basic.dimension.users""": { + None: ( + True, + """SELECT basic_DOT_dimension_DOT_users.id, + basic_DOT_dimension_DOT_users.full_name, + basic_DOT_dimension_DOT_users.age, + basic_DOT_dimension_DOT_users.country, + basic_DOT_dimension_DOT_users.gender, + basic_DOT_dimension_DOT_users.preferred_language, + basic_DOT_dimension_DOT_users.secret_number + FROM (SELECT basic_DOT_source_DOT_users.id, + basic_DOT_source_DOT_users.full_name, + basic_DOT_source_DOT_users.age, + basic_DOT_source_DOT_users.country, + basic_DOT_source_DOT_users.gender, + basic_DOT_source_DOT_users.preferred_language, + basic_DOT_source_DOT_users.secret_number + FROM basic.source.users AS basic_DOT_source_DOT_users) + AS basic_DOT_dimension_DOT_users""", + ), + }, + """dbt.source.jaffle_shop.orders""": { + None: ( + False, + """Node has no query. Cannot generate a build plan without a query.""", + ), + }, + """dbt.dimension.customers""": { + None: ( + True, + """SELECT dbt_DOT_dimension_DOT_customers.id, + dbt_DOT_dimension_DOT_customers.first_name, + dbt_DOT_dimension_DOT_customers.last_name + FROM (SELECT dbt_DOT_source_DOT_jaffle_shop_DOT_customers.id, + dbt_DOT_source_DOT_jaffle_shop_DOT_customers.first_name, + dbt_DOT_source_DOT_jaffle_shop_DOT_customers.last_name + FROM dbt.source.jaffle_shop.customers AS dbt_DOT_source_DOT_jaffle_shop_DOT_customers) + AS dbt_DOT_dimension_DOT_customers""", + ), + }, + """dbt.source.jaffle_shop.customers""": { + None: ( + False, + """Node has no query. Cannot generate a build plan without a query.""", + ), + }, + """basic.dimension.countries""": { + None: ( + True, + """SELECT basic_DOT_dimension_DOT_countries.country, + basic_DOT_dimension_DOT_countries.user_cnt + FROM (SELECT basic_DOT_dimension_DOT_users.country, + COUNT(1) AS user_cnt + FROM (SELECT basic_DOT_source_DOT_users.id, + basic_DOT_source_DOT_users.full_name, + basic_DOT_source_DOT_users.age, + basic_DOT_source_DOT_users.country, + basic_DOT_source_DOT_users.gender, + basic_DOT_source_DOT_users.preferred_language, + basic_DOT_source_DOT_users.secret_number + FROM basic.source.users AS basic_DOT_source_DOT_users) + AS basic_DOT_dimension_DOT_users + GROUP BY basic_DOT_dimension_DOT_users.country) + AS basic_DOT_dimension_DOT_countries""", + ), + }, + """basic.transform.country_agg""": { + None: ( + True, + """SELECT basic_DOT_transform_DOT_country_agg.country, + basic_DOT_transform_DOT_country_agg.num_users + FROM (SELECT basic_DOT_source_DOT_users.country, + COUNT( DISTINCT basic_DOT_source_DOT_users.id) AS num_users + FROM basic.source.users AS basic_DOT_source_DOT_users + GROUP BY basic_DOT_source_DOT_users.country) + AS basic_DOT_transform_DOT_country_agg""", + ), + }, + """basic.num_comments""": { + None: ( + True, + """SELECT COUNT(1) AS basic_DOT_num_comments + FROM basic.source.comments AS basic_DOT_source_DOT_comments""", + ), + }, + """basic.num_users""": { + None: ( + True, + """SELECT SUM(basic_DOT_transform_DOT_country_agg.num_users) AS basic_DOT_num_users + FROM (SELECT basic_DOT_source_DOT_users.country, + COUNT(DISTINCT basic_DOT_source_DOT_users.id) AS num_users + FROM basic.source.users AS basic_DOT_source_DOT_users + + GROUP BY basic_DOT_source_DOT_users.country) AS basic_DOT_transform_DOT_country_agg""", + ), + }, + """dbt.transform.customer_agg""": { + None: ( + True, + """SELECT dbt_DOT_transform_DOT_customer_agg.id, + dbt_DOT_transform_DOT_customer_agg.first_name, + dbt_DOT_transform_DOT_customer_agg.last_name, + dbt_DOT_transform_DOT_customer_agg.order_cnt + FROM (SELECT c.id, + c.first_name, + c.last_name, + COUNT(1) AS order_cnt + FROM dbt.source.jaffle_shop.orders AS o JOIN dbt.source.jaffle_shop.customers AS c ON o.user_id = c.id + GROUP BY c.id, c.first_name, c.last_name) + AS dbt_DOT_transform_DOT_customer_agg""", + ), + }, + } + + +@pytest_asyncio.fixture +async def construction_session( + session: AsyncSession, + current_user: User, +) -> AsyncSession: + """ + Add some source nodes and transform nodes to facilitate testing of extracting dependencies + """ + + postgres = Database(name="postgres", URI="", cost=10, id=1) + + gsheets = Database(name="gsheets", URI="", cost=100, id=2) + primary_key = AttributeType(namespace="system", name="primary_key", description="") + countries_dim_ref = Node( + name="basic.dimension.countries", + type=NodeType.DIMENSION, + current_version="1", + created_by_id=current_user.id, + ) + countries_dim = NodeRevision( + name=countries_dim_ref.name, + type=countries_dim_ref.type, + node=countries_dim_ref, + version="1", + query=""" + SELECT country, + COUNT(1) AS user_cnt + FROM basic.dimension.users + GROUP BY country + """, + columns=[ + Column( + name="country", + order=0, + type=StringType(), + attributes=[ColumnAttribute(attribute_type=primary_key)], + ), + Column(name="user_cnt", type=IntegerType(), order=1), + ], + created_by_id=current_user.id, + ) + + user_dim_ref = Node( + name="basic.dimension.users", + type=NodeType.DIMENSION, + current_version="1", + created_by_id=current_user.id, + ) + user_dim = NodeRevision( + name=user_dim_ref.name, + type=user_dim_ref.type, + node=user_dim_ref, + version="1", + query=""" + SELECT id, + full_name, + age, + country, + gender, + preferred_language, + secret_number + FROM basic.source.users + """, + columns=[ + Column( + name="id", + order=0, + type=IntegerType(), + attributes=[ColumnAttribute(attribute_type=primary_key)], + ), + Column(name="full_name", type=StringType(), order=1), + Column(name="age", type=IntegerType(), order=2), + Column(name="country", type=StringType(), order=3), + Column(name="gender", type=StringType(), order=4), + Column(name="preferred_language", type=StringType(), order=5), + Column(name="secret_number", type=FloatType(), order=6), + ], + created_by_id=current_user.id, + ) + + country_agg_tfm_ref = Node( + name="basic.transform.country_agg", + type=NodeType.TRANSFORM, + current_version="1", + created_by_id=current_user.id, + ) + country_agg_tfm = NodeRevision( + name=country_agg_tfm_ref.name, + type=country_agg_tfm_ref.type, + node=country_agg_tfm_ref, + version="1", + query=""" + SELECT country, + COUNT(DISTINCT id) AS num_users + FROM basic.source.users + GROUP BY country + """, + columns=[ + Column( + name="country", + type=StringType(), + dimension=user_dim_ref, + dimension_column="country", + order=0, + ), + Column(name="num_users", type=IntegerType(), order=1), + ], + created_by_id=current_user.id, + ) + + users_src_ref = Node( + name="basic.source.users", + type=NodeType.SOURCE, + current_version="1", + created_by_id=current_user.id, + ) + users_src = NodeRevision( + name=users_src_ref.name, + type=users_src_ref.type, + node=users_src_ref, + version="1", + columns=[ + Column(name="id", type=IntegerType(), order=0), + Column(name="full_name", type=StringType(), order=1), + Column( + name="names_map", + type=MapType(key_type=StringType(), value_type=StringType()), + order=2, + ), + Column( + name="user_metadata", + type=MapType( + key_type=StringType(), + value_type=MapType( + key_type=StringType(), + value_type=MapType( + key_type=StringType(), + value_type=FloatType(), + ), + ), + ), + order=3, + ), + Column(name="age", type=IntegerType(), order=4), + Column(name="country", type=StringType(), order=5), + Column(name="gender", type=StringType(), order=6), + Column(name="preferred_language", type=StringType(), order=7), + Column(name="secret_number", type=FloatType(), order=8), + ], + created_by_id=current_user.id, + ) + + comments_src_ref = Node( + name="basic.source.comments", + type=NodeType.SOURCE, + current_version="1", + created_by_id=current_user.id, + ) + comments_src = NodeRevision( + name=comments_src_ref.name, + type=comments_src_ref.type, + node=comments_src_ref, + version="1", + columns=[ + Column(name="id", type=IntegerType(), order=0), + Column( + name="user_id", + type=IntegerType(), + dimension=user_dim_ref, + order=1, + ), + Column(name="timestamp", type=TimestampType(), order=2), + Column(name="text", type=StringType(), order=3), + ], + created_by_id=current_user.id, + ) + + num_comments_mtc_ref = Node( + name="basic.num_comments", + type=NodeType.METRIC, + current_version="1", + created_by_id=current_user.id, + ) + num_comments_mtc = NodeRevision( + name=num_comments_mtc_ref.name, + type=num_comments_mtc_ref.type, + node=num_comments_mtc_ref, + version="1", + query=""" + SELECT COUNT(1) AS cnt + FROM basic.source.comments + """, + columns=[ + Column(name="cnt", type=IntegerType(), order=0), + ], + parents=[comments_src_ref], + created_by_id=current_user.id, + ) + + num_comments_mtc_bnd_dims_ref = Node( + name="basic.num_comments_bnd", + type=NodeType.METRIC, + current_version="1", + created_by_id=current_user.id, + ) + num_comments_mtc_bnd_dims = NodeRevision( + name=num_comments_mtc_bnd_dims_ref.name, + type=num_comments_mtc_bnd_dims_ref.type, + node=num_comments_mtc_bnd_dims_ref, + version="1", + query=""" + SELECT COUNT(1) AS cnt + FROM basic.source.comments + """, + parents=[comments_src_ref], + columns=[ + Column(name="cnt", type=IntegerType(), order=0), + ], + required_dimensions=[ + comments_src.columns[0], + comments_src.columns[-1], + ], + created_by_id=current_user.id, + ) + + num_users_mtc_ref = Node( + name="basic.num_users", + type=NodeType.METRIC, + current_version="1", + created_by_id=current_user.id, + ) + num_users_mtc = NodeRevision( + name=num_users_mtc_ref.name, + type=num_users_mtc_ref.type, + node=num_users_mtc_ref, + version="1", + query=""" + SELECT SUM(num_users) AS col0 + FROM basic.transform.country_agg + """, + columns=[ + Column(name="col0", type=IntegerType(), order=0), + ], + parents=[country_agg_tfm_ref], + created_by_id=current_user.id, + ) + num_users_us_join_mtc_ref = Node( + name="basic.num_users_us", + type=NodeType.METRIC, + current_version="1", + created_by_id=current_user.id, + ) + num_users_us_join_mtc = NodeRevision( + name=num_users_us_join_mtc_ref.name, + type=num_users_us_join_mtc_ref.type, + node=num_users_us_join_mtc_ref, + version="1", + query=""" + SELECT SUM(a.num_users) as sum_users + FROM basic.transform.country_agg a + INNER JOIN basic.source.users b + ON a.country=b.country + WHERE a.country='US' + """, + columns=[ + Column( + name="sum_users", + type=IntegerType(), + order=0, + ), + ], + parents=[country_agg_tfm_ref, users_src_ref], + created_by_id=current_user.id, + ) + customers_dim_ref = Node( + name="dbt.dimension.customers", + type=NodeType.DIMENSION, + current_version="1", + created_by_id=current_user.id, + ) + customers_dim = NodeRevision( + name=customers_dim_ref.name, + type=customers_dim_ref.type, + node=customers_dim_ref, + version="1", + query=""" + SELECT id, + first_name, + last_name + FROM dbt.source.jaffle_shop.customers + """, + columns=[ + Column( + name="id", + type=IntegerType(), + attributes=[ColumnAttribute(attribute_type=primary_key)], + order=0, + ), + Column(name="first_name", type=StringType(), order=1), + Column(name="last_name", type=StringType(), order=2), + ], + created_by_id=current_user.id, + ) + + customers_agg_tfm_ref = Node( + name="dbt.transform.customer_agg", + type=NodeType.TRANSFORM, + current_version="1", + created_by_id=current_user.id, + ) + customers_agg_tfm = NodeRevision( + name=customers_agg_tfm_ref.name, + type=customers_agg_tfm_ref.type, + node=customers_agg_tfm_ref, + version="1", + query=""" + SELECT c.id, + c.first_name, + c.last_name, + COUNT(1) AS order_cnt + FROM dbt.source.jaffle_shop.orders o + JOIN dbt.source.jaffle_shop.customers c ON o.user_id = c.id + GROUP BY c.id, + c.first_name, + c.last_name + """, + columns=[ + Column(name="id", type=IntegerType(), order=0), + Column(name="first_name", type=StringType(), order=1), + Column(name="last_name", type=StringType(), order=2), + Column(name="order_cnt", type=IntegerType(), order=3), + ], + created_by_id=current_user.id, + ) + + orders_src_ref = Node( + name="dbt.source.jaffle_shop.orders", + type=NodeType.SOURCE, + current_version="1", + created_by_id=current_user.id, + ) + orders_src = NodeRevision( + name=orders_src_ref.name, + type=orders_src_ref.type, + node=orders_src_ref, + version="1", + columns=[ + Column(name="id", type=IntegerType(), order=0), + Column( + name="user_id", + type=IntegerType(), + dimension=customers_dim_ref, + dimension_column="event_id", + order=1, + ), + Column(name="order_date", type=DateType(), order=2), + Column(name="status", type=StringType(), order=3), + Column(name="_etl_loaded_at", type=TimestampType(), order=4), + ], + created_by_id=current_user.id, + ) + + customers_src_ref = Node( + name="dbt.source.jaffle_shop.customers", + type=NodeType.SOURCE, + current_version="1", + created_by_id=current_user.id, + ) + customers_src = NodeRevision( + name=customers_src_ref.name, + type=customers_src_ref.type, + node=customers_src_ref, + version="1", + columns=[ + Column(name="id", type=IntegerType(), order=0), + Column(name="first_name", type=StringType(), order=1), + Column(name="last_name", type=StringType(), order=2), + ], + created_by_id=current_user.id, + ) + + comments_users_link = DimensionLink( + node_revision=comments_src, + dimension=user_dim_ref, + join_sql="basic.source.comments.user_id = basic.dimension.users.id", + join_type=JoinType.INNER, + ) + + orders_customers_link = DimensionLink( + node_revision=orders_src, + dimension=customers_dim_ref, + join_sql="dbt.source.jaffle_shop.orders.user_id = dbt.dimension.customers.id", + join_type=JoinType.INNER, + ) + + session.add(postgres) + session.add(gsheets) + session.add(countries_dim) + session.add(user_dim) + session.add(country_agg_tfm) + session.add(users_src) + session.add(comments_src) + session.add(num_users_us_join_mtc) + session.add(num_comments_mtc) + session.add(num_comments_mtc_bnd_dims) + session.add(num_users_mtc) + session.add(customers_dim) + session.add(customers_agg_tfm) + session.add(orders_src) + session.add(customers_src) + + session.add(comments_users_link) + session.add(orders_customers_link) + await session.commit() + await session.refresh(comments_users_link) + await session.refresh(orders_customers_link) + return session diff --git a/datajunction-server/tests/construction/exceptions_test.py b/datajunction-server/tests/construction/exceptions_test.py new file mode 100644 index 000000000..0256fbddd --- /dev/null +++ b/datajunction-server/tests/construction/exceptions_test.py @@ -0,0 +1,48 @@ +""" +Tests for building nodes and extracting dependencies +""" + +import pytest + +from datajunction_server.construction.exceptions import CompoundBuildException +from datajunction_server.errors import DJError, DJException, ErrorCode + + +def test_compound_build_exception(): + """ + Test raising a CompoundBuildException + """ + CompoundBuildException().reset() + CompoundBuildException().set_raise(False) + CompoundBuildException().append( + error=DJError( + code=ErrorCode.INVALID_SQL_QUERY, + message="This SQL is invalid.", + ), + message="Testing a compound build exception", + ) + + assert len(CompoundBuildException().errors) == 1 + assert CompoundBuildException().errors[0].code == ErrorCode.INVALID_SQL_QUERY + + assert "Found 1 issue" in str(CompoundBuildException()) + + CompoundBuildException().reset() + + +def test_raise_compound_build_exception(): + """ + Test raising a CompoundBuildException + """ + CompoundBuildException().reset() + CompoundBuildException().set_raise(True) + with pytest.raises(DJException) as exc_info: + CompoundBuildException().append( + error=DJError( + code=ErrorCode.INVALID_SQL_QUERY, + message="This SQL is invalid.", + ), + message="Testing a compound build exception", + ) + + assert "Testing a compound build exception" in str(exc_info.value) diff --git a/datajunction-server/tests/construction/inference_test.py b/datajunction-server/tests/construction/inference_test.py new file mode 100644 index 000000000..03b2e1b63 --- /dev/null +++ b/datajunction-server/tests/construction/inference_test.py @@ -0,0 +1,696 @@ +"""Test type inference.""" + +import pytest +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.errors import DJException +from datajunction_server.models.engine import Dialect +from datajunction_server.sql.parsing import ast +from datajunction_server.sql.parsing.ast import CompileContext +from datajunction_server.sql.parsing.backends.antlr4 import parse +from datajunction_server.sql.parsing.backends.exceptions import DJParseException +from datajunction_server.sql.parsing.types import ( + BigIntType, + BooleanType, + ColumnType, + DateType, + DayTimeIntervalType, + DecimalType, + DoubleType, + FixedType, + FloatType, + IntegerType, + ListType, + MapType, + NullType, + StringType, + TimestampType, + TimeType, +) + + +@pytest.mark.asyncio +async def test_infer_column_with_table(construction_session: AsyncSession): + """ + Test getting the type of a column that has a table + """ + table = ast.Table( + ast.Name("orders", namespace=ast.Name("dbt.source.jaffle_shop")), + ) + ctx = CompileContext( + session=construction_session, + exception=DJException(), + ) + await table.compile(ctx) + assert table.columns[0].type == IntegerType() + assert table.columns[1].type == IntegerType() + assert table.columns[2].type == DateType() + assert table.columns[3].type == StringType() + + +def test_infer_values(): + """ + Test inferring types from values directly + """ + assert ast.String(value="foo").type == StringType() + assert ast.Number(value=10).type == IntegerType() + assert ast.Number(value=-10).type == IntegerType() + assert ast.Number(value=922337203685477).type == BigIntType() + assert ast.Number(value=-922337203685477).type == BigIntType() + assert ast.Number(value=3.4e39).type == DoubleType() + assert ast.Number(value=-3.4e39).type == DoubleType() + assert ast.Number(value=3.4e38).type == FloatType() + assert ast.Number(value=-3.4e38).type == FloatType() + + +def test_raise_on_invalid_infer_binary_op(): + """ + Test raising when trying to infer types from an invalid binary op + """ + with pytest.raises(DJParseException) as exc_info: + ast.BinaryOp( + op=ast.BinaryOpKind.Modulo, + left=ast.String(value="foo"), + right=ast.String(value="bar"), + ).type + + assert ( + "Incompatible types in binary operation foo % bar. " + "Got left string, right string." + ) in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_infer_column_with_an_aliased_table(construction_session: AsyncSession): + """ + Test getting the type of a column that has an aliased table + """ + ctx = CompileContext( + session=construction_session, + exception=DJException(), + ) + table = ast.Table( + ast.Name("orders", namespace=ast.Name("dbt.source.jaffle_shop")), + ) + alias = ast.Alias( + alias=ast.Name( + name="foo", + namespace=ast.Name( + name="a", + namespace=ast.Name( + name="b", + namespace=ast.Name("c"), + ), + ), + ), + child=table, + ) + await alias.compile(ctx) + + assert alias.child.columns[0].type == IntegerType() + assert alias.child.columns[1].type == IntegerType() + assert alias.child.columns[2].type == DateType() + assert alias.child.columns[3].type == StringType() + assert alias.child.columns[4].type == TimestampType() + + +def test_raising_when_table_has_no_dj_node(): + """ + Test raising when getting the type of a column that has a table with no DJ node + """ + table = ast.Table(ast.Name("orders")) + col = ast.Column(ast.Name("status"), _table=table) + + with pytest.raises(DJParseException) as exc_info: + col.type + + assert ("Cannot resolve type of column orders.status") in str(exc_info.value) + + +def test_raising_when_select_has_multiple_expressions_in_projection(): + """ + Test raising when a select has more than one in projection + """ + with pytest.raises(DJParseException) as exc_info: + parse("select 1, 2").select.type + + assert ("single expression in its projection") in str(exc_info.value) + + +def test_raising_when_between_different_types(): + """ + Test raising when a between has multiple types + """ + with pytest.raises(DJParseException) as exc_info: + parse( + "select 1 between 'hello' and TRUE", + ).select.type + + assert ("BETWEEN expects all elements to have the same type") in str(exc_info.value) + + +def test_raising_when_unop_bad_type(): + """ + Test raising when a unop gets a bad type + """ + with pytest.raises(DJParseException) as exc_info: + parse( + "select not 'hello'", + ).select.type + + assert ("Incompatible type in unary operation") in str(exc_info.value) + + +def test_raising_when_expression_has_no_parent(): + """ + Test raising when getting the type of a column that has no parent + """ + col = ast.Column(ast.Name("status"), _table=None) + + with pytest.raises(DJParseException) as exc_info: + col.type + + assert "Cannot resolve type of column status that has no parent" in str( + exc_info.value, + ) + + +@pytest.mark.asyncio +async def test_infer_map_subscripts(construction_session: AsyncSession): + """ + Test inferring map subscript types + """ + query = parse( + """ + SELECT + names_map["first"] as first_name, + names_map["last"] as last_name, + user_metadata["propensity_score"] as propensity_score, + user_metadata["propensity_score"]["weighted"] as weighted_propensity_score, + user_metadata["propensity_score"]["weighted"]["year"] as weighted_propensity_score_year + FROM basic.source.users + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + types = [ + StringType(), + StringType(), + MapType( + key_type=StringType(), + value_type=MapType(key_type=StringType(), value_type=FloatType()), + ), + MapType(key_type=StringType(), value_type=FloatType()), + FloatType(), + ] + assert types == [exp.type for exp in query.select.projection] # type: ignore + + +@pytest.mark.asyncio +async def test_infer_types_complicated(construction_session: AsyncSession): + """ + Test inferring complicated types + """ + query = parse( + """ + SELECT id+1-2/3*5%6&10|8^5, + CAST('2022-01-01T12:34:56Z' AS TIMESTAMP), + -- Raw('average({id})', 'INT', True), + -- Raw('aggregate(array(1, 2, {id}), 0, (acc, x) -> acc + x, acc -> acc * 10)', 'INT'), + -- Raw('NOW()', 'datetime'), + -- DATE_TRUNC('day', '2014-03-10'), + NOW(), + Coalesce(NULL, 5), + Coalesce(NULL), + NULL, + MAX(id) OVER + (PARTITION BY first_name ORDER BY last_name) + AS running_total, + MAX(id) OVER + (PARTITION BY first_name ORDER BY last_name) + AS running_total, + MIN(id) OVER + (PARTITION BY first_name ORDER BY last_name) + AS running_total, + AVG(id) OVER + (PARTITION BY first_name ORDER BY last_name) + AS running_total, + COUNT(id) OVER + (PARTITION BY first_name ORDER BY last_name) + AS running_total, + SUM(id) OVER + (PARTITION BY first_name ORDER BY last_name) + AS running_total, + NOT TRUE, + 10, + id>5, + id<5, + id>=5, + id<=5, + id BETWEEN 4 AND 5, + id IN (5, 5), + id NOT IN (3, 4), + id NOT IN (SELECT -5), + first_name LIKE 'Ca%', + id is null, + (id=5)=TRUE, + 'hello world', + first_name as fn, + last_name<>'yoyo' and last_name='yoyo' or last_name='yoyo', + last_name, + bizarre, + (select 5.0), + CASE WHEN first_name = last_name THEN COUNT(DISTINCT first_name) ELSE + COUNT(DISTINCT last_name) END + FROM ( + SELECT id, + first_name, + last_name<>'yoyo' and last_name='yoyo' or last_name='yoyo' as bizarre, + last_name + FROM dbt.source.jaffle_shop.customers + ) + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + types = [ + IntegerType(), + TimestampType(), + TimestampType(), + IntegerType(), + NullType(), + NullType(), + IntegerType(), + IntegerType(), + IntegerType(), + DoubleType(), + BigIntType(), + BigIntType(), + BooleanType(), + IntegerType(), + BooleanType(), + BooleanType(), + BooleanType(), + BooleanType(), + BooleanType(), + BooleanType(), + BooleanType(), + BooleanType(), + BooleanType(), + BooleanType(), + BooleanType(), + StringType(), + StringType(), + BooleanType(), + StringType(), + BooleanType(), + FloatType(), + BigIntType(), + ] + assert types == [exp.type for exp in query.select.projection] # type: ignore + + +@pytest.mark.asyncio +async def test_infer_bad_case_types(construction_session: AsyncSession): + """ + Test inferring mismatched case types. + """ + with pytest.raises(Exception) as excinfo: + query = parse( + """ + SELECT + CASE WHEN first_name = last_name THEN COUNT(DISTINCT first_name) ELSE last_name END + FROM dbt.source.jaffle_shop.customers + """, + ) + ctx = CompileContext( + session=construction_session, + exception=DJException(), + ) + await query.compile(ctx) + [ + exp.type # type: ignore + for exp in query.select.projection + ] + + assert str(excinfo.value) == "Not all the same type in CASE! Found: bigint, string" + + +@pytest.mark.asyncio +async def test_infer_types_avg(construction_session: AsyncSession): + """ + Test type inference of functions + """ + + query = parse( + """ + SELECT + AVG(id) OVER + (PARTITION BY first_name ORDER BY last_name), + AVG(CAST(id AS DECIMAL(8, 6))), + AVG(CAST(id AS INTERVAL DAY TO SECOND)), + STDDEV(id), + stddev_samp(id), + stddev_pop(id), + variance(id), + var_pop(id) + FROM dbt.source.jaffle_shop.customers + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + types = [ + DoubleType(), + DecimalType(12, 10), + DayTimeIntervalType(), + DoubleType(), + DoubleType(), + DoubleType(), + DoubleType(), + DoubleType(), + ] + assert types == [exp.type for exp in query.select.projection] # type: ignore + + +@pytest.mark.asyncio +async def test_infer_types_min_max_sum_ceil(construction_session: AsyncSession): + """ + Test type inference of functions + """ + + query = parse( + """ + SELECT + MIN(id) OVER + (PARTITION BY first_name ORDER BY last_name), + MAX(id) OVER + (PARTITION BY first_name ORDER BY last_name), + SUM(id) OVER + (PARTITION BY first_name ORDER BY last_name), + CEIL(id), + PERCENT_RANK(id) OVER (PARTITION BY id ORDER BY id) + FROM dbt.source.jaffle_shop.customers + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + types = [IntegerType(), IntegerType(), BigIntType(), BigIntType(), DoubleType()] + assert types == [exp.type for exp in query.select.projection] # type: ignore + + +@pytest.mark.asyncio +async def test_infer_types_count(construction_session: AsyncSession): + """ + Test type inference of functions + """ + + query = parse( + """ + SELECT + COUNT(id) OVER + (PARTITION BY first_name ORDER BY last_name), + COUNT(DISTINCT last_name) + FROM dbt.source.jaffle_shop.customers + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + types = [ + BigIntType(), + BigIntType(), + ] + assert types == [exp.type for exp in query.select.projection] # type: ignore + + +@pytest.mark.asyncio +async def test_infer_types_coalesce(construction_session: AsyncSession): + """ + Test type inference of functions + """ + + query = parse( + """ + SELECT + COALESCE(5, NULL), + COALESCE("random", NULL) + FROM dbt.source.jaffle_shop.customers + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + types = [ + IntegerType(), + StringType(), + ] + assert types == [exp.type for exp in query.select.projection] # type: ignore + + +@pytest.mark.asyncio +async def test_infer_types_array_map(construction_session: AsyncSession): + """ + Test type inference for arrays and maps + """ + + query = parse( + """ + SELECT + ARRAY(5, 6, 7, 8), + MAP(1, 'a', 2, 'b', 3, 'c'), + MAP(1.0, 'a', 2.0, 'b', 3.0, 'c'), + MAP(CAST(1.0 AS DOUBLE), 'a', CAST(2.0 AS DOUBLE), 'b', CAST(3.0 AS DOUBLE), 'c') + FROM dbt.source.jaffle_shop.customers + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + types = [ + ListType(IntegerType()), + MapType(IntegerType(), StringType()), + MapType(FloatType(), StringType()), + MapType(DoubleType(), StringType()), + ] + assert types == [exp.type for exp in query.select.projection] # type: ignore + + query = parse( + """ + SELECT + MAP(1, 'a', 2, 3, 'c') + FROM dbt.source.jaffle_shop.customers + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + assert query.select.projection[0].type == MapType( # type: ignore + key_type=IntegerType(), + value_type=StringType(), + ) + + query = parse( + """ + SELECT + ARRAY(1, 'a') + FROM dbt.source.jaffle_shop.customers + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + with pytest.raises(DJParseException) as exc_info: + query.select.projection[0].type # type: ignore + assert "Multiple types int, string passed to array" in str(exc_info) + + +@pytest.mark.asyncio +async def test_infer_types_if(construction_session: AsyncSession): + """ + Test type inference of IF + """ + query = parse( + """ + SELECT + IF(1=2, 10, 20), + IF(1=2, 'random', 20), + IFNULL(1, 2, 3) + FROM dbt.source.jaffle_shop.customers + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + assert query.select.projection[0].type == IntegerType() # type: ignore + with pytest.raises(DJException) as exc_info: + query.select.projection[1].type # type: ignore + assert ( + "The then result and else result must match in type! Got string and int" + in str(exc_info) + ) + assert query.select.projection[2].type == IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_infer_types_exp(construction_session: AsyncSession): + """ + Test type inference of math functions + """ + query = parse( + """ + SELECT + EXP(2), + FLOOR(22.1), + LENGTH('blah'), + LEVENSHTEIN('a', 'b'), + LN(5), + LOG(10, 100), + LOG2(2), + LOG10(100), + POW(1, 2), + POWER(1, 2), + ROUND(1.2, 0), + ROUND(1.2, -1), + ROUND(CAST(1.233 AS DOUBLE), 1), + ROUND(CAST(1.233 AS DECIMAL(8, 6)), 20), + CEIL(CAST(1.233 AS DECIMAL(8, 6))), + SQRT(12) + FROM dbt.source.jaffle_shop.customers + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + types = [ + DoubleType(), + BigIntType(), + IntegerType(), + IntegerType(), + DoubleType(), + DoubleType(), + DoubleType(), + DoubleType(), + DoubleType(), + DoubleType(), + IntegerType(), + FloatType(), + DoubleType(), + DecimalType(precision=9, scale=6), + DecimalType(precision=3, scale=0), + DoubleType(), + ] + assert types == [exp.type for exp in query.select.projection] # type: ignore + assert query.select.projection[4].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + assert query.select.projection[5].function().dialects == [Dialect.SPARK] # type: ignore + assert query.select.projection[6].function().dialects == [Dialect.SPARK] # type: ignore + assert query.select.projection[7].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_infer_types_str(construction_session: AsyncSession): + """ + Test type inference of EXP + """ + query = parse( + """ + SELECT + LOWER('Extra'), + SUBSTRING('e14', 1, 1), + SUBSTRING('e14', 1, 1) + FROM dbt.source.jaffle_shop.customers + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + types = [ + StringType(), + StringType(), + StringType(), + ] + assert types == [exp.type for exp in query.select.projection] # type: ignore + + +def test_column_type_validation(): + """ + Test type inference of EXP + """ + with pytest.raises(DJException) as exc_info: + ColumnType.validate("decimal") + assert "DJ does not recognize the type `decimal`" in str(exc_info) + assert ColumnType.validate("decimal(10, 8)") == DecimalType(10, 8) + + assert ColumnType.validate("fixed(2)") == FixedType(2) + assert ColumnType.validate("map") == MapType( + StringType(), + StringType(), + ) + assert ColumnType.validate("array") == ListType(IntegerType()) + + +@pytest.mark.asyncio +async def test_infer_types_datetime(construction_session: AsyncSession): + """ + Test type inference of functions + """ + + query = parse( + """ + SELECT + CURRENT_DATE(), + CURRENT_TIME(), + CURRENT_TIMESTAMP(), + + NOW(), + + DATE_ADD('2020-01-01', 10), + DATE_ADD(CURRENT_DATE(), 10), + DATE_SUB('2020-01-01', 10), + DATE_SUB(CURRENT_DATE(), 10), + + DATEDIFF('2020-01-01', '2021-01-01'), + DATEDIFF(CURRENT_DATE(), CURRENT_DATE()), + + + EXTRACT(YEAR FROM '2020-01-01 00:00:00'), + EXTRACT(SECOND FROM '2020-01-01 00:00:00'), + + DAY("2022-01-01"), + MONTH("2022-01-01"), + WEEK("2022-01-01"), + YEAR("2022-01-01") + FROM dbt.source.jaffle_shop.customers + """, + ) + exc = DJException() + ctx = CompileContext(session=construction_session, exception=exc) + await query.compile(ctx) + types = [ + DateType(), + TimeType(), + TimestampType(), + TimestampType(), + DateType(), + DateType(), + DateType(), + DateType(), + IntegerType(), + IntegerType(), + IntegerType(), + DecimalType(precision=8, scale=6), + IntegerType(), + BigIntType(), + BigIntType(), + BigIntType(), + ] + assert types == [exp.type for exp in query.select.projection] # type: ignore diff --git a/datajunction-server/tests/construction/utils_test.py b/datajunction-server/tests/construction/utils_test.py new file mode 100644 index 000000000..a4cd5a3dc --- /dev/null +++ b/datajunction-server/tests/construction/utils_test.py @@ -0,0 +1,47 @@ +""" +Tests for building nodes and extracting dependencies +""" + +import pytest +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.construction.utils import get_dj_node +from datajunction_server.errors import DJErrorException +from datajunction_server.models.node_type import NodeType + + +@pytest.mark.asyncio +async def test_get_dj_node_raise_unknown_node_exception(session: AsyncSession): + """ + Test raising an unknown node exception when calling get_dj_node + """ + with pytest.raises(DJErrorException) as exc_info: + await get_dj_node(session, "foobar") + + assert "No node" in str(exc_info.value) + + with pytest.raises(DJErrorException) as exc_info: + await get_dj_node( + session, + "foobar", + kinds={NodeType.METRIC, NodeType.DIMENSION}, + ) + + assert "dimension" in str(exc_info.value) + assert "metric" in str(exc_info.value) + assert "source" not in str(exc_info.value) + assert "transform" not in str(exc_info.value) + + with pytest.raises(DJErrorException) as exc_info: + # test that the event_type raises because it's a dimension and not a transform + await get_dj_node(session, "event_type", kinds={NodeType.TRANSFORM}) + + assert "No node `event_type` exists of kind transform" in str(exc_info.value) + + # test that the event_type raises because it's a dimension and not a transform + with pytest.raises(DJErrorException) as exc_info: + await get_dj_node(session, "event_type", kinds={NodeType.TRANSFORM}) + + assert "No node `event_type` exists of kind transform" in str( + exc_info.value, + ) diff --git a/datajunction-server/tests/default.duckdb b/datajunction-server/tests/default.duckdb new file mode 100644 index 000000000..560fc4f37 Binary files /dev/null and b/datajunction-server/tests/default.duckdb differ diff --git a/datajunction-server/tests/duckdb.sql b/datajunction-server/tests/duckdb.sql new file mode 100644 index 000000000..eee52213f --- /dev/null +++ b/datajunction-server/tests/duckdb.sql @@ -0,0 +1,584 @@ +CREATE SCHEMA roads; + +CREATE TABLE roads.repair_type ( + repair_type_id int, + repair_type_name string, + contractor_id string +); +INSERT INTO roads.repair_type VALUES +(1, 'Asphalt Overlay', 'Asphalt overlays restore roads to a smooth condition. This resurfacing uses the deteriorating asphalt as a base for which the new layer is added on top of, instead of tearing up the worsening one.'), +(2, 'Patching', 'Patching is the process of filling potholes or excavated areas in the asphalt pavement. Quick repair of potholes or other pavement disintegration helps control further deterioration and expensive repair of the pavement. Without timely patching, water can enter the sub-grade and cause larger and more serious pavement failures.'), +(3, 'Reshaping', 'This is necessary when a road surface it too damaged to be smoothed. Using a grader blade and scarifying if necessary, you rework the gravel sub-base to eliminate large potholes and rebuild a flattened crown.'), +(4, 'Slab Replacement', 'This refers to replacing sections of paved roads. It is a good option for when slabs are chipped, cracked, or uneven, and mitigates the need to replace the entire road when just a small section is damaged.'), +(5, 'Smoothing', 'This is when you lightly rework the gravel of a road without digging in too far to the sub-base. Typically, a motor grader is used in this operation with an attached blade. Smoothing is done when the road has minor damage or is just worn down a bit from use.'), +(6, 'Reconstruction', 'When roads have deteriorated to a point that it is no longer cost-effective to maintain, the entire street or road needs to be rebuilt. Typically, this work is done in phases to limit traffic restrictions. As part of reconstruction, the street may be realigned to improve safety or operations, grading may be changed to improve storm water flow, underground utilities may be added, upgraded or relocated, traffic signals and street lights may be relocated, and street trees and pedestrian ramps may be added.'); + +CREATE TABLE roads.municipality ( + municipality_id string, + contact_name string, + contact_title string, + local_region string, + state_id int, + phone string +); +INSERT INTO roads.municipality VALUES +('New York', 'Alexander Wilkinson', 'Assistant City Clerk', 'Manhattan', 33, '202-291-2922'), +('Los Angeles', 'Hugh Moser', 'Administrative Assistant', 'Santa Monica', 5, '808-211-2323'), +('Chicago', 'Phillip Bradshaw', 'Director of Community Engagement', 'West Ridge', 14, '425-132-3421'), +('Houston', 'Leo Ackerman', 'Municipal Roads Specialist', 'The Woodlands', 44, '413-435-8641'), +('Phoenix', 'Jessie Paul', 'Director of Finance and Administration', 'Old Town Scottsdale', 3, '321-425-5427'), +('Philadelphia', 'Willie Chaney', 'Municipal Manager', 'Center City', 39, '212-213-5361'), +('San Antonio', 'Chester Lyon', 'Treasurer', 'Alamo Heights', 44, '252-216-6938'), +('San Diego', 'Ralph Helms', 'Senior Electrical Project Manager', 'Del Mar', 5, '491-813-2417'), +('Dallas', 'Virgil Craft', 'Assistant Assessor (Town/Municipality)', 'Deep Ellum', 44, '414-563-7894'), +('San Jose', 'Charles Carney', 'Municipal Accounting Manager', 'Santana Row', 5, '408-313-0698'); + +CREATE TABLE roads.hard_hats ( + hard_hat_id int, + last_name string, + first_name string, + title string, + birth_date date, + hire_date date, + address string, + city string, + state string, + postal_code string, + country string, + manager int, + contractor_id int +); +INSERT INTO roads.hard_hats VALUES +(1, 'Brian', 'Perkins', 'Construction Laborer', cast('1978-11-28' as date), cast('2009-02-06' as date), '4 Jennings Ave.', 'Jersey City', 'NJ', '37421', 'USA', 9, 1), +(2, 'Nicholas', 'Massey', 'Carpenter', cast('1993-02-19' as date), cast('2003-04-14' as date), '9373 Southampton Street', 'Middletown', 'CT', '27292', 'USA', 9, 1), +(3, 'Cathy', 'Best', 'Framer', cast('1994-08-30' as date), cast('1990-07-02' as date), '4 Hillside Street', 'Billerica', 'MA', '13440', 'USA', 9, 2), +(4, 'Melanie', 'Stafford', 'Construction Manager', cast('1966-03-19' as date), cast('2003-02-02' as date), '77 Studebaker Lane', 'Southampton', 'PA', '71730', 'USA', 9, 2), +(5, 'Donna', 'Riley', 'Pre-construction Manager', cast('1983-03-14' as date), cast('2012-01-13' as date), '82 Taylor Drive', 'Southgate', 'MI', '33125', 'USA', 9, 4), +(6, 'Alfred', 'Clarke', 'Construction Superintendent', cast('1979-01-12' as date), cast('2013-10-17' as date), '7729 Catherine Street', 'Powder Springs', 'GA', '42001', 'USA', 9, 2), +(7, 'William', 'Boone', 'Construction Laborer', cast('1970-02-28' as date), cast('2013-01-02' as date), '1 Border St.', 'Niagara Falls', 'NY', '14304', 'USA', 9, 4), +(8, 'Luka', 'Henderson', 'Construction Laborer', cast('1988-12-09' as date), cast('2013-03-05' as date), '794 S. Chapel Ave.', 'Phoenix', 'AZ', '85021', 'USA', 9, 1), +(9, 'Patrick', 'Ziegler', 'Construction Laborer', cast('1976-11-27' as date), cast('2020-11-15' as date), '321 Gainsway Circle', 'Muskogee', 'OK', '74403', 'USA', 9, 3); + +CREATE TABLE roads.hard_hat_state ( + hard_hat_id int, + state_id int +); +INSERT INTO roads.hard_hat_state VALUES +(1, 2), +(2, 32), +(3, 28), +(4, 12), +(5, 5), +(6, 3), +(7, 16), +(8, 32), +(9, 41); + +CREATE TABLE roads.repair_order_details ( + repair_order_id int, + repair_type_id int, + price real NOT NULL, + quantity int, + discount real NOT NULL +); +INSERT INTO roads.repair_order_details VALUES +(10001, 1, 63708, 1, 0.05), +(10002, 4, 67253, 1, 0.05), +(10003, 2, 66808, 1, 0.05), +(10004, 4, 18497, 1, 0.05), +(10005, 7, 76463, 1, 0.05), +(10006, 4, 87858, 1, 0.05), +(10007, 1, 63918, 1, 0.05), +(10008, 6, 21083, 1, 0.05), +(10009, 3, 74555, 1, 0.05), +(10010, 5, 27222, 1, 0.05), +(10011, 5, 73600, 1, 0.05), +(10012, 3, 54901, 1, 0.01), +(10013, 5, 51594, 1, 0.01), +(10014, 1, 65114, 1, 0.01), +(10015, 1, 48919, 1, 0.01), +(10016, 3, 70418, 1, 0.01), +(10017, 1, 29684, 1, 0.01), +(10018, 2, 62928, 1, 0.01), +(10019, 2, 97916, 1, 0.01), +(10020, 5, 44120, 1, 0.01), +(10021, 1, 53374, 1, 0.01), +(10022, 2, 87289, 1, 0.01), +(10023, 2, 92366, 1, 0.01), +(10024, 2, 47857, 1, 0.01), +(10025, 1, 68745, 1, 0.01); + +CREATE TABLE roads.repair_orders ( + repair_order_id int, + municipality_id string, + hard_hat_id int, + order_date date, + required_date date, + dispatched_date date, + dispatcher_id int +); +INSERT INTO roads.repair_orders VALUES +(10001, 'New York', 1, cast('2007-07-04' as date), cast('2009-07-18' as date), cast('2007-12-01' as date), 3), +(10002, 'New York', 3, cast('2007-07-05' as date), cast('2009-08-28' as date), cast('2007-12-01' as date), 1), +(10003, 'New York', 5, cast('2007-07-08' as date), cast('2009-08-12' as date), cast('2007-12-01' as date), 2), +(10004, 'Dallas', 1, cast('2007-07-08' as date), cast('2009-08-01' as date), cast('2007-12-01' as date), 1), +(10005, 'San Antonio', 8, cast('2007-07-09' as date), cast('2009-08-01' as date), cast('2007-12-01' as date), 2), +(10006, 'New York', 3, cast('2007-07-10' as date), cast('2009-08-01' as date), cast('2007-12-01' as date), 2), +(10007, 'Philadelphia', 4, cast('2007-04-21' as date), cast('2009-08-08' as date), cast('2007-12-01' as date), 2), +(10008, 'Philadelphia', 5, cast('2007-04-22' as date), cast('2009-08-09' as date), cast('2007-12-01' as date), 3), +(10009, 'Philadelphia', 3, cast('2007-04-25' as date), cast('2009-08-12' as date), cast('2007-12-01' as date), 2), +(10010, 'Philadelphia', 4, cast('2007-04-26' as date), cast('2009-08-13' as date), cast('2007-12-01' as date), 3), +(10011, 'Philadelphia', 4, cast('2007-04-27' as date), cast('2009-08-14' as date), cast('2007-12-01' as date), 1), +(10012, 'Philadelphia', 8, cast('2007-04-28' as date), cast('2009-08-15' as date), cast('2007-12-01' as date), 3), +(10013, 'Philadelphia', 4, cast('2007-04-29' as date), cast('2009-08-16' as date), cast('2007-12-01' as date), 1), +(10014, 'Philadelphia', 6, cast('2007-04-29' as date), cast('2009-08-16' as date), cast('2007-12-01' as date), 2), +(10015, 'Philadelphia', 2, cast('2007-04-12' as date), cast('2009-08-19' as date), cast('2007-12-01' as date), 3), +(10016, 'Philadelphia', 9, cast('2007-04-13' as date), cast('2009-08-20' as date), cast('2007-12-01' as date), 3), +(10017, 'Philadelphia', 2, cast('2007-04-14' as date), cast('2009-08-21' as date), cast('2007-12-01' as date), 3), +(10018, 'Philadelphia', 6, cast('2007-04-15' as date), cast('2009-08-22' as date), cast('2007-12-01' as date), 1), +(10019, 'Philadelphia', 5, cast('2007-05-16' as date), cast('2009-09-06' as date), cast('2007-12-01' as date), 3), +(10020, 'Philadelphia', 1, cast('2007-05-19' as date), cast('2009-08-26' as date), cast('2007-12-01' as date), 1), +(10021, 'Philadelphia', 7, cast('2007-05-10' as date), cast('2009-08-27' as date), cast('2007-12-01' as date), 3), +(10022, 'Philadelphia', 5, cast('2007-05-11' as date), cast('2009-08-14' as date), cast('2007-12-01' as date), 1), +(10023, 'Philadelphia', 1, cast('2007-05-11' as date), cast('2009-08-29' as date), cast('2007-12-01' as date), 1), +(10024, 'Philadelphia', 5, cast('2007-05-11' as date), cast('2009-08-29' as date), cast('2007-12-01' as date), 2), +(10025, 'Philadelphia', 6, cast('2007-05-12' as date), cast('2009-08-30' as date), cast('2007-12-01' as date), 2); + +CREATE TABLE roads.dispatchers ( + dispatcher_id int, + company_name string, + phone string +); +INSERT INTO roads.dispatchers VALUES +(1, 'Pothole Pete', '(111) 111-1111'), +(2, 'Asphalts R Us', '(222) 222-2222'), +(3, 'Federal Roads Group', '(333) 333-3333'), +(4, 'Local Patchers', '1-800-888-8888'), +(5, 'Gravel INC', '1-800-000-0000'), +(6, 'DJ Developers', '1-111-111-1111'); + +CREATE TABLE roads.contractors ( + contractor_id int, + company_name string, + contact_name string, + contact_title string, + address string, + city string, + state string, + postal_code string, + country string, + phone string +); +INSERT INTO roads.contractors VALUES +(1, 'You Need Em We Find Em', 'Max Potter', 'Assistant Director', '4 Plumb Branch Lane', 'Goshen', 'IN', '46526', 'USA', '(111) 111-1111'), +(2, 'Call Forwarding', 'Sylvester English', 'Administrator', '9650 Mill Lane', 'Raeford', 'NC', '28376', 'USA', '(222) 222-2222'), +(3, 'The Connect', 'Paul Raymond', 'Administrator', '7587 Myrtle Ave.', 'Chaska', 'MN', '55318', 'USA', '(333) 333-3333'); + +CREATE TABLE roads.us_region ( + us_region_id int, + us_region_description string +); +INSERT INTO roads.us_region VALUES +(1, 'Eastern'), +(2, 'Western'), +(3, 'Northern'), +(4, 'Southern'); + +CREATE TABLE roads.us_states ( + state_id int, + state_name string, + state_abbr string, + state_region string +); +INSERT INTO roads.us_states VALUES +(1, 'Alabama', 'AL', 'Southern'), +(2, 'Alaska', 'AK', 'Northern'), +(3, 'Arizona', 'AZ', 'Western'), +(4, 'Arkansas', 'AR', 'Southern'), +(5, 'California', 'CA', 'Western'), +(6, 'Colorado', 'CO', 'Western'), +(7, 'Connecticut', 'CT', 'Eastern'), +(8, 'Delaware', 'DE', 'Eastern'), +(9, 'District of Columbia', 'DC', 'Eastern'), +(10, 'Florida', 'FL', 'Southern'), +(11, 'Georgia', 'GA', 'Southern'), +(12, 'Hawaii', 'HI', 'Western'), +(13, 'Idaho', 'ID', 'Western'), +(14, 'Illinois', 'IL', 'Western'), +(15, 'Indiana', 'IN', 'Western'), +(16, 'Iowa', 'IO', 'Western'), +(17, 'Kansas', 'KS', 'Western'), +(18, 'Kentucky', 'KY', 'Southern'), +(19, 'Louisiana', 'LA', 'Southern'), +(20, 'Maine', 'ME', 'Northern'), +(21, 'Maryland', 'MD', 'Eastern'), +(22, 'Massachusetts', 'MA', 'Northern'), +(23, 'Michigan', 'MI', 'Northern'), +(24, 'Minnesota', 'MN', 'Northern'), +(25, 'Mississippi', 'MS', 'Southern'), +(26, 'Missouri', 'MO', 'Southern'), +(27, 'Montana', 'MT', 'Western'), +(28, 'Nebraska', 'NE', 'Western'), +(29, 'Nevada', 'NV', 'Western'), +(30, 'New Hampshire', 'NH', 'Eastern'), +(31, 'New Jersey', 'NJ', 'Eastern'), +(32, 'New Mexico', 'NM', 'Western'), +(33, 'New York', 'NY', 'Eastern'), +(34, 'North Carolina', 'NC', 'Eastern'), +(35, 'North Dakota', 'ND', 'Western'), +(36, 'Ohio', 'OH', 'Western'), +(37, 'Oklahoma', 'OK', 'Western'), +(38, 'Oregon', 'OR', 'Western'), +(39, 'Pennsylvania', 'PA', 'Eastern'), +(40, 'Rhode Island', 'RI', 'Eastern'), +(41, 'South Carolina', 'SC', 'Eastern'), +(42, 'South Dakota', 'SD', 'Western'), +(43, 'Tennessee', 'TN', 'Western'), +(44, 'Texas', 'TX', 'Western'), +(45, 'Utah', 'UT', 'Western'), +(46, 'Vermont', 'VT', 'Eastern'), +(47, 'Virginia', 'VA', 'Eastern'), +(48, 'Washington', 'WA', 'Western'), +(49, 'West Virginia', 'WV', 'Southern'), +(50, 'Wisconsin', 'WI', 'Western'), +(51, 'Wyoming', 'WY', 'Western'); + +CREATE TABLE roads.municipality_municipality_type ( + municipality_id string, + municipality_type_id string +); +INSERT INTO roads.municipality_municipality_type VALUES +('New York', 'A'), +('Los Angeles', 'B'), +('Chicago', 'B'), +('Houston', 'A'), +('Phoenix', 'A'), +('Philadelphia', 'B'), +('San Antonio', 'A'), +('San Diego', 'B'), +('Dallas', 'A'), +('San Jose', 'B'); + +CREATE TABLE roads.municipality_type ( + municipality_type_id string, + municipality_type_desc string +); +INSERT INTO roads.municipality_type VALUES +('A', 'Primary'), +('A', 'Secondary'); + +CREATE SCHEMA campaigns; + +CREATE TABLE campaigns.campaign_views ( + campaign_id int, + campaign_name string, + created_at timestamp, + views int +); + +INSERT INTO campaigns.campaign_views VALUES +(1, 'Clean Up Your Yard Marketing', epoch_ms(1526290800000), 120), +(2, 'Summer Sale Campaign', epoch_ms(1456472400000), 500), +(3, 'New Product Launch', epoch_ms(1341392400000), 250), +(4, 'Holiday Special Offers', epoch_ms(1451600400000), 800), +(5, 'Spring Cleaning Deals', epoch_ms(1472667600000), 300), +(6, 'Back to School Promotion', epoch_ms(1293848400000), 150), +(7, 'Winter Clearance Sale', epoch_ms(1512123600000), 900), +(8, 'Outdoor Adventure Campaign', epoch_ms(1288779600000), 400), +(9, 'Health and Wellness Expo', epoch_ms(1335795600000), 200), +(10, 'Summer Vacation Deals', epoch_ms(1462069200000), 600), +(11, 'Home Renovation Offers', epoch_ms(1396314000000), 350), +(12, 'Fashion Show Sponsorship', epoch_ms(1248478800000), 750), +(13, 'Charity Fundraising Drive', epoch_ms(1363563600000), 420), +(14, 'Tech Gadgets Showcase', epoch_ms(1433106000000), 230), +(15, 'Gourmet Food Festival', epoch_ms(1308032400000), 150), +(16, 'Music Concert Ticket Sales', epoch_ms(1380584400000), 550), +(17, 'Book Fair and Author Meet', epoch_ms(1262274000000), 180), +(18, 'Fitness Challenge Event', epoch_ms(1502053200000), 670), +(19, 'Pet Adoption Awareness', epoch_ms(1319638800000), 290), +(20, 'Art Exhibition Opening', epoch_ms(1409422800000), 390), +(21, 'Wedding Planning Expo', epoch_ms(1274245200000), 420), +(22, 'Sports Equipment Sale', epoch_ms(1419987600000), 780), +(23, 'Tech Startup Conference', epoch_ms(1370302800000), 210), +(24, 'Environmental Awareness', epoch_ms(1328053200000), 840), +(25, 'Travel and Adventure Expo', epoch_ms(1366832400000), 350), +(26, 'Automobile Showroom Launch', epoch_ms(1425162000000), 420), +(27, 'Film Festival Promotion', epoch_ms(1356997200000), 590), +(28, 'Summer Camp Enrollment', epoch_ms(1388778000000), 240), +(29, 'Online Shopping Festival', epoch_ms(1251776400000), 430), +(30, 'Healthcare Symposium', epoch_ms(1443642000000), 980); + +CREATE TABLE campaigns.email ( + campaign_id int, + email_address string, + email_id int, + last_contacted timestamp, + views int +); + +INSERT INTO campaigns.email VALUES +(1, 'mark@fakedomain.com', 1, to_timestamp(1451606400000), 2), +(1, 'john@fakedomain.com', 2, to_timestamp(1454284800000), 7), +(1, 'isaiah@fakedomain.com', 3, to_timestamp(1456790400000), 10), +(1, 'ruby@fakedomain.com', 4, to_timestamp(1459468800000), 5), +(1, 'oliver@fakedomain.com', 5, to_timestamp(1462060800000), 9), +(1, 'emma@fakedomain.com', 6, to_timestamp(1464739200000), 12), +(1, 'liam@fakedomain.com', 7, to_timestamp(1467331200000), 4), +(1, 'ava@fakedomain.com', 8, to_timestamp(1470009600000), 11), +(1, 'noah@fakedomain.com', 9, to_timestamp(1472688000000), 7), +(1, 'isabella@fakedomain.com', 10, to_timestamp(1475280000000), 3), +(2, 'sophia@fakedomain.com', 1, to_timestamp(1477958400000), 8), +(2, 'mason@fakedomain.com', 2, to_timestamp(1480550400000), 6), +(2, 'camila@fakedomain.com', 3, to_timestamp(1483228800000), 11), +(2, 'henry@fakedomain.com', 4, to_timestamp(1485907200000), 13), +(2, 'mia@fakedomain.com', 5, to_timestamp(1488326400000), 6), +(2, 'ethan@fakedomain.com', 6, to_timestamp(1491004800000), 9), +(2, 'lucas@fakedomain.com', 7, to_timestamp(1493596800000), 5), +(2, 'harper@fakedomain.com', 8, to_timestamp(1496275200000), 14), +(2, 'alexander@fakedomain.com', 9, to_timestamp(1498867200000), 12), +(2, 'abigail@fakedomain.com', 10, to_timestamp(1501545600000), 2), +(3, 'james@fakedomain.com', 1, to_timestamp(1504224000000), 7), +(3, 'amelia@fakedomain.com', 2, to_timestamp(1506816000000), 4), +(3, 'benjamin@fakedomain.com', 3, to_timestamp(1509494400000), 10), +(3, 'evelyn@fakedomain.com', 4, to_timestamp(1512086400000), 6), +(3, 'michael@fakedomain.com', 5, to_timestamp(1514764800000), 3), +(3, 'charlotte@fakedomain.com', 6, to_timestamp(1517443200000), 8), +(3, 'daniel@fakedomain.com', 7, to_timestamp(1520035200000), 12), +(3, 'harper@fakedomain.com', 8, to_timestamp(1522713600000), 9), +(3, 'lucas@fakedomain.com', 9, to_timestamp(1525305600000), 5), +(3, 'isabella@fakedomain.com', 10, to_timestamp(1527984000000), 11); + +CREATE TABLE campaigns.sms ( + campaign_id int, + phone_number string, + message string, + last_contacted timestamp, +); + +INSERT INTO campaigns.sms VALUES +(11, '(215) 111-1111', 'Click here to redeem 20% off!: http://smalllink', epoch_ms(1459468800000)), +(12, '(215) 222-2222', 'Interested in new lawn equipment?: http://smalllink', epoch_ms(1459468800001)), +(12, '(215) 333-3333', 'These sales will not last!: http://smalllink', epoch_ms(1459468800002)), +(13, '(215) 444-4444', 'Fall is here, enjoy half-off!: http://smalllink', epoch_ms(1459468800003)), +(14, '(215) 555-5555', 'Get the best deals today!: http://smalllink', epoch_ms(1459468800004)), +(15, '(215) 666-6666', 'Limited time offer - 30% off!: http://smalllink', epoch_ms(1459468800005)), +(16, '(215) 777-7777', 'Do not miss out on our sale!: http://smalllink', epoch_ms(1459468800006)), +(17, '(215) 888-8888', 'Exclusive discount for you!: http://smalllink', epoch_ms(1459468800007)), +(18, '(215) 999-9999', 'Shop now and save big!: http://smalllink', epoch_ms(1459468800008)), +(19, '(215) 000-0000', 'Huge clearance sale - up to 50% off!: http://smalllink', epoch_ms(1459468800009)), +(10, '(215) 123-4567', 'New arrivals with special discounts!: http://smalllink', epoch_ms(1459468800010)), +(11, '(215) 234-5678', 'Save money with our promo codes!: http://smalllink', epoch_ms(1459468800011)), +(12, '(215) 345-6789', 'Enjoy free shipping on all orders!: http://smalllink', epoch_ms(1459468800012)), +(13, '(215) 456-7890', 'Limited stock available - act fast!: http://smalllink', epoch_ms(1459468800013)), +(14, '(215) 567-8901', 'Upgrade your home with our deals!: http://smalllink', epoch_ms(1459468800014)), +(15, '(215) 678-9012', 'Special discount for loyal customers!: http://smalllink', epoch_ms(1459468800015)), +(16, '(215) 789-0123', 'Do not miss our summer sale!: http://smalllink', epoch_ms(1459468800016)), +(17, '(215) 890-1234', 'Exclusive offer - limited time only!: http://smalllink', epoch_ms(1459468800017)), +(18, '(215) 901-2345', 'Shop now and get a free gift!: http://smalllink', epoch_ms(1459468800018)), +(19, '(215) 012-3456', 'Big discounts on popular brands!: http://smalllink', epoch_ms(1459468800019)), +(10, '(215) 987-6543', 'Save up to 70% off on selected items!: http://smalllink', epoch_ms(1459468800020)), +(11, '(215) 876-5432', 'Limited time offer - buy one, get one free!: http://smalllink', epoch_ms(1459468800021)), +(12, '(215) 765-4321', 'Great deals for your next vacation!: http://smalllink', epoch_ms(1459468800022)), +(13, '(215) 654-3210', 'Get ready for the holiday season with our discounts!: http://smalllink', epoch_ms(1459468800023)), +(14, '(215) 543-2109', 'Save on electronics and gadgets!: http://smalllink', epoch_ms(1459468800024)), +(15, '(215) 432-1098', 'Limited stock available - you do not want to miss out!: http://smalllink', epoch_ms(1459468800025)), +(16, '(215) 321-0987', 'Shop now and enjoy free returns!: http://smalllink', epoch_ms(1459468800026)), +(17, '(215) 210-9876', 'Exclusive discount for online orders!: http://smalllink', epoch_ms(1459468800027)), +(18, '(215) 109-8765', 'Upgrade your wardrobe with our sale!: http://smalllink', epoch_ms(1459468800028)), +(19, '(215) 098-7654', 'Will you miss our clearance sale??: http://smalllink', epoch_ms(1459468800029)), +(10, '(215) 987-6543', 'Get the best deals for your home!: http://smalllink', epoch_ms(1459468800030)), +(12, '(215) 222-2222', 'New collection now available!: http://smalllink', epoch_ms(1459468800002)), +(13, '(215) 333-3333', 'Limited stock - dont miss out!: http://smalllink', epoch_ms(1459468800003)), +(14, '(215) 444-4444', 'Free shipping on all orders!: http://smalllink', epoch_ms(1459468800004)), +(15, '(215) 555-5555', 'Sign up for exclusive offers!: http://smalllink', epoch_ms(1459468800005)), +(16, '(215) 666-6666', 'Get ready for summer with our new arrivals!: http://smalllink', epoch_ms(1459468800006)), +(17, '(215) 777-7777', 'Limited time sale - up to 60% off!: http://smalllink', epoch_ms(1459468800007)), +(10, '(215) 111-2222', 'Shop now and receive a gift card!: http://smalllink', epoch_ms(1459468800010)), +(11, '(215) 222-3333', 'Final clearance - last chance to save!: http://smalllink', epoch_ms(1459468800011)), +(12, '(215) 333-4444', 'Get the latest fashion trends at discounted prices!: http://smalllink', epoch_ms(1459468800012)), +(13, '(215) 444-5555', 'Upgrade your electronics with our special offers!: http://smalllink', epoch_ms(1459468800013)), +(14, '(215) 555-6666', 'Big savings on home appliances!: http://smalllink', epoch_ms(1459468800014)), +(15, '(215) 666-7777', 'Limited stock - shop now before its gone!: http://smalllink', epoch_ms(1459468800015)), +(16, '(215) 777-8888', 'Get the best deals on beauty products!: http://smalllink', epoch_ms(1459468800016)), +(11, '(215) 012-3456', 'Dont miss our summer sale!: http://smalllink', epoch_ms(1459468800021)), +(12, '(215) 123-4567', 'Exclusive offer for our loyal customers!: http://smalllink', epoch_ms(1459468800022)), +(13, '(215) 234-5678', 'Shop now and enjoy free returns!: http://smalllink', epoch_ms(1459468800023)), +(14, '(215) 345-6789', 'Upgrade your gaming setup with our deals!: http://smalllink', epoch_ms(1459468800024)), +(15, '(215) 456-7890', 'Save on outdoor essentials for your next adventure!: http://smalllink', epoch_ms(1459468800025)), +(16, '(215) 567-8901', 'Limited time offer - buy one, get one free!: http://smalllink', epoch_ms(1459468800026)), +(19, '(215) 098-7654', 'Discover the latest tech gadgets at discounted prices!: http://smalllink', epoch_ms(1459468800029)), +(12, '(215) 876-5432', 'Get the perfect gift for your loved ones!: http://smalllink', epoch_ms(1459468800031)); + +CREATE TABLE campaigns.commercial ( + campaign_id int, + station string, + last_played timestamp, +); + +INSERT INTO campaigns.commercial VALUES +(23, 'Montgomery Oldies Station', epoch_ms(1688879124599)), +(21, 'Lithonia Jazz & Blues', epoch_ms(1688879124599)), +(20, 'Manchester 90s R&B', epoch_ms(1688879124599)), +(22, 'Los Angeles Rock Hits', epoch_ms(1688879124599)), +(23, 'Chicago Pop Mix', epoch_ms(1688879124599)), +(24, 'Houston Country Station', epoch_ms(1688879124599)), +(25, 'New York Hip Hop', epoch_ms(1688879124599)), +(26, 'Seattle Alternative Rock', epoch_ms(1688879124599)), +(27, 'Miami Latin Beats', epoch_ms(1688879124599)), +(28, 'Denver Indie Folk', epoch_ms(1688879124599)), +(29, 'Boston Classical', epoch_ms(1688879124599)), +(20, 'San Francisco Electronic', epoch_ms(1688879124599)), +(21, 'Philadelphia R&B Hits', epoch_ms(1688879124599)), +(22, 'Dallas Pop Punk', epoch_ms(1688879124599)), +(23, 'Atlanta Gospel', epoch_ms(1688879124599)), +(24, 'Las Vegas Hard Rock', epoch_ms(1688879124599)), +(25, 'Phoenix Country Hits', epoch_ms(1688879124599)), +(26, 'Portland Alternative', epoch_ms(1688879124599)), +(27, 'Austin Indie Rock', epoch_ms(1688879124599)), +(28, 'Nashville Country Classics', epoch_ms(1688879124599)), +(29, 'San Diego Surf Rock', epoch_ms(1688879124599)), +(20, 'Minneapolis Folk', epoch_ms(1688879124599)), +(21, 'Detroit Motown', epoch_ms(1688879124599)), +(22, 'Baltimore Jazz Lounge', epoch_ms(1688879124599)), +(23, 'Kansas City Blues', epoch_ms(1688879124599)), +(24, 'St. Louis Smooth Jazz', epoch_ms(1688879124599)), +(25, 'Cleveland Classic Rock', epoch_ms(1688879124599)), +(26, 'Pittsburgh Metal', epoch_ms(1688879124599)), +(27, 'Charlotte Pop Hits', epoch_ms(1688879124599)), +(28, 'Raleigh-Durham Indie Pop', epoch_ms(1688879124599)), +(29, 'Tampa Bay Reggae', epoch_ms(1688879124599)); + +CREATE SCHEMA games; + +CREATE TABLE games.titles ( + game_id int, + game_name string, + num_distinct_players int, + release_date timestamp, + platform string, + genre string, + publisher_id int, + developer_id int, + average_rating float, + online_mode boolean, + total_sales int, + active_monthly int, + dlcs int +); + +INSERT INTO games.titles ( + game_id, + game_name, + num_distinct_players, + release_date, + platform, + genre, + publisher_id, + developer_id, + average_rating, + online_mode, + total_sales, + active_monthly, + dlcs +) +VALUES + (1, 'Battle of the Chinchillas: Furry Fury', 1000, '2022-01-01 00:00:00', 'PS5', 'Action', 1, 928, 4.5, true, 1000000, 5000, 3), + (2, 'Grand Theft Toaster: Carb City Chronicles', 500, '2021-06-15 00:00:00', 'Xbox Series X', 'RPG', 1, 948, 4.2, false, 750000, 2500, 2), + (3, 'Super Slime Soccer: Gooey Goalkeepers', 2000, '2020-11-30 00:00:00', 'Nintendo Switch', 'Sports', 2, 937, 4.8, true, 500000, 3000, 4), + (4, 'Dance Dance Avocado: Guacamole Groove', 1500, '2022-08-20 00:00:00', 'PC', 'Rhythm', 22, 987, 4.6, true, 250000, 2000, 1), + (5, 'Zombie Zookeeper: Undead Menagerie', 800, '2023-02-10 00:00:00', 'PS4', 'Strategy', 13, 902, 4.4, false, 300000, 1500, 3), + (6, 'Squirrel Simulator: Nutty Adventure', 3000, '2021-03-05 00:00:00', 'Xbox One', 'Simulation', 15, 928, 4.2, true, 400000, 2500, 2), + (7, 'Crash Test Dummies: Wacky Collision', 1200, '2022-05-15 00:00:00', 'PC', 'Action', 12, 928, 4.7, false, 150000, 1000, 1), + (8, 'Pizza Delivery Panic: Cheesy Chaos', 1000, '2023-01-30 00:00:00', 'Nintendo Switch', 'Arcade', 8, 928, 4.3, true, 200000, 1800, 2), + (9, 'Alien Abduction Academy: Extraterrestrial Education', 2500, '2020-09-05 00:00:00', 'PS5', 'Adventure', 7, 987, 4.5, false, 800000, 3500, 3), + (10, 'Crazy Cat Circus: Meow Mayhem', 700, '2022-07-25 00:00:00', 'Xbox Series X', 'Puzzle', 18, 987, 4.1, true, 100000, 1200, 1), + (11, 'Robot Rampage: Mechanical Mayhem', 1800, '2021-04-12 00:00:00', 'PC', 'Action', 4, 987, 4.6, true, 450000, 2200, 2), + (12, 'Super Spy Squirrels: Nutty Espionage', 900, '2023-03-18 00:00:00', 'PS4', 'Stealth', 10, 934, 4.4, false, 400000, 1800, 3), + (13, 'Banana Blaster: Fruit Frenzy', 2800, '2021-02-08 00:00:00', 'Xbox One', 'Shooter', 10, 934, 4.3, true, 700000, 3000, 2), + (14, 'Penguin Paradise: Antarctic Adventure', 1100, '2022-04-05 00:00:00', 'PC', 'Simulation', 20, 902, 4.7, false, 200000, 1200, 1), + (15, 'Unicorn Universe: Rainbow Realm', 500, '2023-01-15 00:00:00', 'Nintendo Switch', 'Adventure', 1, 902, 4.2, true, 150000, 900, 2), + (16, 'Spaghetti Showdown: Saucy Shootout', 2100, '2020-10-20 00:00:00', 'PS5', 'Action', 4, 902, 4.4, true, 550000, 2500, 3), + (17, 'Bubblegum Bandits: Sticky Heist', 800, '2022-06-10 00:00:00', 'Xbox Series X', 'Stealth', 3, 902, 4.1, false, 180000, 800, 1), + (18, 'Safari Slingshot: Wild Wildlife', 1300, '2021-03-01 00:00:00', 'PC', 'Arcade', 3, 987, 4.6, true, 300000, 1500, 2), + (19, 'Monster Mop: Cleaning Catastrophe', 600, '2022-12-20 00:00:00', 'Nintendo Switch', 'Puzzle', 17, 934, 4.3, false, 120000, 700, 1), + (20, 'Galactic Golf: Space Swing', 1900, '2020-08-15 00:00:00', 'PS4', 'Sports', 5, 921, 4.5, true, 400000, 2000, 3), + (21, 'Funky Farm Friends: Groovy Gardening', 900, '2023-02-05 00:00:00', 'Xbox One', 'Simulation', 21, 921, 4.3, true, 250000, 1300, 2), + (22, 'Cake Crusaders: Sugary Siege', 1400, '2021-01-25 00:00:00', 'PC', 'Strategy', 23, 902, 4.7, false, 180000, 900, 1), + (23, 'Ninja Narwhal: Aquatic Assassin', 1000, '2022-03-15 00:00:00', 'Nintendo Switch', 'Action', 23, 901, 4.2, true, 150000, 1000, 2); + +CREATE TABLE games.publishers ( + publisher_id int PRIMARY KEY, + publisher_name string +); + +INSERT INTO games.publishers (publisher_id, publisher_name) +VALUES + (1, 'Wacky Game Studios'), + (2, 'Silly Monkey Games'), + (3, 'Laughing Unicorn Interactive'), + (4, 'Crazy Cat Games'), + (5, 'Quirky Penguin Productions'), + (6, 'Absurd Antelope Studios'), + (7, 'Hilarious Hedgehog Games'), + (8, 'Goofy Giraffe Studios'), + (9, 'Whimsical Walrus Entertainment'), + (10, 'Zany Zebra Games'), + (11, 'Ridiculous Rabbit Studios'), + (12, 'Funny Fox Interactive'), + (13, 'Surreal Snake Games'), + (14, 'Bizarre Bat Studios'), + (15, 'Madcap Moose Productions'), + (16, 'Cuckoo Clock Games'), + (17, 'Lunatic Llama Studios'), + (18, 'Silly Goose Games'), + (19, 'Bonkers Beaver Interactive'), + (20, 'Witty Walrus Games'); + +CREATE TABLE games.developers ( + developer_id int, + developer_name string, + num_games_developed int +); + +INSERT INTO games.developers (developer_id, developer_name, num_games_developed) +VALUES + (928, 'CrazyCodr', 10), + (948, 'PixelPirate', 5), + (937, 'CodeNinja', 3), + (987, 'GameWizard', 8), + (902, 'ByteBender', 15), + (934, 'CodeJester', 4), + (902, 'MadGenius', 12), + (921, 'GameGuru', 9), + (901, 'ScriptMage', 6); + +CREATE SCHEMA accounting; +CREATE TABLE accounting.payment_type_table ( + id int, + payment_type_name string, + payment_type_classification string +); + +INSERT INTO accounting.payment_type_table (id, payment_type_name, payment_type_classification) +VALUES + (1, 'VISA', 'CARD'), + (2, 'MASTERCARD', 'CARD'); + + +CREATE TABLE accounting.revenue ( + payment_id int, + payment_amount float, + payment_type int, + customer_id int, + account_type string +); + +INSERT INTO accounting.revenue (payment_id, payment_amount, payment_type, customer_id, account_type) +VALUES + (1, 25.5, 1, 2, 'ACTIVE'), + (2, 12.5, 2, 2, 'INACTIVE'), + (3, 89, 1, 3, 'ACTIVE'), + (4, 1293.2, 2, 2, 'ACTIVE'), + (5, 23, 1, 4, 'INACTIVE'), + (6, 398.13, 2, 3, 'ACTIVE'), + (7, 239.7, 2, 4, 'ACTIVE'),; diff --git a/datajunction-server/tests/errors_test.py b/datajunction-server/tests/errors_test.py new file mode 100644 index 000000000..c187f6ca0 --- /dev/null +++ b/datajunction-server/tests/errors_test.py @@ -0,0 +1,46 @@ +""" +Tests errors. +""" + +from http import HTTPStatus + +from datajunction_server.errors import DJError, DJException, ErrorCode + + +def test_dj_exception() -> None: + """ + Test the base ``DJException``. + """ + exc = DJException() + assert exc.dbapi_exception == "Error" + assert exc.http_status_code == 500 + + exc = DJException(dbapi_exception="InternalError") + assert exc.dbapi_exception == "InternalError" + assert exc.http_status_code == 500 + + exc = DJException( + dbapi_exception="ProgrammingError", + http_status_code=HTTPStatus.BAD_REQUEST, + ) + assert exc.dbapi_exception == "ProgrammingError" + assert exc.http_status_code == HTTPStatus.BAD_REQUEST + + exc = DJException("Message") + assert str(exc) == "Message" + exc = DJException( + "Message", + errors=[ + DJError(message="Error 1", code=ErrorCode.UNKNOWN_ERROR), + DJError(message="Error 2", code=ErrorCode.UNKNOWN_ERROR), + ], + ) + assert ( + str(exc) + == """Message +The following errors happened: +- Error 1 (error code: 0) +- Error 2 (error code: 0)""" + ) + + assert DJException("Message") == DJException("Message") diff --git a/datajunction-server/tests/examples.py b/datajunction-server/tests/examples.py new file mode 100644 index 000000000..15582908e --- /dev/null +++ b/datajunction-server/tests/examples.py @@ -0,0 +1,2531 @@ +""" +Post requests for all example entities +""" + +from datajunction_server.database.column import Column +from datajunction_server.models.query import QueryWithResults +from datajunction_server.sql.parsing.types import IntegerType, StringType, TimestampType +from datajunction_server.typing import QueryState + +SERVICE_SETUP = ( # type: ignore + ( + "/catalogs/", + {"name": "draft"}, + ), + ( + "/catalogs/", + {"name": "default"}, + ), + ( + "/engines/", + {"name": "spark", "version": "3.1.1", "dialect": "spark"}, + ), + ( + "/catalogs/default/engines/", + [{"name": "spark", "version": "3.1.1", "dialect": "spark"}], + ), + ( + "/engines/", + {"name": "druid", "version": "", "dialect": "druid"}, + ), + ( + "/catalogs/default/engines/", + [{"name": "druid", "version": "", "dialect": "druid"}], + ), + ( + "/catalogs/", + {"name": "public"}, + ), + ( + "/engines/", + {"name": "postgres", "version": "15.2"}, + ), + ( + "/catalogs/public/engines/", + [{"name": "postgres", "version": "15.2"}], + ), + ( # DJ must be primed with a "default" namespace + "/namespaces/default/", + {}, + ), + ( + "/namespaces/basic/", + {}, + ), +) + +ROADS = ( # type: ignore + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "municipality_id", "type": "string"}, + {"name": "hard_hat_id", "type": "int"}, + {"name": "order_date", "type": "timestamp"}, + {"name": "required_date", "type": "timestamp"}, + {"name": "dispatched_date", "type": "timestamp"}, + {"name": "dispatcher_id", "type": "int"}, + ], + "description": "All repair orders", + "mode": "published", + "name": "default.repair_orders", + "catalog": "default", + "schema_": "roads", + "table": "repair_orders", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "municipality_id", "type": "string"}, + {"name": "hard_hat_id", "type": "int"}, + {"name": "order_date", "type": "timestamp"}, + {"name": "required_date", "type": "timestamp"}, + {"name": "dispatched_date", "type": "timestamp"}, + {"name": "dispatcher_id", "type": "int"}, + ], + "description": "All repair orders (view)", + "mode": "published", + "name": "default.repair_orders_view", + "catalog": "default", + "schema_": "roads", + "table": "repair_orders_view", + "query": "CREATE OR REPLACE VIEW roads.repair_orders_view AS SELECT * FROM roads.repair_orders", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "repair_type_id", "type": "int"}, + {"name": "price", "type": "float"}, + {"name": "quantity", "type": "int"}, + {"name": "discount", "type": "float"}, + ], + "description": "Details on repair orders", + "mode": "published", + "name": "default.repair_order_details", + "catalog": "default", + "schema_": "roads", + "table": "repair_order_details", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_type_id", "type": "int"}, + {"name": "repair_type_name", "type": "string"}, + {"name": "contractor_id", "type": "int"}, + ], + "description": "Information on types of repairs", + "mode": "published", + "name": "default.repair_type", + "catalog": "default", + "schema_": "roads", + "table": "repair_type", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "contractor_id", "type": "int"}, + {"name": "company_name", "type": "string"}, + {"name": "contact_name", "type": "string"}, + {"name": "contact_title", "type": "string"}, + {"name": "address", "type": "string"}, + {"name": "city", "type": "string"}, + {"name": "state", "type": "string"}, + {"name": "postal_code", "type": "string"}, + {"name": "country", "type": "string"}, + {"name": "phone", "type": "string"}, + ], + "description": "Information on contractors", + "mode": "published", + "name": "default.contractors", + "catalog": "default", + "schema_": "roads", + "table": "contractors", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "municipality_id", "type": "string"}, + {"name": "municipality_type_id", "type": "string"}, + ], + "description": "Lookup table for municipality and municipality types", + "mode": "published", + "name": "default.municipality_municipality_type", + "catalog": "default", + "schema_": "roads", + "table": "municipality_municipality_type", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "municipality_type_id", "type": "string"}, + {"name": "municipality_type_desc", "type": "string"}, + ], + "description": "Information on municipality types", + "mode": "published", + "name": "default.municipality_type", + "catalog": "default", + "schema_": "roads", + "table": "municipality_type", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "municipality_id", "type": "string"}, + {"name": "contact_name", "type": "string"}, + {"name": "contact_title", "type": "string"}, + {"name": "local_region", "type": "string"}, + {"name": "phone", "type": "string"}, + {"name": "state_id", "type": "int"}, + ], + "description": "Information on municipalities", + "mode": "published", + "name": "default.municipality", + "catalog": "default", + "schema_": "roads", + "table": "municipality", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "dispatcher_id", "type": "int"}, + {"name": "company_name", "type": "string"}, + {"name": "phone", "type": "string"}, + ], + "description": "Information on dispatchers", + "mode": "published", + "name": "default.dispatchers", + "catalog": "default", + "schema_": "roads", + "table": "dispatchers", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "hard_hat_id", "type": "int"}, + {"name": "last_name", "type": "string"}, + {"name": "first_name", "type": "string"}, + {"name": "title", "type": "string"}, + {"name": "birth_date", "type": "timestamp"}, + {"name": "hire_date", "type": "timestamp"}, + {"name": "address", "type": "string"}, + {"name": "city", "type": "string"}, + {"name": "state", "type": "string"}, + {"name": "postal_code", "type": "string"}, + {"name": "country", "type": "string"}, + {"name": "manager", "type": "int"}, + {"name": "contractor_id", "type": "int"}, + ], + "description": "Information on employees", + "mode": "published", + "name": "default.hard_hats", + "catalog": "default", + "schema_": "roads", + "table": "hard_hats", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "hard_hat_id", "type": "int"}, + {"name": "state_id", "type": "string"}, + ], + "description": "Lookup table for employee's current state", + "mode": "published", + "name": "default.hard_hat_state", + "catalog": "default", + "schema_": "roads", + "table": "hard_hat_state", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "state_id", "type": "int"}, + {"name": "state_name", "type": "string"}, + {"name": "state_abbr", "type": "string"}, + {"name": "state_region", "type": "int"}, + ], + "description": "Information on different types of repairs", + "mode": "published", + "name": "default.us_states", + "catalog": "default", + "schema_": "roads", + "table": "us_states", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "us_region_id", "type": "int"}, + {"name": "us_region_description", "type": "string"}, + ], + "description": "Information on US regions", + "mode": "published", + "name": "default.us_region", + "catalog": "default", + "schema_": "roads", + "table": "us_region", + }, + ), + ( + "/nodes/dimension/", + { + "description": "Repair order dimension", + "query": """ + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM default.repair_orders + """, + "mode": "published", + "name": "default.repair_order", + "primary_key": ["repair_order_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Contractor dimension", + "query": """ + SELECT + contractor_id, + company_name, + contact_name, + contact_title, + address, + city, + state, + postal_code, + country, + phone + FROM default.contractors + """, + "mode": "published", + "name": "default.contractor", + "primary_key": ["contractor_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Hard hat dimension", + "query": """ + SELECT + hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id + FROM default.hard_hats + """, + "mode": "published", + "name": "default.hard_hat", + "primary_key": ["hard_hat_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Hard hat dimension #2", + "query": """ + SELECT + hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id + FROM default.hard_hats + """, + "mode": "published", + "name": "default.hard_hat_2", + "primary_key": ["hard_hat_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Hard hat dimension (for deletion)", + "query": """ + SELECT + hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id + FROM default.hard_hats + """, + "mode": "published", + "name": "default.hard_hat_to_delete", + "primary_key": ["hard_hat_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Hard hat dimension", + "query": """ + SELECT + hh.hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id, + hhs.state_id AS state_id + FROM default.hard_hats hh + LEFT JOIN default.hard_hat_state hhs + ON hh.hard_hat_id = hhs.hard_hat_id + WHERE hh.state_id = 'NY' + """, + "mode": "published", + "name": "default.local_hard_hats", + "primary_key": ["hard_hat_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Hard hat dimension #1", + "query": """ + SELECT hh.hard_hat_id + FROM default.hard_hats hh + """, + "mode": "published", + "name": "default.local_hard_hats_1", + "primary_key": ["hard_hat_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Hard hat dimension #2", + "query": """ + SELECT hh.hard_hat_id + FROM default.hard_hats hh + """, + "mode": "published", + "name": "default.local_hard_hats_2", + "primary_key": ["hard_hat_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "US state dimension", + "query": """ + SELECT + state_id, + state_name, + state_abbr AS state_short, + state_region + FROM default.us_states s + """, + "mode": "published", + "name": "default.us_state", + "primary_key": ["state_short"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Dispatcher dimension", + "query": """ + SELECT + dispatcher_id, + company_name, + phone + FROM default.dispatchers + """, + "mode": "published", + "name": "default.dispatcher", + "primary_key": ["dispatcher_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Municipality dimension", + "query": """ + SELECT + m.municipality_id AS municipality_id, + contact_name, + contact_title, + local_region, + state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM default.municipality AS m + LEFT JOIN default.municipality_municipality_type AS mmt + ON m.municipality_id = mmt.municipality_id + LEFT JOIN default.municipality_type AS mt + ON mmt.municipality_type_id = mt.municipality_type_desc + """, + "mode": "published", + "name": "default.municipality_dim", + "primary_key": ["municipality_id"], + }, + ), + ( + "/nodes/transform/", + { + "name": "default.regional_level_agg", + "description": "Regional-level aggregates", + "mode": "published", + "primary_key": [ + "us_region_id", + "state_name", + "order_year", + "order_month", + "order_day", + ], + "query": """ +WITH ro as (SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM default.repair_orders) + SELECT + usr.us_region_id, + us.state_name, + CONCAT(us.state_name, '-', usr.us_region_description) AS location_hierarchy, + EXTRACT(YEAR FROM ro.order_date) AS order_year, + EXTRACT(MONTH FROM ro.order_date) AS order_month, + EXTRACT(DAY FROM ro.order_date) AS order_day, + COUNT(DISTINCT CASE WHEN ro.dispatched_date IS NOT NULL THEN ro.repair_order_id ELSE NULL END) AS completed_repairs, + COUNT(DISTINCT ro.repair_order_id) AS total_repairs_dispatched, + SUM(rd.price * rd.quantity) AS total_amount_in_region, + AVG(rd.price * rd.quantity) AS avg_repair_amount_in_region, + -- ELEMENT_AT(ARRAY_SORT(COLLECT_LIST(STRUCT(COUNT(*) AS cnt, rt.repair_type_name AS repair_type_name)), (left, right) -> case when left.cnt < right.cnt then 1 when left.cnt > right.cnt then -1 else 0 end), 0).repair_type_name AS most_common_repair_type, + AVG(DATEDIFF(ro.dispatched_date, ro.order_date)) AS avg_dispatch_delay, + COUNT(DISTINCT c.contractor_id) AS unique_contractors +FROM ro +JOIN + default.municipality m ON ro.municipality_id = m.municipality_id +JOIN + default.us_states us ON m.state_id = us.state_id + AND AVG(rd.price * rd.quantity) > + (SELECT AVG(price * quantity) FROM default.repair_order_details WHERE repair_order_id = ro.repair_order_id) +JOIN + default.us_states us ON m.state_id = us.state_id +JOIN + default.us_region usr ON us.state_region = usr.us_region_id +JOIN + default.repair_order_details rd ON ro.repair_order_id = rd.repair_order_id +JOIN + default.repair_type rt ON rd.repair_type_id = rt.repair_type_id +JOIN + default.contractors c ON rt.contractor_id = c.contractor_id +GROUP BY + usr.us_region_id, + EXTRACT(YEAR FROM ro.order_date), + EXTRACT(MONTH FROM ro.order_date), + EXTRACT(DAY FROM ro.order_date)""", + }, + ), + ( + "/nodes/transform/", + { + "description": "National level aggregates", + "name": "default.national_level_agg", + "mode": "published", + "query": "SELECT SUM(rd.price * rd.quantity) AS total_amount_nationwide FROM default.repair_order_details rd", + }, + ), + ( + "/nodes/transform/", + { + "description": "Fact transform with all details on repair orders", + "name": "default.repair_orders_fact", + "display_name": "Repair Orders Fact", + "mode": "published", + "query": """SELECT + repair_orders.repair_order_id, + repair_orders.municipality_id, + repair_orders.hard_hat_id, + repair_orders.dispatcher_id, + repair_orders.order_date, + repair_orders.dispatched_date, + repair_orders.required_date, + repair_order_details.discount, + repair_order_details.price, + repair_order_details.quantity, + repair_order_details.repair_type_id, + repair_order_details.price * repair_order_details.quantity AS total_repair_cost, + repair_orders.dispatched_date - repair_orders.order_date AS time_to_dispatch, + repair_orders.dispatched_date - repair_orders.required_date AS dispatch_delay +FROM + default.repair_orders repair_orders +JOIN + default.repair_order_details repair_order_details +ON repair_orders.repair_order_id = repair_order_details.repair_order_id""", + }, + ), + ( + "/nodes/metric/", + { + "description": """For each US region (as defined in the us_region table), we want to calculate: + Regional Repair Efficiency = (Number of Completed Repairs / Total Repairs Dispatched) × + (Total Repair Amount in Region / Total Repair Amount Nationwide) × 100 + Here: + A "Completed Repair" is one where the dispatched_date is not null. + "Total Repair Amount in Region" is the total amount spent on repairs in a given region. + "Total Repair Amount Nationwide" is the total amount spent on all repairs nationwide.""", + "name": "default.regional_repair_efficiency", + "query": """SELECT + (SUM(rm.completed_repairs) * 1.0 / SUM(rm.total_repairs_dispatched)) * + (SUM(rm.total_amount_in_region) * 1.0 / SUM(na.total_amount_nationwide)) * 100 +FROM + default.regional_level_agg rm +CROSS JOIN + default.national_level_agg na""", + "mode": "published", + }, + ), + ( + "/nodes/metric/", + { + "description": "Number of repair orders", + "query": ("SELECT count(repair_order_id) FROM default.repair_orders_fact"), + "mode": "published", + "name": "default.num_repair_orders", + "metric_metadata": { + "direction": "higher_is_better", + "unit": "dollar", + }, + }, + ), + ( + "/nodes/metric/", + { + "description": "Average repair price", + "query": ( + "SELECT avg(repair_orders_fact.price) FROM default.repair_orders_fact repair_orders_fact" + ), + "mode": "published", + "name": "default.avg_repair_price", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total repair cost", + "query": "SELECT sum(total_repair_cost) FROM default.repair_orders_fact", + "mode": "published", + "name": "default.total_repair_cost", + }, + ), + ( + "/nodes/metric/", + { + "description": "Average length of employment", + "query": ( + "SELECT avg(CAST(NOW() AS DATE) - hire_date) FROM default.hard_hat" + ), + "mode": "published", + "name": "default.avg_length_of_employment", + }, + ), + ( + "/nodes/metric/", + { + "name": "default.discounted_orders_rate", + "query": ( + """ + SELECT + cast(sum(if(discount > 0.0, 1, 0)) as double) / count(*) + AS default_DOT_discounted_orders_rate + FROM default.repair_orders_fact + """ + ), + "mode": "published", + "description": "Proportion of Discounted Orders", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total repair order discounts", + "query": ("SELECT sum(price * discount) FROM default.repair_orders_fact"), + "mode": "published", + "name": "default.total_repair_order_discounts", + }, + ), + ( + "/nodes/metric/", + { + "description": "Average repair order discounts", + "query": ("SELECT avg(price * discount) FROM default.repair_orders_fact"), + "mode": "published", + "name": "default.avg_repair_order_discounts", + }, + ), + ( + "/nodes/metric/", + { + "description": "Average time to dispatch a repair order", + "query": ( + "SELECT avg(cast(repair_orders_fact.time_to_dispatch as int)) " + "FROM default.repair_orders_fact repair_orders_fact" + ), + "mode": "published", + "display_name": "Avg Time To Dispatch", + "name": "default.avg_time_to_dispatch", + }, + ), + ( + ("/nodes/default.repair_orders_fact/link"), + { + "dimension_node": "default.municipality_dim", + "join_type": "inner", + "join_on": ( + "default.repair_orders_fact.municipality_id = default.municipality_dim.municipality_id" + ), + }, + ), + ( + "/nodes/default.repair_orders_fact/link", + { + "dimension_node": "default.hard_hat", + "join_type": "inner", + "join_on": ( + "default.repair_orders_fact.hard_hat_id = default.hard_hat.hard_hat_id" + ), + }, + ), + ( + "/nodes/default.repair_orders_fact/link", + { + "dimension_node": "default.hard_hat_to_delete", + "join_type": "left", + "join_on": ( + "default.repair_orders_fact.hard_hat_id = default.hard_hat_to_delete.hard_hat_id" + ), + }, + ), + ( + "/nodes/default.repair_orders_fact/link", + { + "dimension_node": "default.dispatcher", + "join_type": "inner", + "join_on": ( + "default.repair_orders_fact.dispatcher_id = default.dispatcher.dispatcher_id" + ), + }, + ), + ( + "/nodes/default.repair_order_details/link", + { + "dimension_node": "default.repair_order", + "join_type": "inner", + "join_on": ( + "default.repair_order_details.repair_order_id = default.repair_order.repair_order_id" + ), + }, + ), + ( + "/nodes/default.repair_type/link", + { + "dimension_node": "default.contractor", + "join_type": "inner", + "join_on": ( + "default.repair_type.contractor_id = default.contractor.contractor_id" + ), + }, + ), + ( + "/nodes/default.repair_orders/link", + { + "dimension_node": "default.repair_order", + "join_type": "inner", + "join_on": ( + "default.repair_orders.repair_order_id = default.repair_order.repair_order_id" + ), + }, + ), + ( + "/nodes/default.repair_orders/link", + { + "dimension_node": "default.dispatcher", + "join_type": "inner", + "join_on": ( + "default.repair_orders.dispatcher_id = default.dispatcher.dispatcher_id" + ), + }, + ), + ( + "/nodes/default.contractors/link", + { + "dimension_node": "default.us_state", + "join_type": "inner", + "join_on": ("default.contractors.state = default.us_state.state_short"), + }, + ), + ( + "/nodes/default.hard_hat/link", + { + "dimension_node": "default.us_state", + "join_type": "inner", + "join_on": ("default.hard_hat.state = default.us_state.state_short"), + }, + ), + ( + "/nodes/default.repair_order_details/link", + { + "dimension_node": "default.repair_order", + "join_type": "inner", + "join_on": ( + "default.repair_order_details.repair_order_id = default.repair_order.repair_order_id" + ), + }, + ), + ( + "/nodes/default.repair_order/link", + { + "dimension_node": "default.dispatcher", + "join_type": "inner", + "join_on": ( + "default.repair_order.dispatcher_id = default.dispatcher.dispatcher_id" + ), + }, + ), + ( + "/nodes/default.repair_order/link", + { + "dimension_node": "default.hard_hat", + "join_type": "inner", + "join_on": ( + "default.repair_order.hard_hat_id = default.hard_hat.hard_hat_id" + ), + }, + ), + ( + "/nodes/default.repair_order/link", + { + "dimension_node": "default.hard_hat_to_delete", + "join_type": "left", + "join_on": ( + "default.repair_order.hard_hat_id = default.hard_hat_to_delete.hard_hat_id" + ), + }, + ), + ( + "/nodes/default.repair_order/link", + { + "dimension_node": "default.municipality_dim", + "join_type": "inner", + "join_on": ( + "default.repair_order.municipality_id = default.municipality_dim.municipality_id" + ), + }, + ), +) + +NAMESPACED_ROADS = ( # type: ignore + ( # foo.bar Namespaced copy of roads database example + "/namespaces/foo.bar/", + {}, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "municipality_id", "type": "string"}, + {"name": "hard_hat_id", "type": "int"}, + {"name": "order_date", "type": "timestamp"}, + {"name": "required_date", "type": "timestamp"}, + {"name": "dispatched_date", "type": "timestamp"}, + {"name": "dispatcher_id", "type": "int"}, + ], + "description": "All repair orders", + "mode": "published", + "name": "foo.bar.repair_orders", + "catalog": "default", + "schema_": "roads", + "table": "repair_orders", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_order_id", "type": "int"}, + {"name": "repair_type_id", "type": "int"}, + {"name": "price", "type": "float"}, + {"name": "quantity", "type": "int"}, + {"name": "discount", "type": "float"}, + ], + "description": "Details on repair orders", + "mode": "published", + "name": "foo.bar.repair_order_details", + "catalog": "default", + "schema_": "roads", + "table": "repair_order_details", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "repair_type_id", "type": "int"}, + {"name": "repair_type_name", "type": "string"}, + {"name": "contractor_id", "type": "int"}, + ], + "description": "Information on types of repairs", + "mode": "published", + "name": "foo.bar.repair_type", + "catalog": "default", + "schema_": "roads", + "table": "repair_type", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "contractor_id", "type": "int"}, + {"name": "company_name", "type": "string"}, + {"name": "contact_name", "type": "string"}, + {"name": "contact_title", "type": "string"}, + {"name": "address", "type": "string"}, + {"name": "city", "type": "string"}, + {"name": "state", "type": "string"}, + {"name": "postal_code", "type": "string"}, + {"name": "country", "type": "string"}, + {"name": "phone", "type": "string"}, + ], + "description": "Information on contractors", + "mode": "published", + "name": "foo.bar.contractors", + "catalog": "default", + "schema_": "roads", + "table": "contractors", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "municipality_id", "type": "string"}, + {"name": "municipality_type_id", "type": "string"}, + ], + "description": "Lookup table for municipality and municipality types", + "mode": "published", + "name": "foo.bar.municipality_municipality_type", + "catalog": "default", + "schema_": "roads", + "table": "municipality_municipality_type", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "municipality_type_id", "type": "string"}, + {"name": "municipality_type_desc", "type": "string"}, + ], + "description": "Information on municipality types", + "mode": "published", + "name": "foo.bar.municipality_type", + "catalog": "default", + "schema_": "roads", + "table": "municipality_type", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "municipality_id", "type": "string"}, + {"name": "contact_name", "type": "string"}, + {"name": "contact_title", "type": "string"}, + {"name": "local_region", "type": "string"}, + {"name": "phone", "type": "string"}, + {"name": "state_id", "type": "int"}, + ], + "description": "Information on municipalities", + "mode": "published", + "name": "foo.bar.municipality", + "catalog": "default", + "schema_": "roads", + "table": "municipality", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "dispatcher_id", "type": "int"}, + {"name": "company_name", "type": "string"}, + {"name": "phone", "type": "string"}, + ], + "description": "Information on dispatchers", + "mode": "published", + "name": "foo.bar.dispatchers", + "catalog": "default", + "schema_": "roads", + "table": "dispatchers", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "hard_hat_id", "type": "int"}, + {"name": "last_name", "type": "string"}, + {"name": "first_name", "type": "string"}, + {"name": "title", "type": "string"}, + {"name": "birth_date", "type": "timestamp"}, + {"name": "hire_date", "type": "timestamp"}, + {"name": "address", "type": "string"}, + {"name": "city", "type": "string"}, + {"name": "state", "type": "string"}, + {"name": "postal_code", "type": "string"}, + {"name": "country", "type": "string"}, + {"name": "manager", "type": "int"}, + {"name": "contractor_id", "type": "int"}, + ], + "description": "Information on employees", + "mode": "published", + "name": "foo.bar.hard_hats", + "catalog": "default", + "schema_": "roads", + "table": "hard_hats", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "hard_hat_id", "type": "int"}, + {"name": "state_id", "type": "string"}, + ], + "description": "Lookup table for employee's current state", + "mode": "published", + "name": "foo.bar.hard_hat_state", + "catalog": "default", + "schema_": "roads", + "table": "hard_hat_state", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "state_id", "type": "int"}, + {"name": "state_name", "type": "string"}, + {"name": "state_abbr", "type": "string"}, + {"name": "state_region", "type": "int"}, + ], + "description": "Information on different types of repairs", + "mode": "published", + "name": "foo.bar.us_states", + "catalog": "default", + "schema_": "roads", + "table": "us_states", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "us_region_id", "type": "int"}, + {"name": "us_region_description", "type": "string"}, + ], + "description": "Information on US regions", + "mode": "published", + "name": "foo.bar.us_region", + "catalog": "default", + "schema_": "roads", + "table": "us_region", + }, + ), + ( + "/nodes/dimension/", + { + "description": "Repair order dimension", + "query": """ + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM foo.bar.repair_orders + """, + "mode": "published", + "name": "foo.bar.repair_order", + "primary_key": ["repair_order_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Contractor dimension", + "query": """ + SELECT + contractor_id, + company_name, + contact_name, + contact_title, + address, + city, + state, + postal_code, + country, + phone + FROM foo.bar.contractors + """, + "mode": "published", + "name": "foo.bar.contractor", + "primary_key": ["contractor_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Hard hat dimension", + "query": """ + SELECT + hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id + FROM foo.bar.hard_hats + """, + "mode": "published", + "name": "foo.bar.hard_hat", + "primary_key": ["hard_hat_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Hard hat dimension", + "query": """ + SELECT + hh.hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id, + hhs.state_id AS state_id + FROM foo.bar.hard_hats hh + LEFT JOIN foo.bar.hard_hat_state hhs + ON hh.hard_hat_id = hhs.hard_hat_id + WHERE hh.state_id = 'NY' + """, + "mode": "published", + "name": "foo.bar.local_hard_hats", + "primary_key": ["hard_hat_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "US state dimension", + "query": """ + SELECT + state_id, + state_name, + state_abbr, + state_region, + r.us_region_description AS state_region_description + FROM foo.bar.us_states s + LEFT JOIN foo.bar.us_region r + ON s.state_region = r.us_region_id + """, + "mode": "published", + "name": "foo.bar.us_state", + "primary_key": ["state_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Dispatcher dimension", + "query": """ + SELECT + dispatcher_id, + company_name, + phone + FROM foo.bar.dispatchers + """, + "mode": "published", + "name": "foo.bar.dispatcher", + "primary_key": ["dispatcher_id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Municipality dimension", + "query": """ + SELECT + m.municipality_id AS municipality_id, + contact_name, + contact_title, + local_region, + state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM foo.bar.municipality AS m + LEFT JOIN foo.bar.municipality_municipality_type AS mmt + ON m.municipality_id = mmt.municipality_id + LEFT JOIN foo.bar.municipality_type AS mt + ON mmt.municipality_type_id = mt.municipality_type_desc + """, + "mode": "published", + "name": "foo.bar.municipality_dim", + "primary_key": ["municipality_id"], + }, + ), + ( + "/nodes/metric/", + { + "description": "Number of repair orders", + "query": ( + "SELECT count(repair_order_id) as foo_DOT_bar_DOT_num_repair_orders " + "FROM foo.bar.repair_orders" + ), + "mode": "published", + "name": "foo.bar.num_repair_orders", + }, + ), + ( + "/nodes/metric/", + { + "description": "Average repair price", + "query": "SELECT avg(price) FROM foo.bar.repair_order_details", + "mode": "published", + "name": "foo.bar.avg_repair_price", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total repair cost", + "query": "SELECT sum(price) FROM foo.bar.repair_order_details", + "mode": "published", + "name": "foo.bar.total_repair_cost", + }, + ), + ( + "/nodes/metric/", + { + "description": "Average length of employment", + "query": ("SELECT avg(NOW() - hire_date) FROM foo.bar.hard_hats"), + "mode": "published", + "name": "foo.bar.avg_length_of_employment", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total repair order discounts", + "query": ("SELECT sum(price * discount) FROM foo.bar.repair_order_details"), + "mode": "published", + "name": "foo.bar.total_repair_order_discounts", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total repair order discounts", + "query": ("SELECT avg(price * discount) FROM foo.bar.repair_order_details"), + "mode": "published", + "name": "foo.bar.avg_repair_order_discounts", + }, + ), + ( + "/nodes/metric/", + { + "description": "Average time to dispatch a repair order", + "query": ( + "SELECT avg(dispatched_date - order_date) FROM foo.bar.repair_orders" + ), + "mode": "published", + "name": "foo.bar.avg_time_to_dispatch", + }, + ), + ( + ( + "/nodes/foo.bar.repair_order_details/columns/repair_order_id/" + "?dimension=foo.bar.repair_order&dimension_column=repair_order_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_type/columns/contractor_id/" + "?dimension=foo.bar.contractor&dimension_column=contractor_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_orders/columns/repair_order_id/" + "?dimension=foo.bar.repair_order&dimension_column=repair_order_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_order_details/columns/repair_order_id/" + "?dimension=foo.bar.repair_order&dimension_column=repair_order_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_order/columns/dispatcher_id/" + "?dimension=foo.bar.dispatcher&dimension_column=dispatcher_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_order/columns/hard_hat_id/" + "?dimension=foo.bar.hard_hat&dimension_column=hard_hat_id" + ), + {}, + ), + ( + ( + "/nodes/foo.bar.repair_order/columns/municipality_id/" + "?dimension=foo.bar.municipality_dim&dimension_column=municipality_id" + ), + {}, + ), +) + +ACCOUNT_REVENUE = ( # type: ignore + ( # Accounts/Revenue examples begin + "/nodes/source/", + { + "columns": [ + {"name": "id", "type": "int"}, + {"name": "account_type_name", "type": "string"}, + {"name": "account_type_classification", "type": "int"}, + {"name": "preferred_payment_method", "type": "int"}, + ], + "description": "A source table for account type data", + "mode": "published", + "name": "default.account_type_table", + "catalog": "default", + "schema_": "accounting", + "table": "account_type_table", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "id", "type": "int"}, + {"name": "payment_type_name", "type": "string"}, + {"name": "payment_type_classification", "type": "string"}, + ], + "description": "A source table for different types of payments", + "mode": "published", + "name": "default.payment_type_table", + "catalog": "default", + "schema_": "accounting", + "table": "payment_type_table", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "payment_id", "type": "int"}, + {"name": "payment_amount", "type": "float"}, + {"name": "payment_type", "type": "int"}, + {"name": "customer_id", "type": "int"}, + {"name": "account_type", "type": "string"}, + ], + "description": "All repair orders", + "mode": "published", + "name": "default.revenue", + "catalog": "default", + "schema_": "accounting", + "table": "revenue", + }, + ), + ( + "/nodes/dimension/", + { + "description": "Payment type dimensions", + "query": ( + "SELECT id, payment_type_name, payment_type_classification " + "FROM default.payment_type_table" + ), + "mode": "published", + "name": "default.payment_type", + "primary_key": ["id"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Account type dimension", + "query": ( + "SELECT id, account_type_name, " + "account_type_classification FROM " + "default.account_type_table" + ), + "mode": "published", + "name": "default.account_type", + "primary_key": ["id"], + }, + ), + ( + "/nodes/transform/", + { + "query": ( + "SELECT payment_id, payment_amount, customer_id, account_type " + "FROM default.revenue WHERE payment_amount > 1000000" + ), + "description": "Only large revenue payments", + "mode": "published", + "name": "default.large_revenue_payments_only", + }, + ), + ( + "/nodes/transform/", + { + "query": ( + "SELECT payment_id, payment_amount, customer_id, account_type " + "FROM default.revenue WHERE payment_amount > 1000000" + ), + "description": "Only large revenue payments #1", + "mode": "published", + "name": "default.large_revenue_payments_only_1", + }, + ), + ( + "/nodes/transform/", + { + "query": ( + "SELECT payment_id, payment_amount, customer_id, account_type " + "FROM default.revenue WHERE payment_amount > 1000000" + ), + "description": "Only large revenue payments #2", + "mode": "published", + "name": "default.large_revenue_payments_only_2", + }, + ), + ( + "/nodes/transform/", + { + "query": ( + "SELECT payment_id, payment_amount, customer_id, account_type " + "FROM default.revenue WHERE payment_amount > 1000000" + ), + "description": "Only large revenue payments", + "mode": "published", + "name": "default.large_revenue_payments_only_custom", + "custom_metadata": {"foo": "bar"}, + }, + ), + ( + "/nodes/transform/", + { + "query": ( + "SELECT payment_id, payment_amount, customer_id, account_type " + "FROM default.revenue WHERE " + "large_revenue_payments_and_business_only > 1000000 " + "AND account_type='BUSINESS'" + ), + "description": "Only large revenue payments from business accounts", + "mode": "published", + "name": "default.large_revenue_payments_and_business_only", + }, + ), + ( + "/nodes/transform/", + { + "query": ( + "SELECT payment_id, payment_amount, customer_id, account_type " + "FROM default.revenue WHERE " + "large_revenue_payments_and_business_only > 1000000 " + "AND account_type='BUSINESS'" + ), + "description": "Only large revenue payments from business accounts 1", + "mode": "published", + "name": "default.large_revenue_payments_and_business_only_1", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total number of account types", + "query": "SELECT count(id) FROM default.account_type", + "mode": "published", + "name": "default.number_of_account_types", + }, + ), +) + +BASIC = ( # type: ignore + ( + "/namespaces/basic.source/", + {}, + ), + ( + "/namespaces/basic.transform/", + {}, + ), + ( + "/namespaces/basic.dimension/", + {}, + ), + ( + "/nodes/source/", + { + "name": "basic.source.users", + "description": "A user table", + "columns": [ + {"name": "id", "type": "int"}, + {"name": "full_name", "type": "string"}, + {"name": "age", "type": "int"}, + { + "name": "country", + "type": "string", + "dimension": "basic.dimension.countries", + }, + {"name": "gender", "type": "string"}, + {"name": "preferred_language", "type": "string"}, + {"name": "secret_number", "type": "float"}, + {"name": "created_at", "type": "timestamp"}, + {"name": "post_processing_timestamp", "type": "timestamp"}, + ], + "mode": "published", + "catalog": "public", + "schema_": "basic", + "table": "dim_users", + }, + ), + ( + "/nodes/dimension/", + { + "description": "User dimension", + "query": ( + "SELECT id, full_name, age, country, gender, preferred_language, " + "secret_number, created_at, post_processing_timestamp " + "FROM basic.source.users" + ), + "mode": "published", + "name": "basic.dimension.users", + "primary_key": ["id"], + }, + ), + ( + "/nodes/source/", + { + "name": "basic.source.comments", + "description": "A fact table with comments", + "columns": [ + {"name": "id", "type": "int"}, + { + "name": "user_id", + "type": "int", + "dimension": "basic.dimension.users", + }, + {"name": "timestamp", "type": "timestamp"}, + {"name": "text", "type": "string"}, + {"name": "event_timestamp", "type": "timestamp"}, + {"name": "created_at", "type": "timestamp"}, + {"name": "post_processing_timestamp", "type": "timestamp"}, + ], + "mode": "published", + "catalog": "public", + "schema_": "basic", + "table": "comments", + }, + ), + ( + "/nodes/dimension/", + { + "description": "Country dimension", + "query": "SELECT country, COUNT(1) AS user_cnt " + "FROM basic.source.users GROUP BY country", + "mode": "published", + "name": "basic.dimension.countries", + "primary_key": ["country"], + }, + ), + ( + "/nodes/transform/", + { + "description": "Country level agg table", + "query": ( + "SELECT country, COUNT(DISTINCT id) AS num_users " + "FROM basic.source.users GROUP BY 1" + ), + "mode": "published", + "name": "basic.transform.country_agg", + }, + ), + ( + "/nodes/metric/", + { + "description": "Number of comments", + "query": ("SELECT COUNT(1) FROM basic.source.comments"), + "mode": "published", + "name": "basic.num_comments", + }, + ), + ( + "/nodes/metric/", + { + "description": "Number of users.", + "type": "metric", + "query": ("SELECT SUM(1) FROM basic.dimension.users"), + "mode": "published", + "name": "basic.num_users", + }, + ), +) + +BASIC_IN_DIFFERENT_CATALOG = ( # type: ignore + ( + "/namespaces/different.basic/", + {}, + ), + ( + "/namespaces/different.basic.source/", + {}, + ), + ( + "/namespaces/different.basic.transform/", + {}, + ), + ( + "/namespaces/different.basic.dimension/", + {}, + ), + ( + "/nodes/source/", + { + "name": "different.basic.source.users", + "description": "A user table", + "columns": [ + {"name": "id", "type": "int"}, + {"name": "full_name", "type": "string"}, + {"name": "age", "type": "int"}, + {"name": "country", "type": "string"}, + {"name": "gender", "type": "string"}, + {"name": "preferred_language", "type": "string"}, + {"name": "secret_number", "type": "float"}, + {"name": "created_at", "type": "timestamp"}, + {"name": "post_processing_timestamp", "type": "timestamp"}, + ], + "mode": "published", + "catalog": "default", + "schema_": "basic", + "table": "dim_users", + }, + ), + ( + "/nodes/dimension/", + { + "description": "User dimension", + "query": ( + "SELECT id, full_name, age, country, gender, preferred_language, " + "secret_number, created_at, post_processing_timestamp " + "FROM different.basic.source.users" + ), + "mode": "published", + "name": "different.basic.dimension.users", + "primary_key": ["id"], + }, + ), + ( + "/nodes/source/", + { + "name": "different.basic.source.comments", + "description": "A fact table with comments", + "columns": [ + {"name": "id", "type": "int"}, + { + "name": "user_id", + "type": "int", + "dimension": "different.basic.dimension.users", + }, + {"name": "timestamp", "type": "timestamp"}, + {"name": "text", "type": "string"}, + {"name": "event_timestamp", "type": "timestamp"}, + {"name": "created_at", "type": "timestamp"}, + {"name": "post_processing_timestamp", "type": "timestamp"}, + ], + "mode": "published", + "catalog": "default", + "schema_": "basic", + "table": "comments", + }, + ), + ( + "/nodes/dimension/", + { + "description": "Country dimension", + "query": "SELECT country, COUNT(1) AS user_cnt " + "FROM different.basic.source.users GROUP BY country", + "mode": "published", + "name": "different.basic.dimension.countries", + "primary_key": ["country"], + }, + ), + ( + "/nodes/transform/", + { + "description": "Country level agg table", + "query": ( + "SELECT country, COUNT(DISTINCT id) AS num_users " + "FROM different.basic.source.users GROUP BY 1" + ), + "mode": "published", + "name": "different.basic.transform.country_agg", + }, + ), + ( + "/nodes/metric/", + { + "description": "Number of comments", + "query": ("SELECT COUNT(1) FROM different.basic.source.comments"), + "mode": "published", + "name": "different.basic.num_comments", + }, + ), + ( + "/nodes/metric/", + { + "description": "Number of users.", + "type": "metric", + "query": ( + "SELECT SUM(num_users) FROM different.basic.transform.country_agg" + ), + "mode": "published", + "name": "different.basic.num_users", + }, + ), +) + +EVENT = ( # type: ignore + ( # Event examples + "/nodes/source/", + { + "name": "default.event_source", + "description": "Events", + "columns": [ + {"name": "event_id", "type": "int"}, + {"name": "event_latency", "type": "int"}, + {"name": "device_id", "type": "int"}, + {"name": "country", "type": "string"}, + ], + "mode": "published", + "catalog": "default", + "schema_": "logs", + "table": "log_events", + }, + ), + ( + "/nodes/transform/", + { + "name": "default.long_events", + "description": "High-Latency Events", + "query": "SELECT event_id, event_latency, device_id, country " + "FROM default.event_source WHERE event_latency > 1000000", + "mode": "published", + }, + ), + ( + "/nodes/dimension/", + { + "name": "default.country_dim", + "description": "Country Dimension", + "query": "SELECT country, COUNT(DISTINCT event_id) AS events_cnt " + "FROM default.event_source GROUP BY country", + "mode": "published", + "primary_key": ["country"], + }, + ), + ( + "/nodes/default.event_source/link", + { + "dimension_node": "default.country_dim", + "join_type": "left", + "join_on": ("default.event_source.country = default.country_dim.country"), + }, + ), + ( + "/nodes/default.long_events/link", + { + "dimension_node": "default.country_dim", + "join_type": "left", + "join_on": ("default.long_events.country = default.country_dim.country"), + }, + ), + ( + "/nodes/metric/", + { + "name": "default.device_ids_count", + "description": "Number of Distinct Devices", + "query": "SELECT COUNT(DISTINCT device_id) FROM default.event_source", + "mode": "published", + }, + ), + ( + "/nodes/metric/", + { + "name": "default.long_events_distinct_countries", + "description": "Number of Distinct Countries for Long Events", + "query": "SELECT COUNT(DISTINCT country) FROM default.long_events", + "mode": "published", + }, + ), +) + +DBT = ( # type: ignore + ( + "/namespaces/dbt.source/", + {}, + ), + ( + "/namespaces/dbt.source.jaffle_shop/", + {}, + ), + ( + "/namespaces/dbt.transform/", + {}, + ), + ( + "/namespaces/dbt.dimension/", + {}, + ), + ( + "/namespaces/dbt.source.stripe/", + {}, + ), + ( # DBT examples + "/nodes/source/", + { + "columns": [ + {"name": "id", "type": "int"}, + {"name": "first_name", "type": "string"}, + {"name": "last_name", "type": "string"}, + ], + "description": "Customer table", + "mode": "published", + "name": "dbt.source.jaffle_shop.customers", + "catalog": "public", + "schema_": "jaffle_shop", + "table": "customers", + }, + ), + ( + "/nodes/dimension/", + { + "description": "User dimension", + "query": ( + "SELECT id, first_name, last_name FROM dbt.source.jaffle_shop.customers" + ), + "mode": "published", + "name": "dbt.dimension.customers", + "primary_key": ["id"], + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "id", "type": "int"}, + { + "name": "user_id", + "type": "int", + "dimension": "dbt.dimension.customers", + }, + {"name": "order_date", "type": "date"}, + {"name": "status", "type": "string"}, + {"name": "_etl_loaded_at", "type": "timestamp"}, + ], + "description": "Orders fact table", + "mode": "published", + "name": "dbt.source.jaffle_shop.orders", + "catalog": "public", + "schema_": "jaffle_shop", + "table": "orders", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "id", "type": "int"}, + {"name": "orderid", "type": "int"}, + {"name": "paymentmethod", "type": "string"}, + {"name": "status", "type": "string"}, + {"name": "amount", "type": "int"}, + {"name": "created", "type": "date"}, + {"name": "_batched_at", "type": "timestamp"}, + ], + "description": "Payments fact table.", + "mode": "published", + "name": "dbt.source.stripe.payments", + "catalog": "public", + "schema_": "stripe", + "table": "payments", + }, + ), + ( + "/nodes/transform/", + { + "query": ( + "SELECT c.id, " + " c.first_name, " + " c.last_name, " + " COUNT(1) AS order_cnt " + "FROM dbt.source.jaffle_shop.orders o " + "JOIN dbt.source.jaffle_shop.customers c ON o.user_id = c.id " + "GROUP BY c.id, " + " c.first_name, " + " c.last_name " + ), + "description": "Country level agg table", + "mode": "published", + "name": "dbt.transform.customer_agg", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "id", "type": "int"}, + {"name": "item_name", "type": "string"}, + {"name": "sold_count", "type": "int"}, + {"name": "price_per_unit", "type": "float"}, + {"name": "psp", "type": "string"}, + ], + "description": "A source table for sales", + "mode": "published", + "name": "default.sales", + "catalog": "default", + "schema_": "revenue", + "table": "sales", + }, + ), + ( + "/nodes/dimension/", + { + "description": "Item dimension", + "query": ( + "SELECT item_name account_type_classification FROM default.sales" + ), + "mode": "published", + "name": "default.items", + "primary_key": ["account_type_classification"], + }, + ), + ( + "/nodes/metric/", + { + "description": "Total units sold", + "query": "SELECT SUM(sold_count) as default_DOT_items_sold_count FROM default.sales", + "mode": "published", + "name": "default.items_sold_count", + }, + ), + ( + "/nodes/metric/", + { + "description": "Total profit", + "query": "SELECT SUM(sold_count * price_per_unit) FROM default.sales", + "mode": "published", + "name": "default.total_profit", + }, + ), + ( + "/nodes/dbt.source.jaffle_shop.orders/link", + { + "dimension_node": "dbt.dimension.customers", + "join_type": "inner", + "join_on": ( + "dbt.source.jaffle_shop.orders.user_id = dbt.dimension.customers.id" + ), + }, + ), +) + +# lateral view explode/cross join unnest examples +LATERAL_VIEW = ( # type: ignore + ( + "/nodes/source/", + { + "columns": [ + {"name": "id", "type": "int"}, + {"name": "painter", "type": "string"}, + { + "name": "colors", + "type": "map", + }, + ], + "description": "Murals", + "mode": "published", + "name": "basic.murals", + "catalog": "public", + "schema_": "basic", + "table": "murals", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "color_id", "type": "int"}, + {"name": "color_name", "type": "string"}, + { + "name": "opacity", + "type": "float", + }, + { + "name": "luminosity", + "type": "float", + }, + { + "name": "garishness", + "type": "float", + }, + ], + "description": "Patch", + "mode": "published", + "name": "basic.patches", + "catalog": "public", + "schema_": "basic", + "table": "patches", + }, + ), + ( + "/nodes/transform/", + { + "query": """ + SELECT + cast(color_id as string) color_id, + color_name, + opacity, + luminosity, + garishness + FROM basic.patches + """, + "description": "Corrected patches", + "mode": "published", + "name": "basic.corrected_patches", + }, + ), + ( + "/nodes/dimension/", + { + "query": """ + SELECT + id AS mural_id, + t.color_id, + t.color_name color_name + FROM + ( + select + id, + colors + from basic.murals + ) murals + CROSS JOIN UNNEST(colors) AS t(color_id, color_name) + """, + "description": "Mural paint colors", + "mode": "published", + "name": "basic.paint_colors_trino", + "primary_key": ["color_id"], + }, + ), + ( + "/nodes/dimension/", + { + "query": """ + SELECT + id AS mural_id, + color_id, + color_name + FROM + ( + select + id, + EXPLODE(colors) AS (color_id, color_name) + from basic.murals + ) + """, + "description": "Mural paint colors", + "mode": "published", + "name": "basic.paint_colors_spark", + "primary_key": ["color_id"], + }, + ), + ( + "/nodes/metric/", + { + "query": """ + SELECT AVG(luminosity) FROM basic.corrected_patches + """, + "description": "Average luminosity of color patch", + "mode": "published", + "name": "basic.avg_luminosity_patches", + }, + ), +) + +COMPLEX_DIMENSION_LINK = ( + ( + "/nodes/source/", + { + "columns": [ + {"name": "user_id", "type": "int"}, + {"name": "event_start_date", "type": "int"}, + {"name": "event_end_date", "type": "int"}, + {"name": "elapsed_secs", "type": "int"}, + {"name": "user_registration_country", "type": "string"}, + ], + "description": "Events table", + "mode": "published", + "name": "default.events_table", + "catalog": "default", + "schema_": "examples", + "table": "events", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "user_id", "type": "int"}, + {"name": "snapshot_date", "type": "int"}, + {"name": "registration_country", "type": "string"}, + {"name": "residence_country", "type": "string"}, + {"name": "account_type", "type": "string"}, + ], + "description": "Users table", + "mode": "published", + "name": "default.users_table", + "catalog": "default", + "schema_": "examples", + "table": "users", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "country_code", "type": "string"}, + {"name": "name", "type": "string"}, + {"name": "population", "type": "int"}, + ], + "description": "Countries table", + "mode": "published", + "name": "default.countries_table", + "catalog": "default", + "schema_": "examples", + "table": "countries", + }, + ), + ( + "/nodes/transform/", + { + "description": "Events fact", + "query": """ + SELECT + user_id, + event_start_date, + event_end_date, + elapsed_secs, + user_registration_country + FROM default.events_table + """, + "mode": "published", + "name": "default.events", + }, + ), + ( + "/nodes/dimension/", + { + "description": "Users", + "query": """ + SELECT + user_id, + snapshot_date, + registration_country, + residence_country, + account_type + FROM default.users_table + """, + "mode": "published", + "name": "default.users", + "primary_key": ["user_id", "snapshot_date"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Countries", + "query": """ + SELECT + country_code, + name, + population + FROM default.countries_table + """, + "mode": "published", + "name": "default.countries", + "primary_key": ["country_code"], + }, + ), + ( + "/nodes/metric/", + { + "description": "Elapsed Time in Seconds", + "query": "SELECT SUM(elapsed_secs) FROM default.events", + "mode": "published", + "name": "default.elapsed_secs", + }, + ), +) + +DIMENSION_LINK = ( # type: ignore + ( + "/nodes/source/", + { + "columns": [ + {"name": "dateint", "type": "int"}, + {"name": "month", "type": "int"}, + {"name": "year", "type": "int"}, + {"name": "day", "type": "int"}, + ], + "description": "Date table", + "mode": "published", + "name": "default.date", + "catalog": "default", + "schema_": "examples", + "table": "date", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "country_code", "type": "string"}, + {"name": "name", "type": "string"}, + {"name": "formation_date", "type": "int"}, + {"name": "last_election_date", "type": "int"}, + ], + "description": "Countries table", + "mode": "published", + "name": "default.countries", + "catalog": "default", + "schema_": "examples", + "table": "countries", + }, + ), + ( + "/nodes/source/", + { + "columns": [ + {"name": "user_id", "type": "int"}, + {"name": "birth_country", "type": "string"}, + {"name": "residence_country", "type": "string"}, + {"name": "age", "type": "int"}, + ], + "description": "Users table", + "mode": "published", + "name": "default.users", + "catalog": "default", + "schema_": "examples", + "table": "users", + }, + ), + ( + "/nodes/dimension/", + { + "description": "Date dimension", + "query": """ + SELECT + dateint, + month, + year, + day + FROM default.date + """, + "mode": "published", + "name": "default.date_dim", + "primary_key": ["dateint"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "Country dimension", + "query": """ + SELECT + country_code, + name, + formation_date, + last_election_date + FROM default.countries + """, + "mode": "published", + "name": "default.special_country_dim", + "primary_key": ["country_code"], + }, + ), + ( + "/nodes/dimension/", + { + "description": "User dimension", + "query": """ + SELECT + user_id, + birth_country, + residence_country, + age + FROM default.users + """, + "mode": "published", + "name": "default.user_dim", + "primary_key": ["user_id"], + }, + ), + ( + "/nodes/metric/", + { + "description": "Average User Age", + "query": """ + SELECT + AVG(age) + FROM default.user_dim + """, + "mode": "published", + "name": "default.avg_user_age", + }, + ), + ( + "/nodes/default.user_dim/link", + { + "dimension_node": "default.special_country_dim", + "join_type": "left", + "join_on": ( + "default.user_dim.birth_country = default.special_country_dim.country_code" + ), + "role": "birth_country", + }, + ), + ( + "/nodes/default.user_dim/link", + { + "dimension_node": "default.special_country_dim", + "join_type": "left", + "join_on": ( + "default.user_dim.residence_country = default.special_country_dim.country_code" + ), + "role": "residence_country", + }, + ), + ( + "/nodes/default.special_country_dim/link", + { + "dimension_node": "default.date_dim", + "join_type": "left", + "join_on": ( + "default.special_country_dim.formation_date = default.date_dim.dateint" + ), + "role": "formation_date", + }, + ), + ( + "/nodes/default.special_country_dim/link", + { + "dimension_node": "default.date_dim", + "join_type": "left", + "join_on": ( + "default.special_country_dim.last_election_date = default.date_dim.dateint" + ), + "role": "last_election_date", + }, + ), +) + +EXAMPLES = { # type: ignore + "ROADS": ROADS, + "NAMESPACED_ROADS": NAMESPACED_ROADS, + "ACCOUNT_REVENUE": ACCOUNT_REVENUE, + "BASIC": BASIC, + "BASIC_IN_DIFFERENT_CATALOG": BASIC_IN_DIFFERENT_CATALOG, + "EVENT": EVENT, + "DBT": DBT, + "LATERAL_VIEW": LATERAL_VIEW, + "DIMENSION_LINK": DIMENSION_LINK, +} + + +COLUMN_MAPPINGS = { + "public.basic.comments": [ + Column(name="id", type=IntegerType(), order=0), + Column(name="user_id", type=IntegerType(), order=1), + Column(name="timestamp", type=TimestampType(), order=2), + Column(name="text", type=StringType(), order=3), + ], + "default.roads.repair_orders": [ + Column(name="repair_order_id", type=IntegerType(), order=0), + Column(name="municipality_id", type=StringType(), order=1), + Column(name="hard_hat_id", type=IntegerType(), order=2), + Column(name="order_date", type=TimestampType(), order=3), + Column(name="required_date", type=TimestampType(), order=4), + Column(name="dispatched_date", type=TimestampType(), order=5), + Column(name="dispatcher_id", type=IntegerType(), order=6), + Column(name="rating", type=IntegerType(), order=7), + ], + "default.roads.repair_orders_view": [ + Column(name="repair_order_id", type=IntegerType(), order=0), + Column(name="municipality_id", type=StringType(), order=1), + Column(name="hard_hat_id", type=IntegerType(), order=2), + Column(name="order_date", type=TimestampType(), order=3), + Column(name="required_date", type=TimestampType(), order=4), + Column(name="dispatched_date", type=TimestampType(), order=5), + Column(name="dispatcher_id", type=IntegerType(), order=6), + Column(name="rating", type=IntegerType(), order=7), + ], + "public.main.view_foo": [ + Column(name="one", type=IntegerType(), order=0), + Column(name="two", type=StringType(), order=1), + ], +} + +QUERY_DATA_MAPPINGS = { + ( + "WITHdefault_DOT_repair_ordersAS(SELECTdefault_DOT_dispatcher.company_namedefault_DOT_dispatcher_DOT_company_name,count(default_DOT_repair_orders.repair_order_id)default_DOT_num_repair_ordersFROMroads.repair_ordersASdefault_DOT_repair_ordersLEFTOUTERJOIN(SELECTdefault_DOT_repair_orders.dispatcher_id,default_DOT_repair_orders.hard_hat_id,default_DOT_repair_orders.municipality_id,default_DOT_repair_orders.repair_order_idFROMroads.repair_ordersASdefault_DOT_repair_orders)ASdefault_DOT_repair_orderONdefault_DOT_repair_orders.repair_order_id=default_DOT_repair_order.repair_order_idLEFTOUTERJOIN(SELECTdefault_DOT_dispatchers.company_name,default_DOT_dispatchers.dispatcher_idFROMroads.dispatchersASdefault_DOT_dispatchers)ASdefault_DOT_dispatcherONdefault_DOT_repair_order.dispatcher_id=default_DOT_dispatcher.dispatcher_idGROUPBYdefault_DOT_dispatcher.company_name),default_DOT_repair_order_detailsAS(SELECTdefault_DOT_dispatcher.company_namedefault_DOT_dispatcher_DOT_company_name,avg(default_DOT_repair_order_details.price)ASdefault_DOT_avg_repair_priceFROMroads.repair_order_detailsASdefault_DOT_repair_order_detailsLEFTOUTERJOIN(SELECTdefault_DOT_repair_orders.dispatcher_id,default_DOT_repair_orders.hard_hat_id,default_DOT_repair_orders.municipality_id,default_DOT_repair_orders.repair_order_idFROMroads.repair_ordersASdefault_DOT_repair_orders)ASdefault_DOT_repair_orderONdefault_DOT_repair_order_details.repair_order_id=default_DOT_repair_order.repair_order_idLEFTOUTERJOIN(SELECTdefault_DOT_dispatchers.company_name,default_DOT_dispatchers.dispatcher_idFROMroads.dispatchersASdefault_DOT_dispatchers)ASdefault_DOT_dispatcherONdefault_DOT_repair_order.dispatcher_id=default_DOT_dispatcher.dispatcher_idGROUPBYdefault_DOT_dispatcher.company_name)SELECTdefault_DOT_repair_orders.default_DOT_num_repair_orders,default_DOT_repair_order_details.default_DOT_avg_repair_price,COALESCE(default_DOT_repair_orders.default_DOT_dispatcher_DOT_company_name,default_DOT_repair_order_details.default_DOT_dispatcher_DOT_company_name)default_DOT_dispatcher_DOT_company_nameFROMdefault_DOT_repair_ordersFULLOUTERJOINdefault_DOT_repair_order_detailsONdefault_DOT_repair_orders.default_DOT_dispatcher_DOT_company_name=default_DOT_repair_order_details.default_DOT_dispatcher_DOT_company_nameLIMIT10" + ) + .strip() + .replace('"', "") + .replace("\n", "") + .replace("\t", "") + .replace(" ", ""): QueryWithResults( + **{ + "id": "bd98d6be-e2d2-413e-94c7-96d9411ddee2", + "submitted_query": ( + "SELECT avg(repair_order_details.price) AS " + "default_DOT_avg_repair_price,\\n\\tdispatcher.company_name," + "\\n\\tcount(repair_orders.repair_order_id) AS default_DOT_num_repair_orders" + "default_DOT_num_repair_orders \\n FROM roads.repair_order_details AS " + "repair_order_details LEFT OUTER JOIN (SELECT " + "repair_orders.dispatcher_id,\\n\\trepair_orders.hard_hat_id,\\n\\t" + "repair_orders.municipality_id,\\n\\trepair_orders.repair_order_id " + "\\n FROM roads.repair_orders AS repair_orders) AS repair_order ON " + "repair_order_details.repair_order_id = repair_order.repair_order_id\\nLEFT " + "OUTER JOIN (SELECT dispatchers.company_name,\\n\\tdispatchers.dispatcher_id " + "\\n FROM roads.dispatchers AS dispatchers) AS dispatcher ON " + "repair_order.dispatcher_id = dispatcher.dispatcher_id \\n GROUP BY " + "dispatcher.company_name\\nLIMIT 10" + ), + "state": QueryState.FINISHED, + "results": [ + { + "columns": [ + {"name": "default_DOT_num_repair_orders", "type": "int"}, + {"name": "default_DOT_avg_repair_price", "type": "float"}, + {"name": "company_name", "type": "str"}, + ], + "rows": [ + (1.0, "Foo", 100), + (2.0, "Bar", 200), + ], + "sql": "", + }, + ], + "errors": [], + } + ), +} diff --git a/datajunction-server/tests/integration/__init__.py b/datajunction-server/tests/integration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/tests/integration/basic_test.py b/datajunction-server/tests/integration/basic_test.py new file mode 100644 index 000000000..8a3a7e809 --- /dev/null +++ b/datajunction-server/tests/integration/basic_test.py @@ -0,0 +1,23 @@ +""" +Basic integration tests. +""" + +import pytest +from sqlalchemy.engine import create_engine + + +@pytest.mark.integration_test +def test_query() -> None: + """ + Test a simple query. + """ + engine = create_engine("dj://localhost:8000/0") + connection = engine.connect() + + sql = """ +SELECT "core.users.gender", "core.num_comments" +FROM metrics +GROUP BY "core.users.gender" + """ + results = list(connection.execute(sql)) + assert results == [("female", 5), ("non-binary", 10), ("male", 7)] diff --git a/datajunction-server/tests/internal/authentication/basic_test.py b/datajunction-server/tests/internal/authentication/basic_test.py new file mode 100644 index 000000000..0790f1841 --- /dev/null +++ b/datajunction-server/tests/internal/authentication/basic_test.py @@ -0,0 +1,168 @@ +""" +Tests for basic auth helper functions +""" + +import pytest +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.constants import AUTH_COOKIE +from datajunction_server.errors import DJException +from datajunction_server.internal.access.authentication import basic + + +@pytest.mark.asyncio +async def test_login_with_username_and_password(client: AsyncClient): + """ + Test validating a username and a password + """ + await client.post( + "/basic/user/", + data={"email": "dj@datajunction.io", "username": "dj", "password": "dj"}, + ) + response = await client.post( + "/basic/login/", + data={"username": "dj", "password": "dj"}, + ) + assert response.status_code in (200, 201) + assert response.cookies.get(AUTH_COOKIE) + + +@pytest.mark.asyncio +async def test_logout(client: AsyncClient): + """ + Test validating logging out + """ + await client.post("/logout/") + + +def test_hash_and_verify_password(): + """ + Test hashing a password and verifying a password against a hash + """ + hashed_password = basic.get_password_hash(password="foo") + assert basic.validate_password_hash( + plain_password="foo", + hashed_password=hashed_password, + ) + + +@pytest.mark.asyncio +async def test_validate_username_and_password( + client: AsyncClient, + session: AsyncSession, +): + """ + Test validating a username and a password + """ + await client.post( + "/basic/user/", + data={"email": "dj@datajunction.io", "username": "dj", "password": "dj"}, + ) + user = await basic.validate_user_password( + username="dj", + password="dj", + session=session, + ) + assert user.username == "dj" + + +@pytest.mark.asyncio +async def test_get_user(client: AsyncClient, session: AsyncSession): + """ + Test getting a user + """ + await client.post( + "/basic/user/", + data={"email": "dj@datajunction.io", "username": "dj", "password": "dj"}, + ) + user = await basic.get_user(username="dj", session=session) + assert user.username == "dj" + + +@pytest.mark.asyncio +async def test_get_user_raise_on_user_not_found(session: AsyncSession): + """ + Test raising when trying to get a user that doesn't exist + """ + with pytest.raises(DJException) as exc_info: + await basic.get_user(username="dj", session=session) + assert "User dj not found" in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_login_raise_on_user_not_found(client: AsyncClient): + """ + Test raising when trying to login as a user that doesn't exist + """ + response = await client.post( + "/basic/login/", + data={"username": "foo", "password": "bar"}, + ) + assert response.status_code == 401 + + +@pytest.mark.asyncio +async def test_fail_invalid_credentials(client: AsyncClient, session: AsyncSession): + """ + Test failing on invalid user credentials + """ + await client.post( + "/basic/user/", + data={"email": "dj@datajunction.io", "username": "dj", "password": "incorrect"}, + ) + with pytest.raises(DJException) as exc_info: + await basic.validate_user_password( + username="dj", + password="dj", + session=session, + ) + assert "Invalid password for user dj" in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_fail_on_user_already_exists(client: AsyncClient): + """ + Test failing when creating a user that already exists + """ + await client.post( + "/basic/user/", + data={"email": "dj@datajunction.io", "username": "dj", "password": "dj"}, + ) + response = await client.post( + "/basic/user/", + data={"email": "dj@datajunction.io", "username": "dj", "password": "dj"}, + ) + assert response.status_code == 409 + assert response.json() == { + "message": "User dj already exists.", + "errors": [ + { + "code": 2, + "message": "User dj already exists.", + "debug": None, + "context": "", + }, + ], + "warnings": [], + } + + +@pytest.mark.asyncio +async def test_whoami(client: AsyncClient): + """ + Test the /whoami/ endpoint + """ + response = await client.get("/whoami/") + assert response.status_code in (200, 201) + assert response.json() == { + "id": 1, + "username": "dj", + "email": None, + "name": None, + "oauth_provider": "basic", + "is_admin": False, + "created_collections": [], + "created_nodes": [], + "created_tags": [], + } diff --git a/datajunction-server/tests/internal/authentication/github_test.py b/datajunction-server/tests/internal/authentication/github_test.py new file mode 100644 index 000000000..c6ecc72db --- /dev/null +++ b/datajunction-server/tests/internal/authentication/github_test.py @@ -0,0 +1,16 @@ +""" +Tests for GitHub OAuth helper functions +""" + +from datajunction_server.internal.access.authentication import github + + +def test_get_authorize_url(): + """ + Test generating a GitHub OAuth authorize url for a GitHub app client ID + """ + assert github.get_authorize_url("foo") == ( + "https://github.com/login/oauth/authorize?" + "client_id=foo&scope=read:user&redirect_uri=" + "http://localhost:8000/github/token/" + ) diff --git a/datajunction-server/tests/internal/authentication/http_test.py b/datajunction-server/tests/internal/authentication/http_test.py new file mode 100644 index 000000000..cc0c44d74 --- /dev/null +++ b/datajunction-server/tests/internal/authentication/http_test.py @@ -0,0 +1,127 @@ +""" +Test internal http authentication logic +""" + +import asyncio +from unittest.mock import MagicMock + +import pytest + +from datajunction_server.errors import DJException +from datajunction_server.internal.access.authentication.http import DJHTTPBearer +from datajunction_server.models.user import OAuthProvider, UserOutput + +EXAMPLE_TOKEN = ( + "eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..SxGbG0NRepMY4z9-2-ZZdg.ug" + "0FvJUoybiGGpUItL4VbM1O_oinX7dMBUM1V3OYjv30fddn9m9UrrXxv3ERIyKu2zVJ" + "xx1gSoM5k8petUHCjatFQqA-iqnvjloFKEuAmxLdCHKUDgfKzCIYtbkDcxtzXLuqlj" + "B0-ConD6tpjMjFxNrp2KD4vwaS0oGsDJGqXlMo0MOhe9lHMLraXzOQ6xDgDFHiFert" + "Fc0T_9jYkcpmVDPl9pgPf55R.sKF18rttq1OZ_EjZqw8Www" +) + + +def test_dj_http_bearer_raise_when_unauthenticated(): + """ + Test raising when no cookie or auth headers are provided + """ + bearer = DJHTTPBearer() + request = MagicMock() + request.cookies.get.return_value = None + request.headers.get.return_value = None + with pytest.raises(DJException) as exc_info: + asyncio.run(bearer(request)) + assert "Not authenticated" in str(exc_info.value) + + +def test_dj_http_bearer_raise_with_empty_bearer_token(): + """ + Test raising when the authorization header has an empty bearer token + """ + bearer = DJHTTPBearer() + request = MagicMock() + request.cookies.get.return_value = None + request.headers.get.return_value = "Bearer " + with pytest.raises(DJException) as exc_info: + asyncio.run(bearer(request)) + assert "Not authenticated" in str(exc_info.value) + + +def test_dj_http_bearer_raise_with_unsupported_scheme(): + """ + Test raising when the authorization header scheme is not a supported one + """ + bearer = DJHTTPBearer() + request = MagicMock() + request.cookies.get.return_value = None + request.headers.get.return_value = f"Foo {EXAMPLE_TOKEN}" + with pytest.raises(DJException) as exc_info: + asyncio.run(bearer(request)) + assert "Invalid authentication credentials" in str(exc_info.value) + + +def test_dj_http_bearer_raise_with_non_jwt_token(): + """ + Test raising when the token can't be parsed as a JWT + """ + bearer = DJHTTPBearer() + request = MagicMock() + request.cookies.get.return_value = None + request.headers.get.return_value = "Foo NotAJWT" + with pytest.raises(DJException) as exc_info: + asyncio.run(bearer(request)) + assert "Invalid authentication credentials" in str(exc_info.value) + + +def test_dj_http_bearer_w_cookie(): + """ + Test using the DJHTTPBearer middleware with a cookie + """ + bearer = DJHTTPBearer() + request = MagicMock() + request.cookies.get.return_value = EXAMPLE_TOKEN + asyncio.run(bearer(request)) + assert UserOutput.from_orm(request.state.user).dict() == { + "id": 1, + "username": "dj", + "email": None, + "name": None, + "oauth_provider": OAuthProvider.BASIC, + "is_admin": False, + "created_collections": [], + "created_nodes": [], + "created_tags": [], + } + + +def test_dj_http_bearer_w_auth_headers(): + """ + Test using the DJHTTPBearer middleware with an authorization header + """ + bearer = DJHTTPBearer() + request = MagicMock() + request.cookies.get.return_value = None + request.headers.get.return_value = f"Bearer {EXAMPLE_TOKEN}" + asyncio.run(bearer(request)) + assert UserOutput.from_orm(request.state.user).dict() == { + "id": 1, + "username": "dj", + "email": None, + "name": None, + "oauth_provider": OAuthProvider.BASIC, + "is_admin": False, + "created_collections": [], + "created_nodes": [], + "created_tags": [], + } + + +def test_raise_on_non_jwt_cookie(): + """ + Test using the DJHTTPBearer middleware with a cookie + """ + bearer = DJHTTPBearer() + request = MagicMock() + request.cookies.get.return_value = "NotAJWT" + with pytest.raises(DJException) as exc_info: + asyncio.run(bearer(request)) + assert "Cannot decode authorization token" in str(exc_info.value) diff --git a/datajunction-server/tests/internal/authentication/token_test.py b/datajunction-server/tests/internal/authentication/token_test.py new file mode 100644 index 000000000..a5d2aea18 --- /dev/null +++ b/datajunction-server/tests/internal/authentication/token_test.py @@ -0,0 +1,29 @@ +""" +Test JWT helper functions +""" + +from datetime import timedelta + +from datajunction_server.internal.access.authentication import tokens + + +def test_create_and_get_token(): + """ + Test creating a JWT and getting it back from a request + """ + jwe_string = tokens.create_token( + data={"foo": "bar"}, + secret="a-fake-secretkey", + iss="http://localhost:8000/", + expires_delta=timedelta(minutes=30), + ) + data = tokens.decode_token(jwe_string) + assert data["foo"] == "bar" + + +def test_encrypt_and_decrypt(): + """ + Test encrypting and decrypting a value + """ + encrypted_string = tokens.encrypt("foo") + assert tokens.decrypt(encrypted_string) == "foo" diff --git a/datajunction-server/tests/internal/authentication/whoami_test.py b/datajunction-server/tests/internal/authentication/whoami_test.py new file mode 100644 index 000000000..5f83958ec --- /dev/null +++ b/datajunction-server/tests/internal/authentication/whoami_test.py @@ -0,0 +1,30 @@ +""" +Tests for whoami router +""" + +import pytest +from httpx import AsyncClient + +from datajunction_server.internal.access.authentication.tokens import decode_token + + +@pytest.mark.asyncio +async def test_whoami(client: AsyncClient): + """ + Test /whoami endpoint + """ + response = await client.get("/whoami/") + assert response.status_code in (200, 201) + assert response.json()["username"] == "dj" + + +@pytest.mark.asyncio +async def test_short_lived_token(client: AsyncClient): + """ + Test getting a short-lived token from the /token endpoint + """ + response = await client.get("/token/") + assert response.status_code in (200, 201) + data = response.json() + user = decode_token(data["token"]) + assert user["username"] == "dj" diff --git a/datajunction-server/tests/internal/caching/cachelib_cache_test.py b/datajunction-server/tests/internal/caching/cachelib_cache_test.py new file mode 100644 index 000000000..10106d799 --- /dev/null +++ b/datajunction-server/tests/internal/caching/cachelib_cache_test.py @@ -0,0 +1,62 @@ +""" +Tests for cachelib cache implementation +""" + +from starlette.requests import Headers, Request + +from datajunction_server.internal.caching.cachelib_cache import CachelibCache, get_cache +from datajunction_server.internal.caching.noop_cache import NoOpCache + + +def test_cachelib_cache(): + """ + Test getting, setting, and deleting using the cachelib implementation + """ + cache = CachelibCache() + assert cache.set(key="foo", value="bar", timeout=300) is None + assert cache.get(key="foo") == "bar" + assert cache.delete(key="foo") is None + assert cache.get(key="foo") is None + + +def test_cachelib_cache_nocache_headers(): + """Test cachelib cache with various request headers""" + + # Test with "no-cache" in headers + nocache_request = Request( + { + "type": "http", + "method": "GET", + "path": "/", + "headers": Headers({"Cache-Control": "no-cache"}).raw, + "query_string": b"", + }, + ) + get_cache(nocache_request) + assert isinstance(get_cache(nocache_request), NoOpCache) + + # Test without "no-cache" in headers + request = Request( + { + "type": "http", + "method": "GET", + "path": "/", + "headers": Headers({"Cache-Control": "max-age=3600"}).raw, + "query_string": b"", + }, + ) + get_cache(request) + assert isinstance(get_cache(request), CachelibCache) + + # Test with no headers at all + headerless_request = Request( + { + "type": "http", + "method": "GET", + "path": "/", + "headers": [], + "query_string": b"", + }, + ) + + assert isinstance(get_cache(headerless_request), CachelibCache) diff --git a/datajunction-server/tests/internal/caching/noop_cache_test.py b/datajunction-server/tests/internal/caching/noop_cache_test.py new file mode 100644 index 000000000..00de78efc --- /dev/null +++ b/datajunction-server/tests/internal/caching/noop_cache_test.py @@ -0,0 +1,17 @@ +""" +Tests for noop cache +""" + +from datajunction_server.internal.caching.noop_cache import NoOpCache + + +def test_noop_cache(): + """ + Test getting, setting, and deleting using a NoOpCache implementation + """ + cache = NoOpCache() + assert cache.set(key="foo", value="bar") is None + assert ( + cache.get(key="foo") is None + ) # Returns None because NoOp doesn't actually cache + assert cache.delete(key="foo") is None diff --git a/datajunction-server/tests/migrations_test.py b/datajunction-server/tests/migrations_test.py new file mode 100644 index 000000000..868bc08c5 --- /dev/null +++ b/datajunction-server/tests/migrations_test.py @@ -0,0 +1,71 @@ +"""Verify alembic migrations.""" + +import pytest +from alembic.autogenerate import compare_metadata +from alembic.config import Config +from alembic.runtime.environment import EnvironmentContext +from alembic.runtime.migration import MigrationContext +from alembic.script import ScriptDirectory +from httpx import AsyncClient +from sqlalchemy import create_engine +from sqlalchemy.engine.base import Connection +from testcontainers.postgres import PostgresContainer + +from datajunction_server.database.base import Base + + +@pytest.fixture(scope="function", name="connection") +def connection(postgres_container: PostgresContainer) -> Connection: + """ + Create a Postgres connection for verifying models. + """ + url = postgres_container.get_connection_url() + engine = create_engine( + url=url, + ) + with engine.connect() as conn: + transaction = conn.begin() + yield conn + transaction.rollback() + + +def test_migrations_are_current(connection): + """ + Verify that the alembic migrations are in line with the models. + """ + target_metadata = Base.metadata + + config = Config("alembic.ini") + config.set_main_option("script_location", "datajunction_server/alembic") + script = ScriptDirectory.from_config(config) + + context = EnvironmentContext( + config, + script, + fn=lambda rev, _: script._upgrade_revs("head", rev), + ) + context.configure(connection=connection) + context.run_migrations() + + # Don't use compare_type due to false positives. + migrations_state = MigrationContext.configure( + connection, + opts={"compare_type": False}, + ) + diff = compare_metadata(migrations_state, target_metadata) + assert diff == [], "The alembic migrations do not match the models." + + +@pytest.mark.asyncio +async def test_openapi_schema(client: AsyncClient): + """ + Fetch and validate the OpenAPI schema. + """ + response = await client.get("/openapi.json") + assert response.status_code == 200, "Failed to fetch OpenAPI schema" + + schema = response.json() + assert "openapi" in schema, "Missing 'openapi' version field" + assert "info" in schema, "Missing 'info' section" + assert "paths" in schema, "Missing 'paths' section" + assert "components" in schema, "Missing 'components' section" diff --git a/datajunction-server/tests/models/__init__.py b/datajunction-server/tests/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/tests/models/catalog_test.py b/datajunction-server/tests/models/catalog_test.py new file mode 100644 index 000000000..6b1060f25 --- /dev/null +++ b/datajunction-server/tests/models/catalog_test.py @@ -0,0 +1,20 @@ +""" +Tests for ``datajunction_server.models.catalog``. +""" + +from datajunction_server.database.catalog import Catalog + + +def test_catalog_str_and_hash(): + """ + Test that a catalog instance works properly with str and hash + """ + spark = Catalog(id=1, name="spark") + assert str(spark) == "spark" + assert hash(spark) == 1 + trino = Catalog(id=2, name="trino") + assert str(trino) == "trino" + assert hash(trino) == 2 + druid = Catalog(id=3, name="druid") + assert str(druid) == "druid" + assert hash(druid) == 3 diff --git a/datajunction-server/tests/models/hash_test.py b/datajunction-server/tests/models/hash_test.py new file mode 100644 index 000000000..34e1bb2a6 --- /dev/null +++ b/datajunction-server/tests/models/hash_test.py @@ -0,0 +1,21 @@ +""" +Tests for ``datajunction_server.models.database``. +""" + +import datajunction_server.sql.parsing.types as ct +from datajunction_server.database.column import Column +from datajunction_server.database.database import Database, Table + + +def test_hash() -> None: + """ + Test the hash method to compare models. + """ + database = Database(id=1, name="test", URI="sqlite://") + assert database in {database} + + table = Table(id=1, database=database, table="table") + assert table in {table} + + column = Column(id=1, name="test", type=ct.IntegerType(), order=0) + assert column in {column} diff --git a/datajunction-server/tests/models/measure_test.py b/datajunction-server/tests/models/measure_test.py new file mode 100644 index 000000000..11a37cf87 --- /dev/null +++ b/datajunction-server/tests/models/measure_test.py @@ -0,0 +1,27 @@ +""" +Tests for ``datajunction_server.models.measures``. +""" + +import datajunction_server.sql.parsing.types as ct +from datajunction_server.database.column import Column +from datajunction_server.database.measure import Measure +from datajunction_server.models.measure import AggregationRule + + +def test_measures_backpopulate() -> None: + """ + Test the Measure model and that it backpopulates Column and vice versa. + """ + column1 = Column(name="finalized_amount", type=ct.StringType(), order=0) + column2 = Column(name="final_amount", type=ct.StringType(), order=0) + measure = Measure( + name="amount", + columns=[column1, column2], + additive=AggregationRule.ADDITIVE, + ) + assert column1.measure == measure + assert column2.measure == measure + + column3 = Column(name="amount3", type=ct.StringType(), measure=measure, order=0) + column4 = Column(name="amount4", type=ct.StringType(), measure=measure, order=0) + assert measure.columns == [column1, column2, column3, column4] diff --git a/datajunction-server/tests/models/node_test.py b/datajunction-server/tests/models/node_test.py new file mode 100644 index 000000000..f7537c281 --- /dev/null +++ b/datajunction-server/tests/models/node_test.py @@ -0,0 +1,319 @@ +""" +Tests for ``datajunction_server.models.node``. +""" + +import pytest +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database.availabilitystate import AvailabilityState +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.models.node import ( + AvailabilityStateBase, + NodeCursor, + PartitionAvailability, +) +from datajunction_server.models.node_type import NodeType +from datajunction_server.typing import UTCDatetime + + +def test_node_relationship(session: AsyncSession) -> None: + """ + Test the n:n self-referential relationships. + """ + node_a = Node(name="A", current_version="1") + node_a_rev = NodeRevision(name="A", version="1", node=node_a) + + node_b = Node(name="B", current_version="1") + node_a_rev = NodeRevision(name="B", version="1", node=node_b) + + node_c = Node(name="C", current_version="1") + node_c_rev = NodeRevision( + name="C", + version="1", + node=node_c, + parents=[node_a, node_b], + ) + + session.add(node_c_rev) + + assert node_a.children == [node_c_rev] + assert node_b.children == [node_c_rev] + assert node_c.children == [] + + assert node_a_rev.parents == [] + assert node_a_rev.parents == [] + assert node_c_rev.parents == [node_a, node_b] + + +def test_extra_validation() -> None: + """ + Test ``extra_validation``. + """ + node = Node(name="A", type=NodeType.METRIC, current_version="1") + node_revision = NodeRevision( + name=node.name, + type=node.type, + node=node, + version="1", + ) + with pytest.raises(Exception) as excinfo: + node_revision.extra_validation() + assert str(excinfo.value) == "Node A of type metric needs a query" + + node = Node(name="A", type=NodeType.METRIC, current_version="1") + node_revision = NodeRevision( + name=node.name, + type=node.type, + node=node, + version="1", + query="SELECT count(repair_order_id) AS Anum_repair_orders FROM repair_orders", + ) + node_revision.extra_validation() + + node = Node(name="A", type=NodeType.METRIC, current_version="1") + node_revision = NodeRevision( + name=node.name, + type=node.type, + node=node, + version="1", + query="SELECT repair_order_id + " + "repair_order_id AS Anum_repair_orders " + "FROM repair_orders", + ) + with pytest.raises(Exception) as excinfo: + node_revision.extra_validation() + assert str(excinfo.value) == ( + "Metric A has an invalid query, should have an aggregate expression" + ) + + node = Node(name="AA", type=NodeType.METRIC, current_version="1") + node_revision = NodeRevision( + name=node.name, + type=node.type, + node=node, + version="1", + query="SELECT ln(count(distinct repair_order_id)) FROM repair_orders", + ) + node_revision.extra_validation() + + node = Node(name="A", type=NodeType.TRANSFORM, current_version="1") + node_revision = NodeRevision( + name=node.name, + type=node.type, + node=node, + version="1", + query="SELECT * FROM B", + ) + node_revision.extra_validation() + + node = Node(name="A", type=NodeType.TRANSFORM, current_version="1") + node_revision = NodeRevision( + name=node.name, + type=node.type, + node=node, + version="1", + ) + with pytest.raises(Exception) as excinfo: + node_revision.extra_validation() + assert str(excinfo.value) == "Node A of type transform needs a query" + + node = Node(name="A", type=NodeType.CUBE, current_version="1") + node_revision = NodeRevision(name=node.name, type=node.type, node=node, version="1") + with pytest.raises(Exception) as excinfo: + node_revision.extra_validation() + assert str(excinfo.value) == "Node A of type cube node needs cube elements" + + node = Node(name="A", type=NodeType.TRANSFORM, current_version="1") + node_revision = NodeRevision( + name=node.name, + type=node.type, + node=node, + version="1", + query="SELECT * FROM B", + required_dimensions=["B.x"], + ) + with pytest.raises(Exception) as excinfo: + node_revision.extra_validation() + + assert str(excinfo.value) == ( + "Node A of type transform cannot have " + "bound dimensions which are only for metrics." + ) + + +def test_merging_availability_simple_no_partitions() -> None: + """ + Test merging simple availability for no partitions. + """ + avail_1 = AvailabilityStateBase( + catalog="catalog", + schema_="schema", + table="foo", + valid_through_ts=111, + ) + avail_2 = AvailabilityStateBase( + catalog="catalog", + schema_="schema", + table="foo", + valid_through_ts=222, + ) + assert avail_1.merge(avail_2).dict() == { + "min_temporal_partition": None, + "max_temporal_partition": None, + "catalog": "catalog", + "schema_": "schema", + "table": "foo", + "valid_through_ts": 222, + "categorical_partitions": [], + "temporal_partitions": [], + "partitions": [], + "custom_metadata": {}, + "materialization_id": None, + } + + +def test_merging_availability_complex_no_partitions() -> None: + """ + Test merging complex availability for no partitions. + """ + avail_1 = AvailabilityStateBase( + catalog="druid", + schema_="", + table="dj_product__launchpad__launchpad_cube", + min_temporal_partition=["20230924"], + max_temporal_partition=["20230924"], + categorical_partitions=[], + temporal_partitions=[], + partitions=[], + valid_through_ts=20230924, + ) + avail_2 = AvailabilityStateBase( + catalog="druid", + schema_="", + table="dj_product__launchpad__launchpad_cube", + min_temporal_partition=["20230926"], + max_temporal_partition=["20230927"], + categorical_partitions=[], + temporal_partitions=[], + partitions=[], + valid_through_ts=20230927, + ) + assert avail_1.merge(avail_2).dict() == { + "min_temporal_partition": ["20230924"], + "max_temporal_partition": ["20230927"], + "catalog": "druid", + "schema_": "", + "table": "dj_product__launchpad__launchpad_cube", + "valid_through_ts": 20230927, + "categorical_partitions": [], + "temporal_partitions": [], + "partitions": [], + "custom_metadata": {}, + "materialization_id": None, + } + + +def test_merging_availability_complex_with_partitions() -> None: + """ + Test merging complex availability with partitions. + """ + avail_1 = AvailabilityStateBase( + catalog="iceberg", + schema_="salad", + table="dressing", + min_temporal_partition=["20230101"], + max_temporal_partition=["20230925"], + categorical_partitions=["country"], + temporal_partitions=["region_date"], + partitions=[ + PartitionAvailability( + value=[None], + valid_through_ts=20230404, + min_temporal_partition=["20230101"], + max_temporal_partition=["20230404"], + ), + PartitionAvailability( + value=["US"], + valid_through_ts=20230925, + min_temporal_partition=["20230924"], + max_temporal_partition=["20230925"], + ), + ], + valid_through_ts=20230925, + ) + avail_2 = AvailabilityState( + catalog="iceberg", + schema_="salad", + table="dressing", + min_temporal_partition=["20230101"], + max_temporal_partition=["20231010"], + categorical_partitions=["country"], + temporal_partitions=["region_date"], + partitions=[ + PartitionAvailability( + value=["US"], + valid_through_ts=20230926, + min_temporal_partition=["20230924"], + max_temporal_partition=["20230926"], + ), + PartitionAvailability( + value=["CA"], + valid_through_ts=20231010, + min_temporal_partition=["20220101"], + max_temporal_partition=["20231010"], + ), + ], + valid_through_ts=20231015, + ) + avail_1 = avail_1.merge(avail_2) + assert avail_1.dict() == { + "catalog": "iceberg", + "schema_": "salad", + "table": "dressing", + "min_temporal_partition": ["20230101"], + "max_temporal_partition": ["20231010"], + "valid_through_ts": 20231015, + "categorical_partitions": ["country"], + "temporal_partitions": ["region_date"], + "partitions": [ + { + "value": ["CA"], + "valid_through_ts": 20231010, + "min_temporal_partition": ["20220101"], + "max_temporal_partition": ["20231010"], + }, + { + "value": ["US"], + "valid_through_ts": 20230926, + "min_temporal_partition": ["20230101"], + "max_temporal_partition": ["20230926"], + }, + ], + "custom_metadata": {}, + "materialization_id": None, + } + + +def test_node_cursors() -> None: + """ + Test encoding and decoding node cursors + """ + created_at = UTCDatetime( + year=2024, + month=1, + day=1, + hour=12, + minute=30, + second=33, + ) + + cursor = NodeCursor(created_at=created_at, id=1010) + + encoded_cursor = ( + "eyJjcmVhdGVkX2F0IjogIjIwMjQtMDEtMDFUMTI6MzA6MzMiLCAiaWQiOiAxMDEwfQ==" + ) + assert cursor.encode() == encoded_cursor + + decoded_cursor = NodeCursor.decode(encoded_cursor) + assert decoded_cursor.created_at == cursor.created_at + assert decoded_cursor.id == cursor.id diff --git a/datajunction-server/tests/models/query_test.py b/datajunction-server/tests/models/query_test.py new file mode 100644 index 000000000..e9232590f --- /dev/null +++ b/datajunction-server/tests/models/query_test.py @@ -0,0 +1,102 @@ +""" +Tests for the query model. +""" + +from datetime import datetime + +import msgpack + +from datajunction_server.models.query import ( + ColumnMetadata, + QueryResults, + QueryWithResults, + StatementResults, + decode_results, + encode_results, +) +from datajunction_server.typing import QueryState + + +def test_msgpack() -> None: + """ + Test the msgpack encoding/decoding + """ + query_with_results = QueryWithResults( + catalog=None, + schema=None, + id="5599b970-23f0-449b-baea-c87a2735423b", + submitted_query="SELECT 42 AS answer", + executed_query="SELECT 42 AS answer", + scheduled=datetime(2021, 1, 1), + started=datetime(2021, 1, 2), + finished=datetime(2021, 1, 3), + state=QueryState.FINISHED, + progress=1, + output_table=None, + results=QueryResults( + __root__=[ + StatementResults( + sql="SELECT 42 AS answer", + columns=[ColumnMetadata(name="answer", type="int")], + rows=[(42,)], + row_count=1, + ), + ], + ), + next=None, + previous=None, + errors=[], + ) + encoded = msgpack.packb( + query_with_results.dict(by_alias=True), + default=encode_results, + ) + decoded = msgpack.unpackb(encoded, ext_hook=decode_results) + assert decoded == { + "id": "5599b970-23f0-449b-baea-c87a2735423b", + "submitted_query": "SELECT 42 AS answer", + "executed_query": "SELECT 42 AS answer", + "engine_name": None, + "engine_version": None, + "output_table": None, + "scheduled": datetime(2021, 1, 1, 0, 0), + "started": datetime(2021, 1, 2, 0, 0), + "finished": datetime(2021, 1, 3, 0, 0), + "progress": 1.0, + "state": "FINISHED", + "results": [ + { + "sql": "SELECT 42 AS answer", + "columns": [ + { + "name": "answer", + "type": "int", + "node": None, + "column": None, + "semantic_type": None, + "semantic_entity": None, + }, + ], + "rows": [[42]], + "row_count": 1, + }, + ], + "next": None, + "previous": None, + "errors": [], + "links": None, + } + + +def test_encode_results_unknown() -> None: + """ + Test that ``encode_results`` passes through unknown objects. + """ + assert encode_results(1) == 1 + + +def test_decode_results_unknown() -> None: + """ + Test that ``decode_results`` passes through unknown objects. + """ + assert decode_results(42, b"packed") == msgpack.ExtType(42, b"packed") diff --git a/datajunction-server/tests/service_clients_test.py b/datajunction-server/tests/service_clients_test.py new file mode 100644 index 000000000..cc89204b1 --- /dev/null +++ b/datajunction-server/tests/service_clients_test.py @@ -0,0 +1,836 @@ +""" +Tests for ``datajunction_server.service_clients``. +""" + +from unittest.mock import ANY, MagicMock + +import pytest +from pytest_mock import MockerFixture +from requests import Request + +from datajunction_server.database.engine import Engine +from datajunction_server.errors import ( + DJDoesNotExistException, + DJError, + DJQueryServiceClientEntityNotFound, + DJQueryServiceClientException, + ErrorCode, +) +from datajunction_server.models.cube_materialization import ( + CubeMetric, + DruidCubeMaterializationInput, + MeasureKey, + NodeNameVersion, +) +from datajunction_server.models.materialization import ( + GenericMaterializationInput, + MaterializationStrategy, +) +from datajunction_server.models.node_type import NodeType +from datajunction_server.models.partition import PartitionBackfill +from datajunction_server.models.query import QueryCreate +from datajunction_server.service_clients import ( + QueryServiceClient, + RequestsSessionWithEndpoint, +) + + +class TestRequestsSessionWithEndpoint: + """ + Test using requests session with endpoint. + """ + + example_endpoint = "http://pieservice:7020" + + @pytest.fixture + def requests_session(self) -> RequestsSessionWithEndpoint: + """ + Create a requests session. + """ + return RequestsSessionWithEndpoint(endpoint=self.example_endpoint) + + def test_prepare_request( + self, + requests_session: RequestsSessionWithEndpoint, + ) -> None: + """ + Test preparing request + """ + req = Request( + "GET", + f"{self.example_endpoint}/pies/?flavor=blueberry", + data=None, + ) + prepped = requests_session.prepare_request(req) + assert prepped.headers["Connection"] == "keep-alive" + + def test_make_requests( + self, + mocker: MockerFixture, + requests_session: RequestsSessionWithEndpoint, + ): + """ + Test making requests. + """ + mock_request = mocker.patch("requests.Session.request") + + requests_session.get("/pies/") + mock_request.assert_called_with( + "GET", + f"{self.example_endpoint}/pies/", + allow_redirects=True, + ) + + requests_session.post("/pies/", json={"flavor": "blueberry", "diameter": 10}) + mock_request.assert_called_with( + "POST", + f"{self.example_endpoint}/pies/", + data=None, + json={"flavor": "blueberry", "diameter": 10}, + ) + + +class TestQueryServiceClient: + """ + Test using the query service client. + """ + + endpoint = "http://queryservice:8001" + + def test_query_service_client_get_columns_for_table( + self, + mocker: MockerFixture, + ) -> None: + """ + Test the query service client. + """ + + mock_request = mocker.patch("requests.Session.request") + mock_request.return_value = MagicMock(status_code=200, text="Unknown") + query_service_client = QueryServiceClient(uri=self.endpoint) + query_service_client.get_columns_for_table("hive", "test", "pies") + mock_request.assert_called_with( + "GET", + "http://queryservice:8001/table/hive.test.pies/columns/", + params={}, + allow_redirects=True, + headers=ANY, + ) + + query_service_client.get_columns_for_table( + "hive", + "test", + "pies", + engine=Engine(name="spark", version="2.4.4"), + ) + mock_request.assert_called_with( + "GET", + "http://queryservice:8001/table/hive.test.pies/columns/", + params={"engine": "spark", "engine_version": "2.4.4"}, + allow_redirects=True, + headers=ANY, + ) + + # failed request with unknown reason + mock_request = mocker.patch("requests.Session.request") + mock_request.return_value = MagicMock(status_code=400, text="Unknown") + query_service_client = QueryServiceClient(uri=self.endpoint) + with pytest.raises(DJQueryServiceClientException) as exc_info: + query_service_client.get_columns_for_table("hive", "test", "pies") + assert "Error response from query service" in str(exc_info.value) + + # failed request with table not found + mock_request = mocker.patch("requests.Session.request") + mock_request.return_value = MagicMock(status_code=404, text="Table not found") + with pytest.raises(DJDoesNotExistException) as exc_info: + query_service_client.get_columns_for_table("hive", "test", "pies") + assert "Table not found" in str(exc_info.value) + + # no columns returned + mock_request = mocker.patch("requests.Session.request") + mock_request.return_value = MagicMock( + status_code=200, + json=MagicMock(return_value={"columns": []}), + ) + with pytest.raises(DJDoesNotExistException) as exc_info: + query_service_client.get_columns_for_table("hive", "test", "pies") + assert "No columns found" in str(exc_info.value) + + def test_query_service_client_create_view(self, mocker: MockerFixture) -> None: + """ + Test creating a view using the query service client. + """ + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "catalog_name": "public", + "engine_name": "postgres", + "engine_version": "15.2", + "id": "ef209eef-c31a-4089-aae6-833259a08e22", + "submitted_query": "CREATE OR REPLACE VIEW foo SELECT 1 as num", + "executed_query": "CREATE OR REPLACE VIEW foo SELECT 1 as num", + "scheduled": "2023-01-01T00:00:00.000000", + "started": "2023-01-01T00:00:00.000000", + "finished": "2023-01-01T00:00:00.000001", + "state": "FINISHED", + "progress": 1, + "results": [], + "next": None, + "previous": None, + "errors": [], + } + + mock_request = mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.post", + return_value=mock_response, + ) + + # successful request + query_service_client = QueryServiceClient(uri=self.endpoint) + query_create = QueryCreate( + catalog_name="default", + engine_name="postgres", + engine_version="15.2", + submitted_query="CREATE OR REPLACE VIEW foo SELECT 1 as num", + async_=False, + ) + query_service_client.create_view( + view_name="foo", + query_create=query_create, + ) + + mock_request.assert_called_with( + "/queries/", + headers=ANY, + json={ + "catalog_name": "default", + "engine_name": "postgres", + "engine_version": "15.2", + "submitted_query": "CREATE OR REPLACE VIEW foo SELECT 1 as num", + "async_": False, + }, + ) + + def test_query_service_client_create_view_with_failure( + self, + mocker: MockerFixture, + ) -> None: + """ + Test creating a view using the query service client with a filed response. + """ + + mock_response = MagicMock() + mock_response.status_code = 500 + mock_response.json.return_value = {"message": "Errors", "errors": ["a", "b"]} + mock_request = mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.post", + return_value=mock_response, + ) + + # successful request + query_service_client = QueryServiceClient(uri=self.endpoint) + query_create = QueryCreate( + catalog_name="default", + engine_name="postgres", + engine_version="15.2", + submitted_query="CREATE OR REPLACE VIEW foo SELECT 1 as num", + async_=False, + ) + + with pytest.raises(DJQueryServiceClientException) as exc_info: + query_service_client.create_view( + view_name="foo", + query_create=query_create, + ) + assert "Error response from query service" in str(exc_info.value) + assert exc_info.value.errors == [ + DJError( + code=ErrorCode.QUERY_SERVICE_ERROR, + message="a", + debug=None, + context="", + ), + DJError( + code=ErrorCode.QUERY_SERVICE_ERROR, + message="b", + debug=None, + context="", + ), + ] + + mock_request.assert_called_with( + "/queries/", + headers=ANY, + json={ + "catalog_name": "default", + "engine_name": "postgres", + "engine_version": "15.2", + "submitted_query": "CREATE OR REPLACE VIEW foo SELECT 1 as num", + "async_": False, + }, + ) + + def test_query_service_client_submit_query(self, mocker: MockerFixture) -> None: + """ + Test submitting a query to a query service client. + """ + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "catalog_name": "public", + "engine_name": "postgres", + "engine_version": "15.2", + "id": "ef209eef-c31a-4089-aae6-833259a08e22", + "submitted_query": "SELECT 1 as num", + "executed_query": "SELECT 1 as num", + "scheduled": "2023-01-01T00:00:00.000000", + "started": "2023-01-01T00:00:00.000000", + "finished": "2023-01-01T00:00:00.000001", + "state": "FINISHED", + "progress": 1, + "results": [ + { + "sql": "SELECT 1 as num", + "columns": [{"name": "num", "type": "STR"}], + "rows": [[1]], + "row_count": 1, + }, + ], + "next": None, + "previous": None, + "errors": [], + } + + mock_request = mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.post", + return_value=mock_response, + ) + + query_service_client = QueryServiceClient(uri=self.endpoint) + query_create = QueryCreate( + catalog_name="default", + engine_name="postgres", + engine_version="15.2", + submitted_query="SELECT 1", + async_=False, + ) + query_service_client.submit_query( + query_create, + ) + + mock_request.assert_called_with( + "/queries/", + headers=ANY, + json={ + "catalog_name": "default", + "engine_name": "postgres", + "engine_version": "15.2", + "submitted_query": "SELECT 1", + "async_": False, + }, + ) + + def test_query_service_client_get_query(self, mocker: MockerFixture) -> None: + """ + Test getting a previously submitted query from a query service client. + """ + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "catalog_name": "public", + "engine_name": "postgres", + "engine_version": "15.2", + "id": "ef209eef-c31a-4089-aae6-833259a08e22", + "submitted_query": "SELECT 1 as num", + "executed_query": "SELECT 1 as num", + "scheduled": "2023-01-01T00:00:00.000000", + "started": "2023-01-01T00:00:00.000000", + "finished": "2023-01-01T00:00:00.000001", + "state": "FINISHED", + "progress": 1, + "results": [ + { + "sql": "SELECT 1 as num", + "columns": [{"name": "num", "type": "STR"}], + "rows": [[1]], + "row_count": 1, + }, + ], + "next": None, + "previous": None, + "errors": [], + "database_id": 1, # Will be deprecated soon in favor of catalog + } + + mock_request = mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.get", + return_value=mock_response, + ) + + query_service_client = QueryServiceClient(uri=self.endpoint) + query_service_client.get_query( + "ef209eef-c31a-4089-aae6-833259a08e22", + ) + + mock_request.assert_called_with( + "/queries/ef209eef-c31a-4089-aae6-833259a08e22/", + headers=ANY, + ) + + def test_query_service_client_materialize(self, mocker: MockerFixture) -> None: + """ + Test materialize from a query service client. + """ + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "urls": ["http://fake.url/job"], + "output_tables": ["common.a", "common.b"], + } + + mock_request = mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.post", + return_value=mock_response, + ) + + query_service_client = QueryServiceClient(uri=self.endpoint) + query_service_client.materialize( + GenericMaterializationInput( + name="default", + job="SparkSqlMaterializationJob", + strategy=MaterializationStrategy.FULL, + node_name="default.hard_hat", + node_version="v1", + node_type=NodeType.DIMENSION, + schedule="0 * * * *", + query="", + spark_conf={}, + upstream_tables=["default.hard_hats"], + partitions=[], + columns=[], + ), + ) + + mock_request.assert_called_with( + "/materialization/", + json={ + "name": "default", + "job": "SparkSqlMaterializationJob", + "strategy": "full", + "node_name": "default.hard_hat", + "node_version": "v1", + "node_type": "dimension", + "partitions": [], + "query": "", + "schedule": "0 * * * *", + "spark_conf": {}, + "upstream_tables": ["default.hard_hats"], + "columns": [], + "lookback_window": "1 DAY", + }, + headers=ANY, + ) + + def test_query_service_client_deactivate_materialization( + self, + mocker: MockerFixture, + ) -> None: + """ + Test deactivate materialization from a query service client. + """ + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "urls": ["http://fake.url/job"], + "output_tables": ["common.a", "common.b"], + } + + mock_request = mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.delete", + return_value=mock_response, + ) + + query_service_client = QueryServiceClient(uri=self.endpoint) + query_service_client.deactivate_materialization( + node_name="default.hard_hat", + materialization_name="default", + ) + + mock_request.assert_called_with( + "/materialization/default.hard_hat/default/", + headers=ANY, + ) + + def test_query_service_client_raising_error(self, mocker: MockerFixture) -> None: + """ + Test handling an error response from the query service client + """ + mock_400_response = MagicMock() + mock_400_response.status_code = 400 + mock_400_response.json.return_value = { + "message": "Errors", + "errors": ["a", "b"], + } + + mock_404_response = MagicMock() + mock_404_response.status_code = 404 + mock_404_response.json.return_value = { + "message": "Query not found", + "errors": ["a"], + } + + query_service_client = QueryServiceClient(uri=self.endpoint) + + with mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.get", + return_value=mock_400_response, + ): + with pytest.raises(DJQueryServiceClientException) as exc_info: + query_service_client.get_query( + "ef209eef-c31a-4089-aae6-833259a08e22", + ) + + with mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.get", + return_value=mock_404_response, + ): + with pytest.raises(DJQueryServiceClientEntityNotFound) as exc_info: + query_service_client.get_query( + "ef209eef-c31a-4089-aae6-833259a08e22", + ) + + assert "Error response from query service" in str(exc_info.value) + query_create = QueryCreate( + catalog_name="hive", + engine_name="postgres", + engine_version="15.2", + submitted_query="SELECT 1", + async_=False, + ) + + with mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.post", + return_value=mock_400_response, + ): + with pytest.raises(DJQueryServiceClientException) as exc_info: + query_service_client.submit_query( + query_create, + ) + assert "Error response from query service" in str(exc_info.value) + assert exc_info.value.errors == [ + DJError( + code=ErrorCode.QUERY_SERVICE_ERROR, + message="a", + debug=None, + context="", + ), + DJError( + code=ErrorCode.QUERY_SERVICE_ERROR, + message="b", + debug=None, + context="", + ), + ] + + def test_materialize(self, mocker: MockerFixture) -> None: + """ + Test get materialization urls for a given node materialization + """ + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "urls": ["http://fake.url/job"], + "output_tables": ["common.a", "common.b"], + } + + mock_request = mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.post", + return_value=mock_response, + ) + + query_service_client = QueryServiceClient(uri=self.endpoint) + response = query_service_client.materialize( + GenericMaterializationInput( + name="default", + job="SparkSqlMaterializationJob", + strategy=MaterializationStrategy.FULL, + node_name="default.hard_hat", + node_version="v1", + node_type=NodeType.DIMENSION, + schedule="0 * * * *", + query="", + spark_conf={}, + upstream_tables=["default.hard_hats"], + partitions=[], + columns=[], + ), + ) + mock_request.assert_called_with( + "/materialization/", + json={ + "name": "default", + "job": "SparkSqlMaterializationJob", + "lookback_window": "1 DAY", + "strategy": "full", + "node_name": "default.hard_hat", + "node_version": "v1", + "node_type": "dimension", + "schedule": "0 * * * *", + "query": "", + "upstream_tables": ["default.hard_hats"], + "spark_conf": {}, + "partitions": [], + "columns": [], + }, + headers=ANY, + ) + assert response == { + "urls": ["http://fake.url/job"], + "output_tables": ["common.a", "common.b"], + } + + def test_get_materialization_info(self, mocker: MockerFixture) -> None: + """ + Test get materialization urls for a given node materialization + """ + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "urls": ["http://fake.url/job"], + "output_tables": ["common.a", "common.b"], + } + + mock_request = mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.get", + return_value=mock_response, + ) + + query_service_client = QueryServiceClient(uri=self.endpoint) + response = query_service_client.get_materialization_info( + node_name="default.hard_hat", + node_version="v3.1", + node_type=NodeType.DIMENSION, + materialization_name="default", + ) + mock_request.assert_called_with( + "/materialization/default.hard_hat/v3.1/default/?node_type=dimension", + timeout=3, + headers=ANY, + ) + assert response == { + "urls": ["http://fake.url/job"], + "output_tables": ["common.a", "common.b"], + } + + def test_get_materialization_info_error(self, mocker: MockerFixture) -> None: + """ + Test get materialization info with errors + """ + mock_response = MagicMock() + mock_response.status_code = 500 + mock_response.json.return_value = {"message": "An error has occurred"} + + mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.get", + return_value=mock_response, + ) + + query_service_client = QueryServiceClient(uri=self.endpoint) + response = query_service_client.get_materialization_info( + node_name="default.hard_hat", + node_version="v3.1", + node_type=NodeType.DIMENSION, + materialization_name="default", + ) + assert response == { + "urls": [], + "output_tables": [], + } + + def test_run_backfill(self, mocker: MockerFixture) -> None: + """ + Test running backfill on temporal partitions and categorical partitions + """ + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "urls": ["http://fake.url/job"], + "output_tables": [], + } + + mocked_call = mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.post", + return_value=mock_response, + ) + + query_service_client = QueryServiceClient(uri=self.endpoint) + response = query_service_client.run_backfill( + node_name="default.hard_hat", + node_version="v1", + node_type=NodeType.DIMENSION, + partitions=[ + PartitionBackfill( + column_name="hire_date", + range=["20230101", "20230201"], + ), + ], + materialization_name="default", + ) + assert response == { + "urls": ["http://fake.url/job"], + "output_tables": [], + } + mocked_call.assert_called_with( + "/materialization/run/default.hard_hat/default/?node_version=v1&node_type=dimension", + json=[ + { + "column_name": "hire_date", + "range": ["20230101", "20230201"], + "values": None, + }, + ], + timeout=20, + headers=ANY, + ) + + mocked_call.reset_mock() + + query_service_client = QueryServiceClient(uri=self.endpoint) + response = query_service_client.run_backfill( + node_name="default.hard_hat", + node_version="v1", + node_type=NodeType.DIMENSION, + partitions=[ + PartitionBackfill( + column_name="hire_date", + range=["20230101", "20230201"], + ), + PartitionBackfill( + column_name="state", + values=["CA", "DE"], + ), + ], + materialization_name="default", + ) + assert response == { + "urls": ["http://fake.url/job"], + "output_tables": [], + } + mocked_call.assert_called_with( + "/materialization/run/default.hard_hat/default/?node_version=v1&node_type=dimension", + json=[ + { + "column_name": "hire_date", + "range": ["20230101", "20230201"], + "values": None, + }, + { + "column_name": "state", + "range": None, + "values": ["CA", "DE"], + }, + ], + timeout=20, + headers=ANY, + ) + + def test_filtered_headers(self): + """ + We should filter out certain headers. + """ + assert QueryServiceClient.filtered_headers( + { + "User-Agent": "python-requests/2.29.0", + "Accept-Encoding": "gzip, deflate", + "Accept": "*/*", + }, + ) == { + "User-Agent": "python-requests/2.29.0", + "Accept": "*/*", + } + assert QueryServiceClient.filtered_headers( + { + "User-Agent": "python-requests/2.29.0", + "accept-encoding": "gzip, deflate", + "Accept": "*/*", + }, + ) == { + "User-Agent": "python-requests/2.29.0", + "Accept": "*/*", + } + + def test_materialize_cube(self, mocker: MockerFixture) -> None: + """ + Test materialize cube via query service client + """ + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "urls": ["http://fake.url/job"], + "output_tables": ["common.a", "common.b"], + } + + mock_request = mocker.patch( + "datajunction_server.service_clients.RequestsSessionWithEndpoint.post", + return_value=mock_response, + ) + + query_service_client = QueryServiceClient(uri=self.endpoint) + materialization_input = DruidCubeMaterializationInput( + id=1, + name="default", + strategy=MaterializationStrategy.INCREMENTAL_TIME, + schedule="@daily", + job="DruidCubeMaterialization", + cube=NodeNameVersion(name="default.repairs_cube", version="v1.0"), + dimensions=["default.hard_hat.first_name", "default.hard_hat.last_name"], + metrics=[ + CubeMetric( + metric=NodeNameVersion( + name="default.num_repair_orders", + version="v1.0", + ), + required_measures=[ + MeasureKey( + node=NodeNameVersion( + name="default.repair_orders", + version="v1.0", + ), + measure_name="count", + ), + ], + derived_expression="SUM(count)", + ), + CubeMetric( + metric=NodeNameVersion( + name="default.avg_repair_price", + version="v1.0", + ), + required_measures=[ + MeasureKey( + node=NodeNameVersion( + name="default.repair_orders", + version="v1.0", + ), + measure_name="sum_price_123abc", + ), + ], + derived_expression="SUM(sum_price_123abc)", + ), + ], + measures_materializations=[], + combiners=[], + ) + response = query_service_client.materialize_cube(materialization_input) + mock_request.assert_called_with( + "/cubes/materialize", + json=materialization_input.dict(), + timeout=20, + headers=ANY, + ) + assert response == { + "urls": ["http://fake.url/job"], + "output_tables": ["common.a", "common.b"], + } diff --git a/datajunction-server/tests/sql/__init__.py b/datajunction-server/tests/sql/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/tests/sql/dag_test.py b/datajunction-server/tests/sql/dag_test.py new file mode 100644 index 000000000..60645daea --- /dev/null +++ b/datajunction-server/tests/sql/dag_test.py @@ -0,0 +1,190 @@ +""" +Tests for ``datajunction_server.sql.dag``. +""" + +import pytest +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.database.column import Column +from datajunction_server.database.database import Database +from datajunction_server.database.node import Node, NodeRevision +from datajunction_server.database.user import User +from datajunction_server.errors import DJException +from datajunction_server.models.node import DimensionAttributeOutput, NodeType +from datajunction_server.sql.dag import get_dimensions, topological_sort +from datajunction_server.sql.parsing.types import IntegerType, StringType + + +@pytest.mark.asyncio +async def test_get_dimensions(session: AsyncSession, current_user: User) -> None: + """ + Test ``get_dimensions``. + """ + database = Database(id=1, name="one", URI="sqlite://") + session.add(database) + + dimension_ref = Node( + name="B", + type=NodeType.DIMENSION, + current_version="1", + created_by_id=current_user.id, + ) + dimension = NodeRevision( + node=dimension_ref, + name=dimension_ref.name, + type=dimension_ref.type, + display_name="B", + version="1", + columns=[ + Column(name="id", type=IntegerType(), order=0), + Column(name="attribute", type=StringType(), order=1), + ], + created_by_id=current_user.id, + ) + dimension_ref.current = dimension + session.add(dimension) + session.add(dimension_ref) + + parent_ref = Node( + name="A", + current_version="1", + type=NodeType.SOURCE, + created_by_id=current_user.id, + ) + parent = NodeRevision( + node=parent_ref, + name=parent_ref.name, + type=parent_ref.type, + display_name="A", + version="1", + columns=[ + Column(name="ds", type=StringType(), order=0), + Column(name="b_id", type=IntegerType(), dimension=dimension_ref, order=1), + ], + created_by_id=current_user.id, + ) + parent_ref.current = parent + session.add(parent) + session.add(parent_ref) + + child_ref = Node( + name="C", + current_version="1", + type=NodeType.METRIC, + created_by_id=current_user.id, + ) + child = NodeRevision( + node=child_ref, + name=child_ref.name, + display_name="C", + version="1", + query="SELECT COUNT(*) FROM A", + parents=[parent_ref], + type=NodeType.METRIC, + created_by_id=current_user.id, + ) + child_ref.current = child + session.add(child) + session.add(child_ref) + await session.commit() + + assert await get_dimensions(session, child_ref) == [ + DimensionAttributeOutput( + name="B.attribute", + node_name="B", + node_display_name="B", + properties=[], + type="string", + path=["A.b_id"], + filter_only=False, + ), + DimensionAttributeOutput( + name="B.id", + node_name="B", + node_display_name="B", + properties=[], + type="int", + path=["A.b_id"], + filter_only=False, + ), + ] + + +@pytest.mark.asyncio +async def test_topological_sort(session: AsyncSession) -> None: + """ + Test ``topological_sort``. + """ + node_a = Node(name="test.A", type=NodeType.TRANSFORM) + node_rev_a = NodeRevision( + node=node_a, + name=node_a.name, + parents=[], + ) + node_a.current = node_rev_a + session.add(node_a) + session.add(node_rev_a) + + node_b = Node(name="test.B", type=NodeType.TRANSFORM) + node_rev_b = NodeRevision( + node=node_b, + name=node_b.name, + parents=[node_a], + ) + node_b.current = node_rev_b + session.add(node_b) + session.add(node_rev_b) + + node_c = Node(name="test.C", type=NodeType.TRANSFORM) + node_rev_c = NodeRevision( + node=node_c, + name=node_c.name, + parents=[], + ) + node_c.current = node_rev_c + session.add(node_c) + session.add(node_rev_c) + + node_d = Node(name="test.D", type=NodeType.TRANSFORM) + node_rev_c.parents = [node_b, node_d] + node_rev_d = NodeRevision( + node=node_d, + name=node_d.name, + parents=[node_a], + ) + node_d.current = node_rev_d + session.add(node_d) + session.add(node_rev_d) + + node_e = Node(name="test.E", type=NodeType.TRANSFORM) + node_rev_e = NodeRevision( + node=node_e, + name=node_e.name, + parents=[node_c, node_b], + ) + node_e.current = node_rev_e + session.add(node_e) + session.add(node_rev_e) + + node_f = Node(name="test.F", type=NodeType.TRANSFORM) + node_rev_d.parents.append(node_f) + node_rev_f = NodeRevision( + node=node_f, + name=node_f.name, + parents=[node_e], + ) + node_f.current = node_rev_f + session.add(node_f) + session.add(node_rev_f) + + ordering = topological_sort([node_a, node_b, node_c, node_d, node_e]) + assert [node.name for node in ordering] == [ + node_a.name, + node_d.name, + node_b.name, + node_c.name, + node_e.name, + ] + with pytest.raises(DJException) as exc_info: + topological_sort([node_a, node_b, node_c, node_d, node_e, node_f]) + assert "Graph has at least one cycle" in str(exc_info) diff --git a/datajunction-server/tests/sql/decompose_test.py b/datajunction-server/tests/sql/decompose_test.py new file mode 100644 index 000000000..20d510a6c --- /dev/null +++ b/datajunction-server/tests/sql/decompose_test.py @@ -0,0 +1,629 @@ +""" +Tests for ``datajunction_server.sql.decompose``. +""" + +import pytest + +from datajunction_server.models.cube_materialization import ( + Aggregability, + AggregationRule, + Measure, +) +from datajunction_server.sql.decompose import MeasureExtractor +from datajunction_server.sql.parsing.backends.antlr4 import parse +from datajunction_server.sql.parsing.backends.exceptions import DJParseException + + +def test_simple_sum(): + """ + Test decomposition for a metric definition that is a simple sum. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT SUM(sales_amount) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="sales_amount_sum_a1b27bc7", + expression="sales_amount", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse("SELECT SUM(sales_amount_sum_a1b27bc7) FROM parent_node"), + ) + + +def test_sum_with_cast(): + """ + Test decomposition for a metric definition that has a sum with a cast. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT CAST(SUM(sales_amount) AS DOUBLE) * 100.0 FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="sales_amount_sum_a1b27bc7", + expression="sales_amount", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT CAST(SUM(sales_amount_sum_a1b27bc7) AS DOUBLE) * 100.0 FROM parent_node", + ), + ) + + extractor = MeasureExtractor.from_query_string( + "SELECT 100.0 * SUM(sales_amount) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="sales_amount_sum_a1b27bc7", + expression="sales_amount", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse("SELECT 100.0 * SUM(sales_amount_sum_a1b27bc7) FROM parent_node"), + ) + + +def test_sum_with_coalesce(): + """ + Test decomposition for a metric definition that has a sum with coalesce. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT COALESCE(SUM(sales_amount), 0) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="sales_amount_sum_a1b27bc7", + expression="sales_amount", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse("SELECT COALESCE(SUM(sales_amount_sum_a1b27bc7), 0) FROM parent_node"), + ) + + extractor = MeasureExtractor.from_query_string( + "SELECT SUM(COALESCE(sales_amount, 0)) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="sales_amount_sum_bc5c6414", + expression="COALESCE(sales_amount, 0)", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse("SELECT SUM(sales_amount_sum_bc5c6414) FROM parent_node"), + ) + + +def test_multiple_sums(): + """ + Test decomposition for a metric definition that has multiple sums. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT SUM(sales_amount) + SUM(fraud_sales) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="sales_amount_sum_a1b27bc7", + expression="sales_amount", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + Measure( + name="fraud_sales_sum_0a5d6799", + expression="fraud_sales", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT SUM(sales_amount_sum_a1b27bc7) + " + "SUM(fraud_sales_sum_0a5d6799) FROM parent_node", + ), + ) + + extractor = MeasureExtractor.from_query_string( + "SELECT SUM(sales_amount) - SUM(fraud_sales) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT SUM(sales_amount_sum_a1b27bc7) - " + "SUM(fraud_sales_sum_0a5d6799) FROM parent_node", + ), + ) + + +def test_nested_functions(): + """ + Test behavior with deeply nested functions inside aggregations. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT SUM(ROUND(COALESCE(sales_amount, 0) * 1.1)) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="sales_amount_sum_d511860a", + expression="ROUND(COALESCE(sales_amount, 0) * 1.1)", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse("SELECT SUM(sales_amount_sum_d511860a) FROM parent_node"), + ) + + extractor = MeasureExtractor.from_query_string( + "SELECT LN(SUM(COALESCE(sales_amount, 0)) + 1) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="sales_amount_sum_bc5c6414", + expression="COALESCE(sales_amount, 0)", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse("SELECT LN(SUM(sales_amount_sum_bc5c6414) + 1) FROM parent_node"), + ) + + +def test_average(): + """ + Test decomposition for a metric definition that uses AVG. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT AVG(sales_amount) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + + expected_measures = [ + Measure( + name="sales_amount_count_a1b27bc7", + expression="sales_amount", + aggregation="COUNT", + rule=AggregationRule(type=Aggregability.FULL), + ), + Measure( + name="sales_amount_sum_a1b27bc7", + expression="sales_amount", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT SUM(sales_amount_sum_a1b27bc7) / " + "SUM(sales_amount_count_a1b27bc7) FROM parent_node", + ), + ) + + +def test_rate(): + """ + Test decomposition for a rate metric definition. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT SUM(clicks) / SUM(impressions) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures0 = [ + Measure( + name="clicks_sum_c9e9e0fc", + expression="clicks", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + Measure( + name="impressions_sum_87e980e6", + expression="impressions", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures0 + assert str(derived_sql) == str( + parse( + "SELECT SUM(clicks_sum_c9e9e0fc) / SUM(impressions_sum_87e980e6) FROM parent_node", + ), + ) + + extractor = MeasureExtractor.from_query_string( + "SELECT 1.0 * SUM(clicks) / NULLIF(SUM(impressions), 0) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="clicks_sum_c9e9e0fc", + expression="clicks", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + Measure( + name="impressions_sum_87e980e6", + expression="impressions", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT 1.0 * SUM(clicks_sum_c9e9e0fc) / " + "NULLIF(SUM(impressions_sum_87e980e6), 0) FROM parent_node", + ), + ) + + extractor = MeasureExtractor.from_query_string( + "SELECT CAST(CAST(SUM(clicks) AS INT) AS DOUBLE) / " + "CAST(SUM(impressions) AS DOUBLE) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + assert measures == expected_measures0 + assert str(derived_sql) == str( + parse( + "SELECT CAST(CAST(SUM(clicks_sum_c9e9e0fc) AS INT) AS DOUBLE) / " + "CAST(SUM(impressions_sum_87e980e6) AS DOUBLE) FROM parent_node", + ), + ) + + extractor = MeasureExtractor.from_query_string( + "SELECT COALESCE(SUM(clicks) / SUM(impressions), 0) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="clicks_sum_c9e9e0fc", + expression="clicks", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + Measure( + name="impressions_sum_87e980e6", + expression="impressions", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT COALESCE(SUM(clicks_sum_c9e9e0fc) / " + "SUM(impressions_sum_87e980e6), 0) FROM parent_node", + ), + ) + + extractor = MeasureExtractor.from_query_string( + "SELECT IF(SUM(clicks) > 0, CAST(SUM(impressions) AS DOUBLE) " + "/ CAST(SUM(clicks) AS DOUBLE), NULL) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="clicks_sum_c9e9e0fc", + expression="clicks", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + Measure( + name="impressions_sum_87e980e6", + expression="impressions", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT IF(SUM(clicks_sum_c9e9e0fc) > 0, CAST(SUM(impressions_sum_87e980e6) AS DOUBLE)" + " / CAST(SUM(clicks_sum_c9e9e0fc) AS DOUBLE), NULL) FROM parent_node", + ), + ) + + extractor = MeasureExtractor.from_query_string( + "SELECT ln(sum(clicks) + 1) / sum(views) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="clicks_sum_c9e9e0fc", + expression="clicks", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + Measure( + name="views_sum_59a14a57", + expression="views", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT ln(sum(clicks_sum_c9e9e0fc) + 1) / sum(views_sum_59a14a57) FROM parent_node", + ), + ) + + +def test_has_ever(): + """ + Test decomposition for a metric definition that uses MAX. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT MAX(IF(condition, 1, 0)) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="condition_max_98f2d913", + expression="IF(condition, 1, 0)", + aggregation="MAX", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse("SELECT MAX(condition_max_98f2d913) FROM parent_node"), + ) + + +def test_fraction_with_if(): + """ + Test decomposition for a rate metric with complex numerator and denominators. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT IF(SUM(COALESCE(action, 0)) > 0, " + "CAST(SUM(COALESCE(action_two, 0)) AS DOUBLE) / " + "CAST(SUM(COALESCE(action, 0)) AS DOUBLE), NULL) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + + expected_measures = [ + Measure( + name="action_sum_d0b4f8e5", + expression="COALESCE(action, 0)", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + Measure( + name="action_two_sum_0c8945fc", + expression="COALESCE(action_two, 0)", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT IF(SUM(action_sum_d0b4f8e5) > 0, " + "CAST(SUM(action_two_sum_0c8945fc) AS DOUBLE) / " + "CAST(SUM(action_sum_d0b4f8e5) AS DOUBLE), NULL) FROM parent_node", + ), + ) + + +def test_count(): + """ + Test decomposition for a count metric. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT COUNT(IF(action = 1, action_event_ts, 0)) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="action_action_event_ts_count_59b28b54", + expression="IF(action = 1, action_event_ts, 0)", + aggregation="COUNT", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse("SELECT SUM(action_action_event_ts_count_59b28b54) FROM parent_node"), + ) + + +def test_count_distinct_rate(): + """ + Test decomposition for a metric that uses count distinct. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT COUNT(DISTINCT user_id) / COUNT(action) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="user_id_count_5deb6d4f", + expression="DISTINCT user_id", + aggregation="COUNT", + rule=AggregationRule(type=Aggregability.LIMITED), + ), + Measure( + name="action_count_418c5509", + expression="action", + aggregation="COUNT", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT COUNT( DISTINCT user_id_count_5deb6d4f) / " + "SUM(action_count_418c5509) FROM parent_node", + ), + ) + + +def test_no_aggregation(): + """ + Test behavior when there is no aggregation function in the metric query. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT sales_amount FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [] + assert measures == expected_measures + assert str(derived_sql) == str(parse("SELECT sales_amount FROM parent_node")) + + +def test_multiple_aggregations_with_conditions(): + """ + Test behavior with conditional aggregations in the metric query. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT SUM(IF(region = 'US', sales_amount, 0)) + " + "COUNT(DISTINCT IF(region = 'US', account_id, NULL)) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="region_sales_amount_sum_55eb544e", + expression="IF(region = 'US', sales_amount, 0)", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL), + ), + Measure( + name="region_account_id_count_04a6925b", + expression="DISTINCT IF(region = 'US', account_id, NULL)", + aggregation="COUNT", + rule=AggregationRule(type=Aggregability.LIMITED), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT SUM(region_sales_amount_sum_55eb544e) + COUNT(" + "DISTINCT region_account_id_count_04a6925b) FROM parent_node", + ), + ) + + extractor = MeasureExtractor.from_query_string( + "SELECT cast(coalesce(max(a), max(b), 0) as double) + " + "cast(coalesce(max(a), max(b)) as double) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="a_max_0cc175b9", + expression="a", + aggregation="MAX", + rule=AggregationRule(type=Aggregability.FULL), + ), + Measure( + name="b_max_92eb5ffe", + expression="b", + aggregation="MAX", + rule=AggregationRule(type=Aggregability.FULL), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT CAST(coalesce(max(a_max_0cc175b9), max(b_max_92eb5ffe), 0) AS DOUBLE) + " + "CAST(coalesce(max(a_max_0cc175b9), max(b_max_92eb5ffe)) AS DOUBLE) FROM parent_node", + ), + ) + + +def test_empty_query(): + """ + Test behavior when the metric query is empty. + """ + with pytest.raises(DJParseException, match="Empty query provided!"): + extractor = MeasureExtractor.from_query_string("") + extractor.extract() + + +def test_unsupported_aggregation_function(): + """ + Test behavior when the query contains unsupported aggregation functions. We just return an + empty list of measures in this case, because there are no pre-aggregatable measures. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT MEDIAN(sales_amount) FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [] + assert measures == expected_measures + assert str(derived_sql) == str( + parse("SELECT MEDIAN(sales_amount) FROM parent_node"), + ) + + extractor = MeasureExtractor.from_query_string( + "SELECT approx_percentile(duration_ms, 1.0, 0.9) / 1000 FROM parent_node", + ) + measures, derived_sql = extractor.extract() + expected_measures = [] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT approx_percentile(duration_ms, 1.0, 0.9) / 1000 FROM parent_node", + ), + ) + + +def test_metric_query_with_aliases(): + """ + Test behavior when the query contains unsupported aggregation functions. We just return an + empty list of measures in this case, because there are no pre-aggregatable measures. + """ + extractor = MeasureExtractor.from_query_string( + "SELECT avg(cast(repair_orders_fact.time_to_dispatch as int)) " + "FROM default.repair_orders_fact repair_orders_fact", + ) + measures, derived_sql = extractor.extract() + expected_measures = [ + Measure( + name="time_to_dispatch_count_bf99afd6", + expression="CAST(time_to_dispatch AS INT)", + aggregation="COUNT", + rule=AggregationRule(type=Aggregability.FULL, level=None), + ), + Measure( + name="time_to_dispatch_sum_bf99afd6", + expression="CAST(time_to_dispatch AS INT)", + aggregation="SUM", + rule=AggregationRule(type=Aggregability.FULL, level=None), + ), + ] + assert measures == expected_measures + assert str(derived_sql) == str( + parse( + "SELECT SUM(time_to_dispatch_sum_bf99afd6) / " + "SUM(time_to_dispatch_count_bf99afd6) FROM default.repair_orders_fact", + ), + ) diff --git a/datajunction-server/tests/sql/functions_test.py b/datajunction-server/tests/sql/functions_test.py new file mode 100644 index 000000000..67c6f4bd4 --- /dev/null +++ b/datajunction-server/tests/sql/functions_test.py @@ -0,0 +1,3718 @@ +""" +Tests for ``datajunction_server.sql.functions``. +""" + +import pytest +from sqlalchemy.ext.asyncio import AsyncSession + +import datajunction_server.sql.functions as F +import datajunction_server.sql.parsing.types as ct +from datajunction_server.errors import DJException, DJNotImplementedException +from datajunction_server.models.engine import Dialect +from datajunction_server.sql.functions import ( + Avg, + Coalesce, + Count, + Max, + Min, + Now, + Sum, + ToDate, + function_registry, +) +from datajunction_server.sql.parsing import ast +from datajunction_server.sql.parsing.backends.antlr4 import parse +from datajunction_server.sql.parsing.backends.exceptions import DJParseException +from datajunction_server.sql.parsing.types import ( + BigIntType, + DateType, + DecimalType, + DoubleType, + FloatType, + IntegerType, + NullType, + StringType, + WildcardType, +) + + +@pytest.mark.asyncio +async def test_missing_functions() -> None: + """ + Test missing functions. + """ + with pytest.raises(DJNotImplementedException) as excinfo: + function_registry["INVALID_FUNCTION"] + assert ( + str(excinfo.value) == "The function `INVALID_FUNCTION` hasn't been implemented " + "in DJ yet. You can file an issue at https://github.com/" + "DataJunction/dj/issues/new?title=Function+missing:+" + "INVALID_FUNCTION to request it to be added, or use the " + "documentation at https://github.com/DataJunction/dj/blob" + "/main/docs/functions.rst to implement it." + ) + + +@pytest.mark.asyncio +async def test_bad_combo_types() -> None: + """ + Tests dispatch raises on bad types + """ + with pytest.raises(TypeError) as exc: + Avg.infer_type(ast.Column(ast.Name("x"), _type=StringType())) + assert "got an invalid combination of types" in str(exc) + + +@pytest.mark.asyncio +async def test_abs(session: AsyncSession): + """ + Test the `abs` Spark function + """ + query = parse( + """ + select abs(-1) + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + query = parse( + """ + select abs(-1.1) + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + assert query.select.projection[0].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_acos(session: AsyncSession): + """ + Test the `acos` function + """ + query = parse( + """ + select acos(0.0) + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_aggregate(session: AsyncSession): + """ + Test the `aggregate` Spark function + """ + query = parse( + """ + select + aggregate(items, '', (acc, x) -> (case + when acc = '' then element_at(split(x, '::'), 1) + when acc = 'a' then acc + else element_at(split(x, '::'), 1) end)) as item, + aggregate(items, '', (acc, x) -> (case + when acc = '' then element_at(split(x, '::'), 1) + when acc = 'a' then acc + else element_at(split(x, '::'), 1) end), acc -> acc) as item1 + from ( + select 1 as id, ARRAY('b', 'c', 'a', 'x', 'g', 'z') AS items + ) + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert query.select.projection[0].type == StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_complex_nested_functions(session: AsyncSession): + """ + Test that the CAST(...) workaround works for complex nested lambda functions + """ + query = parse( + """ + SELECT + CAST( + AGGREGATE( + MAP_VALUES( + AGGREGATE( + COLLECT_LIST( + MAP( + x, + NAMED_STRUCT( + 'a1', + CASE + WHEN y = 'apples' + THEN z ELSE 0 + END, + 'b1', + COALESCE(y, 0) + ) + ) + ), + CAST(MAP() AS MAP>), + (acc, x) -> MAP_ZIP_WITH( + acc, x, + (k, v1, v2) -> NAMED_STRUCT( + 'z', + COALESCE(v1['z'], 0) + COALESCE(v2['z'], 0), + 'y', + COALESCE(v1['y'], 0) + COALESCE(v2['y'], 0) + + ) + ), + acc -> TRANSFORM_VALUES(acc, (_, v) -> v['z']/(20.0*v['y'])) + ) + ), + NAMED_STRUCT( + 'z1', + CAST(0 AS DOUBLE), + 'y1', + CAST(0 AS DOUBLE) + ), + (acc, x) -> NAMED_STRUCT('y1', acc['z1'] + x, 'y1', acc['y1'] + 1), + acc -> acc['y1']/acc['z1'] + ) AS STRING + ) AS etc +FROM ( + SELECT '124345' as x, + 'something' as y, + 29109 as z +) + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert query.select.projection[0].type == StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_any_value(session: AsyncSession): + """ + Test the `any_value` function + """ + query = parse( + """select any_value(col) FROM (select (1), (2) AS col)""", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_approx_count_distinct(session: AsyncSession): + """ + Test the `approx_count_distinct` function + """ + query = parse( + """ + select + approx_count_distinct(col), + approx_count_distinct_ds_hll(col), + approx_count_distinct_ds_theta(col) + FROM (select (1), (2) AS col)""", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.LongType() # type: ignore + assert query.select.projection[0].function().dialects == [Dialect.DRUID] # type: ignore + assert query.select.projection[1].type == ct.LongType() # type: ignore + assert query.select.projection[1].function().dialects == [Dialect.DRUID] # type: ignore + assert query.select.projection[2].type == ct.LongType() # type: ignore + assert query.select.projection[2].function().dialects == [Dialect.DRUID] # type: ignore + + +@pytest.mark.asyncio +async def test_approx_percentile(session: AsyncSession): + """ + Test the `approx_percentile` Spark function + """ + query_with_list = parse("SELECT approx_percentile(10.0, array(0.5, 0.4, 0.1), 100)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query_with_list.compile(ctx) + assert not exc.errors + assert query_with_list.select.projection[0].type == ct.ListType( # type: ignore + element_type=ct.FloatType(), + ) + + query_with_list = parse("SELECT approx_percentile(10.0, 0.5, 100)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query_with_list.compile(ctx) + assert not exc.errors + assert query_with_list.select.projection[0].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_array(session: AsyncSession): + """ + Test the `array` Spark function + """ + query = parse( + """ + SELECT array() FROM (select 1 as col) + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.NullType()) # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + query = parse( + """ + SELECT array(1, 2, 3) FROM (select 1 as col) + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_array_agg(session: AsyncSession): + """ + Test the `array_agg` Spark function + """ + query = parse( + """ + SELECT array_agg(col) FROM (select 1 as col) + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + query = parse( + """ + SELECT array_agg(col) FROM (select 'foo' as col) + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.StringType()) # type: ignore + + +@pytest.mark.asyncio +async def test_array_append(session: AsyncSession): + """ + Test the `array_append` Spark function + """ + query = parse("SELECT array_append(array('b', 'd', 'c', 'a'), 'd')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.StringType()) # type: ignore + + query = parse("SELECT array_append(array(1, 2, 3, 4), 5)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + + query = parse("SELECT array_append(array(true, false, true, true), false)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.BooleanType()) # type: ignore + + +@pytest.mark.asyncio +async def test_array_compact(session: AsyncSession): + """ + Test the `array_compact` Spark function + """ + query = parse( + 'SELECT array_compact(array(1, 2, 3, null)), array_compact(array("a", "b", "c"))', + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + assert query.select.projection[1].type == ct.ListType(element_type=ct.StringType()) # type: ignore + + +@pytest.mark.asyncio +async def test_array_concat(session: AsyncSession): + """ + Test the `array_concat` Spark function + """ + query = parse( + 'SELECT array_concat(array("a", "b", "d"), array("a", "b", "c"))', + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.StringType()) # type: ignore + assert query.select.projection[0].function().dialects == [Dialect.DRUID] # type: ignore + + +@pytest.mark.asyncio +async def test_array_contains(session: AsyncSession): + """ + Test the `array_contains` Spark function + """ + query = parse("select array_contains(array(1, 2, 3), 2)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_array_distinct(session: AsyncSession): + """ + Test the `array_distinct` Spark function + """ + query = parse( + """ + SELECT array_distinct(array(1, 2, 3, 3)) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + + query = parse( + """ + SELECT array_distinct(array('a', 'b', 'b', 'z')) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType(element_type=ct.StringType()) # type: ignore + + +@pytest.mark.asyncio +async def test_array_except(session: AsyncSession): + """ + Test the `array_except` Spark function + """ + query = parse( + """ + SELECT array_except(array(1, 2, 3), array(1, 3, 5)) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + + query = parse( + """ + SELECT array_except(array('a', 'b', 'b', 'z'), array('a', 'b')) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType(element_type=ct.StringType()) # type: ignore + + +@pytest.mark.asyncio +async def test_array_intersect(session: AsyncSession): + """ + Test the `array_intersect` Spark function + """ + query = parse( + """ + SELECT array_intersect(array(1, 2, 3), array(1, 3, 5)) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + + query = parse( + """ + SELECT array_intersect(array('a', 'b', 'b', 'z'), array('a', 'b')) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType(element_type=ct.StringType()) # type: ignore + + +@pytest.mark.asyncio +async def test_array_join(session: AsyncSession): + """ + Test the `array_join` Spark function + """ + query = parse( + """ + SELECT array_join(array('hello', 'world'), ' ') + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.StringType() # type: ignore + + query = parse( + """ + SELECT array_join(array('hello', null ,'world'), ' ', ',') + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_array_max(session: AsyncSession): + """ + Test the `array_max` Spark function + """ + query = parse( + """ + SELECT array_max(array(1, 20, null, 3)) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_array_min(session: AsyncSession): + """ + Test the `array_min` Spark function + """ + query = parse( + """ + SELECT array_min(array(1.0, 202.2, null, 3.333)) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_array_offset_ordinal(session: AsyncSession): + """ + Test the `array_offset` and `array_ordinal` functions + """ + query = parse( + """ + SELECT + array_offset(array(1.0, 202.2, null, 3.333), 1), + array_ordinal(array(1.0, 202.2, null, 3.333), 1) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[0].function().dialects == [Dialect.DRUID] # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + assert query.select.projection[1].function().dialects == [Dialect.DRUID] # type: ignore + + +@pytest.mark.asyncio +async def test_array_position(session: AsyncSession): + """ + Test the `array_position` function + """ + query = parse( + """ + SELECT array_position(array(1.0, 202.2, null, 3.333), 1.0) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.LongType() # type: ignore + + +@pytest.mark.asyncio +async def test_array_remove(session: AsyncSession): + """ + Test the `array_remove` function + """ + query = parse( + """ + SELECT array_remove(array(1.0, 202.2, null, 3.333), 1.0) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType(element_type=ct.FloatType()) # type: ignore + + +@pytest.mark.asyncio +async def test_array_repeat(session: AsyncSession): + """ + Test the `array_repeat` function + """ + query = parse( + """ + SELECT array_repeat('abc', 10), array_repeat(100, 10), array_repeat(1.23, 10) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType(element_type=ct.StringType()) # type: ignore + assert query.select.projection[1].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + assert query.select.projection[2].type == ct.ListType(element_type=ct.FloatType()) # type: ignore + + +@pytest.mark.asyncio +async def test_array_size(session: AsyncSession): + """ + Test the `array_size` and `array_length` functions + """ + query = parse( + """ + SELECT + array_size(array('abc', 'd', 'e', 'f')), + array_length(array('abc', 'd', 'e', 'f')) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.LongType() # type: ignore + assert query.select.projection[0].function().dialects == [Dialect.SPARK] # type: ignore + assert query.select.projection[1].type == ct.LongType() # type: ignore + assert query.select.projection[1].function().dialects == [Dialect.DRUID] # type: ignore + + +@pytest.mark.asyncio +async def test_array_sort(session: AsyncSession): + """ + Test the `array_sort` function + """ + query = parse( + """ + SELECT + array_sort(array('b', 'd', null, 'c', 'a')) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType( # type: ignore + element_type=ct.StringType(), + ) + + +@pytest.mark.asyncio +async def test_array_union(session: AsyncSession): + """ + Test the `array_union` function + """ + query = parse( + """ + SELECT array_union(array('b', 'd', null), array('c', 'a')) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType( # type: ignore + element_type=ct.StringType(), + ) + + +@pytest.mark.asyncio +async def test_array_overlap(session: AsyncSession): + """ + Test the `array_overlap` function + """ + query = parse( + """ + SELECT arrays_overlap(array(1, 2, 3), array(3, 4, 5)) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_avg() -> None: + """ + Test ``avg`` function. + """ + assert ( + Avg.infer_type(ast.Column(ast.Name("x"), _type=IntegerType())) == DoubleType() + ) + assert Avg.infer_type(ast.Column(ast.Name("x"), _type=FloatType())) == DoubleType() + assert Avg.dialects == [Dialect.SPARK, Dialect.DRUID] + + +@pytest.mark.asyncio +async def test_cardinality(session: AsyncSession): + """ + Test the `cardinality` Spark function + """ + query_with_list = parse("SELECT cardinality(array('b', 'd', 'c', 'a'))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query_with_list.compile(ctx) + assert not exc.errors + assert query_with_list.select.projection[0].type == ct.IntegerType() # type: ignore + + query_with_map = parse("SELECT cardinality(map('a', 1, 'b', 2))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query_with_map.compile(ctx) + assert not exc.errors + assert query_with_map.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_cbrt_func(session: AsyncSession): + """ + Test the `cbrt` function + """ + query = parse("SELECT cbrt(27), cbrt(64.0)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == FloatType() # type: ignore + assert query.select.projection[1].type == FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_char_func(session: AsyncSession): + """ + Test the `char` function + """ + query = parse("SELECT char(65), char(97)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == StringType() # type: ignore + assert query.select.projection[1].type == StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_char_length_func(session: AsyncSession): + """ + Test the `char_length` function + """ + query = parse("SELECT char_length('hello'), char_length('world')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == IntegerType() # type: ignore + assert query.select.projection[1].type == IntegerType() # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_character_length_func(session: AsyncSession): + """ + Test the `character_length` function + """ + query = parse("SELECT character_length('hello'), character_length('world')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == IntegerType() # type: ignore + assert query.select.projection[1].type == IntegerType() # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_chr_func(session: AsyncSession): + """ + Test the `chr` function + """ + query = parse("SELECT chr(65), chr(97)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == StringType() # type: ignore + assert query.select.projection[1].type == StringType() # type: ignore + + +@pytest.mark.parametrize( + "types, expected", + [ + ((ct.IntegerType(),), ct.BigIntType()), + ((ct.FloatType(),), ct.BigIntType()), + ((ct.DoubleType(),), ct.BigIntType()), + ((ct.TinyIntType(), ct.IntegerType()), ct.DecimalType(precision=3, scale=0)), + ((ct.SmallIntType(), ct.IntegerType()), ct.DecimalType(precision=5, scale=0)), + ((ct.IntegerType(), ct.IntegerType()), ct.DecimalType(precision=10, scale=0)), + ((ct.BigIntType(), ct.IntegerType()), ct.DecimalType(precision=20, scale=0)), + ((ct.DoubleType(), ct.IntegerType()), ct.DecimalType(precision=30, scale=0)), + ((ct.FloatType(), ct.IntegerType()), ct.DecimalType(precision=14, scale=0)), + ( + (ct.DecimalType(10, 2), ct.IntegerType()), + ct.DecimalType(precision=9, scale=0), + ), + ( + (ct.DecimalType(precision=9, scale=0),), + ct.DecimalType(precision=10, scale=0), + ), + ], +) +@pytest.mark.asyncio +async def test_ceil(types, expected) -> None: + """ + Test ``ceil`` function. + """ + if len(types) == 1: + assert F.Ceil.infer_type(ast.Column(ast.Name("x"), _type=types[0])) == expected + else: + assert ( + F.Ceil.infer_type( + *( + ast.Column(ast.Name("x"), _type=types[0]), + ast.Number(0, _type=types[1]), + ) + ) + == expected + ) + assert F.Ceil.dialects == [Dialect.SPARK, Dialect.DRUID] + + +@pytest.mark.asyncio +async def test_ceil_ceiling_funcs(session: AsyncSession): + """ + Test the `ceil` and `ceiling` functions + """ + query = parse( + "SELECT ceil(-0.1), ceil(5), ceil(3.1411, 3), ceil(3.1411, -3), " + "ceiling(-0.1), ceiling(5), ceiling(3.1411, 3), ceiling(3.1411, -3)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == BigIntType() # type: ignore + assert query.select.projection[1].type == BigIntType() # type: ignore + assert query.select.projection[2].type == DecimalType(precision=14, scale=3) # type: ignore + assert query.select.projection[3].type == DecimalType(precision=14, scale=0) # type: ignore + assert query.select.projection[4].type == BigIntType() # type: ignore + assert query.select.projection[5].type == BigIntType() # type: ignore + assert query.select.projection[6].type == DecimalType(precision=14, scale=3) # type: ignore + assert query.select.projection[7].type == DecimalType(precision=14, scale=0) # type: ignore + + +@pytest.mark.asyncio +async def test_coalesce_infer_type() -> None: + """ + Test type inference in the ``Coalesce`` function. + """ + assert ( + Coalesce.infer_type( + ast.Column(ast.Name("x"), _type=StringType()), + ast.Column(ast.Name("x"), _type=StringType()), + ast.Column(ast.Name("x"), _type=StringType()), + ) + == StringType() + ) + + assert ( + Coalesce.infer_type( + ast.Column(ast.Name("x"), _type=IntegerType()), + ast.Column(ast.Name("x"), _type=NullType()), + ast.Column(ast.Name("x"), _type=BigIntType()), + ) + == IntegerType() + ) + + assert ( + Coalesce.infer_type( + ast.Column(ast.Name("x"), _type=StringType()), + ast.Column(ast.Name("x"), _type=StringType()), + ast.Column(ast.Name("x"), _type=NullType()), + ) + == StringType() + ) + assert Coalesce.dialects == [Dialect.SPARK, Dialect.DRUID] + + +@pytest.mark.asyncio +async def test_concat_func(session: AsyncSession): + """ + Test the `concat` function + """ + query = parse( + "SELECT concat('hello', '+', 'world'), " + "concat(array(1, 2), array(3)), " + "concat(map(1, 'a'), map(2, 'b'))", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.ListType(ct.IntegerType()) # type: ignore + assert query.select.projection[2].type == ct.MapType( # type: ignore + key_type=ct.IntegerType(), + value_type=ct.StringType(), + ) + + +@pytest.mark.asyncio +async def test_concat_ws_func(session: AsyncSession): + """ + Test the `concat_ws` function + """ + query = parse( + "SELECT concat_ws(',', 'hello', 'world'), " + "concat_ws('-', 'spark', 'sql', 'function'), " + "concat_ws('-', array('spark', 'sql', 'function'))", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == StringType() # type: ignore + assert query.select.projection[1].type == StringType() # type: ignore + assert query.select.projection[2].type == StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_contains_string(session: AsyncSession): + """ + Test the `contains_string` function + """ + query = parse( + "SELECT contains_string('hello', 'world')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + assert query.select.projection[0].function().dialects == [Dialect.DRUID] # type: ignore + + +@pytest.mark.asyncio +async def test_collect_list(session: AsyncSession): + """ + Test the `collect_list` function + """ + query = parse("SELECT collect_list(col) FROM (SELECT (1), (2) AS col)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType( # type: ignore + element_type=ct.IntegerType(), + ) + + +@pytest.mark.asyncio +async def test_collect_set(session: AsyncSession): + """ + Test the `collect_set` function + """ + query = parse("SELECT collect_set(col) FROM (SELECT (1), (2) AS col)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType( # type: ignore + element_type=ct.IntegerType(), + ) + + +@pytest.mark.asyncio +async def test_contains_func(session: AsyncSession): + """ + Test the `contains` function + """ + query = parse( + "SELECT contains('hello world', 'world'), contains('hello world', 'spark')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + assert query.select.projection[1].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_conv_func(session: AsyncSession): + """ + Test the `conv` function + """ + query = parse("SELECT conv('10', 10, 2), conv(15, 10, 16)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_convert_timezone_func(session: AsyncSession): + """ + Test the `convert_timezone` function + """ + query = parse( + "SELECT convert_timezone('PST', 'EST', cast('2023-07-30 12:34:56' as timestamp))", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.TimestampType() # type: ignore + + +@pytest.mark.asyncio +async def test_corr_func(session: AsyncSession): + """ + Test the `corr` function + """ + query = parse("SELECT corr(2.0, 3.0), corr(5, 10)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_cos_func(session: AsyncSession): + """ + Test the `cos` function + """ + query = parse("SELECT cos(0), cos(3.1416)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_cosh_func(session: AsyncSession): + """ + Test the `cosh` function + """ + query = parse("SELECT cosh(0), cosh(1.0)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_cot_func(session: AsyncSession): + """ + Test the `cot` function + """ + query = parse("SELECT cot(1), cot(0.7854)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_count() -> None: + """ + Test ``Count`` function. + """ + assert ( + Count.infer_type(ast.Column(ast.Name("x"), _type=WildcardType())) + == BigIntType() + ) + assert Count.is_aggregation is True + assert Count.dialects == [Dialect.SPARK, Dialect.DRUID] + + +@pytest.mark.asyncio +async def test_count_if_func(session: AsyncSession): + """ + Test the `count_if` function + """ + query = parse("SELECT count_if(true), count_if(false)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_count_min_sketch(session: AsyncSession): + """ + Test the `count_min_sketch` function + """ + query = parse( + "SELECT count_min_sketch(col, 0.5, 0.5, 1) FROM (SELECT (1), (2), (1) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BinaryType() # type: ignore + + +@pytest.mark.asyncio +async def test_covar_pop_func(session: AsyncSession): + """ + Test the `covar_pop` function + """ + query = parse("SELECT covar_pop(1.0, 2.0), covar_pop(3, 4)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_covar_samp_func(session: AsyncSession): + """ + Test the `covar_samp` function + """ + query = parse("SELECT covar_samp(1.0, 2.0), covar_samp(3, 4)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_crc32_func(session: AsyncSession): + """ + Test the `crc32` function + """ + query = parse("SELECT crc32('hello'), crc32('world')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BigIntType() # type: ignore + assert query.select.projection[1].type == ct.BigIntType() # type: ignore + + +@pytest.mark.asyncio +async def test_csc_func(session: AsyncSession): + """ + Test the `csc` function + """ + query = parse("SELECT csc(1), csc(0.7854)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_cume_dist_func(session: AsyncSession): + """ + Test the `cume_dist` function + """ + query = parse("SELECT cume_dist(), cume_dist()") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_curdate_func(session: AsyncSession): + """ + Test the `curdate` function + """ + query = parse("SELECT curdate(), curdate()") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DateType() # type: ignore + assert query.select.projection[1].type == ct.DateType() # type: ignore + + +@pytest.mark.asyncio +async def test_current_catalog_func(session: AsyncSession): + """ + Test the `current_catalog` function + """ + query = parse("SELECT current_catalog(), current_catalog()") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_current_database_func(session: AsyncSession): + """ + Test the `current_database` function + """ + query = parse("SELECT current_database(), current_database()") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_current_schema_func(session: AsyncSession): + """ + Test the `current_schema` function + """ + query = parse("SELECT current_schema(), current_schema()") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_current_timezone_current_user_funcs(session: AsyncSession): + """ + Test the `current_timezone` function + Test the `current_user` function + """ + query = parse("SELECT current_timezone(), current_user()") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_date_func(session: AsyncSession): + """ + Test the `date` function + """ + query = parse( + "SELECT date('2023-07-30'), date(cast('2023-07-30 12:34:56' as timestamp))", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DateType() # type: ignore + assert query.select.projection[1].type == ct.DateType() # type: ignore + + +@pytest.mark.asyncio +async def test_date_from_unix_date_func(session: AsyncSession): + """ + Test the `date_from_unix_date` function + """ + query = parse("SELECT date_from_unix_date(0), date_from_unix_date(18500)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DateType() # type: ignore + assert query.select.projection[1].type == ct.DateType() # type: ignore + + +@pytest.mark.asyncio +async def test_date_format(session: AsyncSession) -> None: + """ + Test ``date_format`` function. + """ + query_with_array = parse("SELECT date_format(NOW(), 'yyyyMMdd') as date_partition") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query_with_array.compile(ctx) + assert not exc.errors + assert query_with_array.select.projection[0].type == StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_date_part_func(session: AsyncSession): + """ + Test the `date_part` function + """ + query = parse( + "SELECT date_part('year', cast('2023-07-30' as date)), date_part('hour', cast('2023-07-30 12:34:56' as timestamp))", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_dj_logical_timestamp(session: AsyncSession) -> None: + """ + Test ``DJ_LOGICAL_TIMESTAMP`` function. + """ + query_with_array = parse("SELECT dj_logical_timestamp() as date_partition") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query_with_array.compile(ctx) + assert not exc.errors + assert query_with_array.select.projection[0].type == StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_dayofmonth_func(session: AsyncSession): + """ + Test the `dayofmonth` function + """ + query = parse( + "SELECT dayofmonth(cast('2023-07-30' as date)), dayofmonth('2023-01-01')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_dayofweek_func(session: AsyncSession): + """ + Test the `dayofweek` function + """ + query = parse( + "SELECT dayofweek(cast('2023-07-30' as date)), dayofweek('2023-01-01')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_dayofyear_func(session: AsyncSession): + """ + Test the `dayofyear` function + """ + query = parse( + "SELECT dayofyear(cast('2023-07-30' as date)), dayofyear('2023-01-01')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_decimal_func(session: AsyncSession): + """ + Test the `decimal` function + """ + query = parse("SELECT decimal(123), decimal('456.78')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DecimalType(8, 6) # type: ignore + assert query.select.projection[1].type == ct.DecimalType(8, 6) # type: ignore + + +@pytest.mark.asyncio +async def test_decode_func(session: AsyncSession): + """ + Test the `decode` function + """ + query = parse("SELECT decode(unhex('4D7953514C'), 'UTF-8')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_degrees_func(session: AsyncSession): + """ + Test the `degrees` function + """ + query = parse("SELECT degrees(1), degrees(3.141592653589793)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_div(session: AsyncSession): + """ + Test the `div` function + """ + query = parse("SELECT div(1, 2)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.LongType() # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_double_func(session: AsyncSession): + """ + Test the `double` function + """ + query = parse("SELECT double('123.45'), double(67890)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DoubleType() # type: ignore + assert query.select.projection[1].type == ct.DoubleType() # type: ignore + + +@pytest.mark.asyncio +async def test_e_func(session: AsyncSession): + """ + Test the `e` function + """ + query = parse("SELECT e(), e()") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + assert query.select.projection[1].function().dialects == [Dialect.SPARK] # type: ignore + + +@pytest.mark.asyncio +async def test_element_at(session: AsyncSession): + """ + Test the `element_at` Spark function + """ + query_with_array = parse("SELECT element_at(array(1, 2, 3, 4), 2)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query_with_array.compile(ctx) + assert not exc.errors + assert query_with_array.select.projection[0].type == IntegerType() # type: ignore + + query_with_map = parse("SELECT element_at(map(1, 'a', 2, 'b'), 2)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query_with_map.compile(ctx) + assert not exc.errors + assert query_with_map.select.projection[0].type == StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_elt_func(session: AsyncSession): + """ + Test the `elt` function + """ + query = parse("SELECT elt(1, 'a', 'b', 'c'), elt(3, 'd', 'e', 'f')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_encode_func(session: AsyncSession): + """ + Test the `encode` function + """ + query = parse("SELECT encode('hello', 'UTF-8'), encode('world', 'UTF-8')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_endswith_func(session: AsyncSession): + """ + Test the `endswith` function + """ + query = parse("SELECT endswith('hello', 'lo'), endswith('world', 'ld')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + assert query.select.projection[1].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_equal_null_func(session: AsyncSession): + """ + Test the `equal_null` function + """ + query = parse("SELECT equal_null('hello', 'hello'), equal_null(NULL, NULL)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + assert query.select.projection[1].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_every_func(session: AsyncSession): + """ + Test the `every` function + """ + query = parse("SELECT every(col), every(col) FROM (SELECT (true), (false) AS col)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + assert query.select.projection[1].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_exists_func(session: AsyncSession): + """ + Test the `exists` function + """ + query = parse("SELECT exists(array(1, 2, 3), x -> x % 2 > 0)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_explode_outer_func(session: AsyncSession): + """ + Test the `explode_outer` function + """ + query = parse( + "SELECT explode_outer(array(1, 2, 3)), explode_outer(map('key1', 'value1', 'key2', 'value2'))", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_expm1_func(session: AsyncSession): + """ + Test the `expm1` function + """ + query = parse("SELECT expm1(1), expm1(0.5)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_factorial_func(session: AsyncSession): + """ + Test the `factorial` function + """ + query = parse("SELECT factorial(0), factorial(5)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_filter(session: AsyncSession): + """ + Test the `filter` function + """ + query = parse("SELECT filter(col, s -> s != 3) FROM (SELECT array(1, 2, 3) AS col)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType( # type: ignore + element_type=ct.IntegerType(), + ) + + query = parse( + "SELECT filter(col, (s, i) -> s + i != 3) FROM (SELECT array(1, 2, 3) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType( # type: ignore + element_type=ct.IntegerType(), + ) + + with pytest.raises(DJParseException): + query = parse( + "SELECT filter(col, (s, i, a) -> s + i != 3) FROM (SELECT array(1, 2, 3) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + + +@pytest.mark.asyncio +async def test_find_in_set_func(session: AsyncSession): + """ + Test the `find_in_set` function + """ + query = parse("SELECT find_in_set('b', 'a,b,c,d'), find_in_set('e', 'a,b,c,d')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_first_and_first_value(session: AsyncSession): + """ + Test `first` and `first_value` + """ + query = parse( + "SELECT first(col), first(col, true), first_value(col), " + "first_value(col, true) FROM (SELECT (1), (2) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + assert query.select.projection[2].type == ct.IntegerType() # type: ignore + assert query.select.projection[3].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_flatten(session: AsyncSession): + """ + Test `flatten` + """ + query = parse("SELECT flatten(array(array(1, 2), array(3, 4)))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType( # type: ignore + element_type=ct.IntegerType(), + ) + + +@pytest.mark.asyncio +async def test_float_func(session: AsyncSession): + """ + Test the `float` function + """ + query = parse("SELECT float(123), float('456.78')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + + +@pytest.mark.parametrize( + "types, expected", + [ + ((ct.IntegerType(),), ct.BigIntType()), + ((ct.FloatType(),), ct.BigIntType()), + ((ct.DoubleType(),), ct.BigIntType()), + ((ct.TinyIntType(), ct.IntegerType()), ct.DecimalType(precision=3, scale=0)), + ((ct.SmallIntType(), ct.IntegerType()), ct.DecimalType(precision=5, scale=0)), + ((ct.IntegerType(), ct.IntegerType()), ct.DecimalType(precision=10, scale=0)), + ((ct.BigIntType(), ct.IntegerType()), ct.DecimalType(precision=20, scale=0)), + ((ct.DoubleType(), ct.IntegerType()), ct.DecimalType(precision=30, scale=0)), + ((ct.FloatType(), ct.IntegerType()), ct.DecimalType(precision=14, scale=0)), + ( + (ct.DecimalType(10, 2), ct.IntegerType()), + ct.DecimalType(precision=9, scale=0), + ), + ( + (ct.DecimalType(precision=9, scale=0),), + ct.DecimalType(precision=10, scale=0), + ), + ], +) +@pytest.mark.asyncio +async def test_floor(types, expected) -> None: + """ + Test ``floor`` function. + """ + if len(types) == 1: + assert F.Floor.infer_type(ast.Column(ast.Name("x"), _type=types[0])) == expected + else: + assert ( + F.Floor.infer_type( + *( + ast.Column(ast.Name("x"), _type=types[0]), + ast.Number(0, _type=types[1]), + ) + ) + == expected + ) + assert F.Floor.dialects == [Dialect.SPARK, Dialect.DRUID] + + +@pytest.mark.asyncio +async def test_forall_func(session: AsyncSession): + """ + Test the `forall` function + """ + query = parse( + "SELECT forall(array(1, 2, 3), x -> x > 0), forall(array(1, 2, 3), x -> x < 0)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + # assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + assert query.select.projection[1].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_format_number_func(session: AsyncSession): + """ + Test the `format_number` function + """ + query = parse( + "SELECT format_number(12345.6789, 2), format_number(98765.4321, '###.##')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_format_string_func(session: AsyncSession): + """ + Test the `format_string` function + """ + query = parse( + "SELECT format_string('%s %s', 'hello', 'world'), format_string('%d %d', 1, 2)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +# TODO: Fix these two +# @pytest.mark.asyncio +# async def test_from_csv_func(session: AsyncSession): +# """ +# Test the `from_csv` function +# """ +# query = parse("SELECT from_csv('1,2,3', 'a INT, b INT, c INT'), from_csv('4,5,6', 'x INT, y INT, z INT')") +# exc = DJException() +# ctx = ast.CompileContext(session=session, exception=exc) +# await query.compile(ctx) +# assert not exc.errors +# assert isinstance(query.select.projection[0].type, ct.StructType) # type: ignore +# assert isinstance(query.select.projection[1].type, ct.StructType) # type: ignore + + +@pytest.mark.asyncio +async def test_from_json_func(session: AsyncSession): + """ + Test the `from_json` function + """ + query = parse( + "SELECT from_json('1,2,3', 'a INT, b INT, c INT'), from_json('[\"a\",\"b\"]', 'ARRAY')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert isinstance(query.select.projection[0].type, ct.StructType) # type: ignore + assert isinstance(query.select.projection[1].type, ct.ListType) # type: ignore + + +@pytest.mark.asyncio +async def test_from_unix_time_func(session: AsyncSession): + """ + Test the `from_unix_time` function + """ + query = parse( + "SELECT from_unixtime(1609459200, 'yyyy-MM-dd HH:mm:ss'), from_unixtime(1609459200, 'dd/MM/yyyy HH:mm:ss')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_from_utc_timestamp_func(session: AsyncSession): + """ + Test the `from_utc_timestamp` function + """ + query = parse( + "SELECT from_utc_timestamp('2023-01-01 00:00:00', 'PST'), " + "from_utc_timestamp(cast('2023-01-01 00:00:00' as timestamp), 'IST')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.TimestampType() # type: ignore + assert query.select.projection[1].type == ct.TimestampType() # type: ignore + + +@pytest.mark.asyncio +async def test_get_func(session: AsyncSession): + """ + Test the `get` function + """ + query = parse("SELECT get(array(1, 2, 3), 0), get(array('a', 'b', 'c'), 1)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_get_json_object_func(session: AsyncSession): + """ + Test the `get_json_object` function + """ + query = parse( + "SELECT get_json_object('{\"key\": \"value\"}', '$.key'), " + 'get_json_object(\'{"key1": "value1", "key2": "value2"}\', \'$.key2\')', + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_getbit_func(session: AsyncSession): + """ + Test the `getbit` function + """ + query = parse("SELECT getbit(1010, 0), getbit(1010, 1)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_greatest(session: AsyncSession): + """ + Test `greatest` + """ + query = parse("SELECT greatest(10, 9, 2, 4, 3)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_grouping_func(session: AsyncSession): + """ + Test the `grouping` function + """ + query = parse( + "SELECT grouping(col1), grouping(col2) FROM " + "(SELECT (1), (2) AS col1, (3), (4) AS col2) GROUP BY col1", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_grouping_id_func(session: AsyncSession): + """ + Test the `grouping_id` function + """ + query = parse( + "SELECT grouping_id(col1, col2) FROM " + "(SELECT (1), (2) AS col1, (3), (4) AS col2) GROUP BY col1", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BigIntType() # type: ignore + + +@pytest.mark.asyncio +async def test_hash_func(session: AsyncSession): + """ + Test the `hash` function + """ + query = parse("SELECT hash('hello'), hash(123)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_hex_func(session: AsyncSession): + """ + Test the `hex` function + """ + query = parse("SELECT hex('hello'), hex(123)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_histogram_numeric_func(session: AsyncSession): + """ + Test the `histogram_numeric` function + """ + query = parse("SELECT histogram_numeric(col, 5) FROM (SELECT (1), (2) AS col)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert isinstance(query.select.projection[0].type, ct.ListType) # type: ignore + assert isinstance(query.select.projection[0].type.element.type, ct.StructType) # type: ignore + + +@pytest.mark.asyncio +async def test_hour_func(session: AsyncSession): + """ + Test the `hour` function + """ + query = parse( + "SELECT hour(cast('2023-01-01 12:34:56' as timestamp)), hour('2023-01-01 23:45:56')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_hypot_func(session: AsyncSession): + """ + Test the `hypot` function + """ + query = parse("SELECT hypot(3, 4), hypot(5.0, 12.0)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + assert query.select.projection[1].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_if(session: AsyncSession): + """ + Test the `if` functions + """ + query = parse( + "SELECT if(col1 = 'x', NULL, 1) FROM (SELECT ('aee'), ('bee') AS col1)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_ilike_like_func(session: AsyncSession): + """ + Test the `ilike`, `like` functions + """ + query = parse( + "SELECT col1 ilike '%pattern%', ilike(col1, '%pattern%'), " + "like(col1, '%pattern%') FROM (SELECT ('aee'), ('bee') AS col1)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + assert query.select.projection[1].type == ct.BooleanType() # type: ignore + assert query.select.projection[2].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_initcap_func(session: AsyncSession): + """ + Test the `initcap` function + """ + query = parse("SELECT initcap('hello world'), initcap('SQL function')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_inline_func(session: AsyncSession): + """ + Test the `inline` function + """ + # This test assumes there's a table with a column of type ARRAY> + query = parse( + "SELECT inline(col1), inline_outer(array(struct(1, 'a'), struct(2, 'b')))" + " FROM (SELECT (array(struct('a', 1, 'b', '222'))) AS col1)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert isinstance(query.select.projection[0].type, ct.StructType) # type: ignore + assert isinstance(query.select.projection[1].type, ct.StructType) # type: ignore + + +@pytest.mark.asyncio +async def test_input_file_block_length_func(session: AsyncSession): + """ + Test the `input_file_block_length` function + """ + query = parse("SELECT input_file_block_length()") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.LongType() # type: ignore + + +@pytest.mark.asyncio +async def test_input_file_block_start_func(session: AsyncSession): + """ + Test the `input_file_block_start` function + """ + query = parse("SELECT input_file_block_start()") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.LongType() # type: ignore + + +@pytest.mark.asyncio +async def test_input_file_name_func(session: AsyncSession): + """ + Test the `input_file_name` function + """ + query = parse("SELECT input_file_name()") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_int(session: AsyncSession): + """ + Test the `int` function + """ + query = parse("SELECT int('3')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_instr_func(session: AsyncSession): + """ + Test the `instr` function + """ + query = parse("SELECT instr('hello world', 'world'), instr('hello world', 'SQL')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_isnan_func(session: AsyncSession): + """ + Test the `isnan` function + """ + query = parse("SELECT isnan(1/0), isnan(0.0/0.0)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + assert query.select.projection[1].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_isnotnull_isnull(session: AsyncSession): + """ + Test the `isnotnull`, `isnull` functions + """ + query = parse("SELECT isnotnull(0), isnull(null)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + assert query.select.projection[1].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_json_array_length_func(session: AsyncSession): + """ + Test the `json_array_length` function + """ + query = parse("SELECT json_array_length('[1, 2, 3]')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_json_object_keys_func(session: AsyncSession): + """ + Test the `json_object_keys` function + """ + query = parse('SELECT json_object_keys(\'{"key1": "value1", "key2": "value2"}\')') + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert isinstance(query.select.projection[0].type, ct.ListType) # type: ignore + assert query.select.projection[0].type.element.type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_json_tuple_func(session: AsyncSession): + """ + Test the `json_tuple` function + """ + query = parse( + 'SELECT json_tuple(\'{"key1": "value1", "key2": "value2"}\', \'key1\', \'key2\')', + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert isinstance(query.select.projection[0].type, ct.ListType) # type: ignore + + +@pytest.mark.asyncio +async def test_kurtosis_func(session: AsyncSession): + """ + Test the `kurtosis` function + """ + query = parse("SELECT kurtosis(col) FROM (SELECT (1), (2), (3) AS col)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DoubleType() # type: ignore + + +@pytest.mark.asyncio +async def test_lag_func(session: AsyncSession): + """ + Test the `lag` function + """ + query = parse( + "SELECT lag(col) OVER (ORDER BY col) FROM (SELECT (1), (2), (3) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + # The output type depends on the type of `col` + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_last_and_last_value(session: AsyncSession): + """ + Test the `last` function + """ + query = parse( + "SELECT last(col) OVER (PARTITION BY col2 ORDER BY col3), " + "last_value(col) OVER (PARTITION BY col2 ORDER BY col3) " + "FROM (SELECT (1), (2), (3) AS col, (1), (2), (3) AS col2, " + "(3), (4), (5) as col3)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + # The output type depends on the type of `col` + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_last_day_func(session: AsyncSession): + """ + Test the `last_day` function + """ + query = parse("SELECT last_day('2023-01-01'), last_day(cast('2023-02-15' as date))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DateType() # type: ignore + assert query.select.projection[1].type == ct.DateType() # type: ignore + + +@pytest.mark.asyncio +async def test_lcase_func(session: AsyncSession): + """ + Test the `lcase` function + """ + query = parse("SELECT lcase('HELLO'), lcase('WORLD')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_lead_func(session: AsyncSession): + """ + Test the `lead` function + """ + query = parse( + "SELECT lead(col, 1, 'N/A') OVER (ORDER BY col) FROM (SELECT ('1'), ('a'), ('x') AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + # The output type depends on the type of `col` + # Assuming `col` is of type StringType for this test + assert query.select.projection[0].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_least_func(session: AsyncSession): + """ + Test the `least` function + """ + query = parse("SELECT least(10, 20, 30, 40)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[0].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +# TODO: figure out why antlr parser fails +# @pytest.mark.asyncio +# async def test_left_func(session: AsyncSession): +# """ +# Test the `left` function +# """ +# query = parse("SELECT left('hello world', 5)") +# exc = DJException() +# ctx = ast.CompileContext(session=session, exception=exc) +# await query.compile(ctx) +# assert not exc.errors +# assert query.select.projection[0].type == ct.StringType() # type: ignore +# assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_len_func(session: AsyncSession): + """ + Test the `len` function + """ + query = parse("SELECT len('hello'), length('world')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[0].function().dialects == [Dialect.SPARK] # type: ignore + + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].function().dialects == [ # type: ignore + Dialect.SPARK, + Dialect.DRUID, + ] + + +@pytest.mark.asyncio +async def test_like_func(session: AsyncSession): + """ + Test the `like` function + """ + query = parse( + "SELECT col like '%pattern%' FROM (SELECT ('a'), ('b'), ('c') AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_localtimestamp_func(session: AsyncSession): + """ + Test the `localtimestamp` function + """ + query = parse("SELECT localtimestamp()") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.TimestampType() # type: ignore + + +@pytest.mark.asyncio +async def test_locate_func(session: AsyncSession): + """ + Test the `locate` function + """ + query = parse( + "SELECT locate('world', 'hello world'), locate('SQL', 'hello world', 2)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_log_functions(session: AsyncSession): + """ + Test the `log1p`, `log10` and `log2` functions + """ + query = parse( + "SELECT log1p(col), log10(col), log2(col) FROM (SELECT (1), (2), (3) as col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DoubleType() # type: ignore + assert query.select.projection[1].type == ct.DoubleType() # type: ignore + assert query.select.projection[2].type == ct.DoubleType() # type: ignore + + +@pytest.mark.asyncio +async def test_lpad_func(session: AsyncSession): + """ + Test the `lpad` function + """ + query = parse("SELECT lpad('hello', 10, ' '), lpad('SQL', 5, '0')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_ltrim_func(session: AsyncSession): + """ + Test the `ltrim` function + """ + query = parse("SELECT ltrim(' hello'), ltrim('-----world-', '-')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_make_date_func(session: AsyncSession): + """ + Test the `make_date` function + """ + query = parse("SELECT make_date(2023, 7, 30), make_date(2023, 12, 31)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DateType() # type: ignore + assert query.select.projection[1].type == ct.DateType() # type: ignore + + +@pytest.mark.asyncio +async def test_make_dt_interval_func(session: AsyncSession): + """ + Test the `make_dt_interval` function + """ + query = parse("SELECT make_dt_interval(1, 2, 30, 45)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DayTimeIntervalType() # type: ignore + + +@pytest.mark.asyncio +async def test_make_interval_func(session: AsyncSession): + """ + Test the `make_interval` function + """ + query = parse("SELECT make_interval(1, 6)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.YearMonthIntervalType() # type: ignore + + +@pytest.mark.asyncio +async def test_make_timestamp_func(session: AsyncSession): + """ + Test the `make_timestamp` function + """ + query = parse("SELECT make_timestamp(2023, 7, 30, 14, 45, 30)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.TimestampType() # type: ignore + + +@pytest.mark.asyncio +async def test_make_timestamp_ltz_func(session: AsyncSession): + """ + Test the `make_timestamp_ltz` function + """ + query = parse( + "SELECT make_timestamp_ltz(2023, 7, 30, 14, 45, 30, 'UTC'), " + "make_timestamp_ltz(2023, 7, 30, 14, 45, 30)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.TimestampType() # type: ignore + assert query.select.projection[1].type == ct.TimestampType() # type: ignore + + +@pytest.mark.asyncio +async def test_make_timestamp_ntz_func(session: AsyncSession): + """ + Test the `make_timestamp_ntz` function + """ + query = parse("SELECT make_timestamp_ntz(2023, 7, 30, 14, 45, 30)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.TimestampType() # type: ignore + + +@pytest.mark.asyncio +async def test_make_ym_interval_func(session: AsyncSession): + """ + Test the `make_ym_interval` function + """ + query = parse("SELECT make_ym_interval(1, 6)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.YearMonthIntervalType() # type: ignore + + +@pytest.mark.asyncio +async def test_map_concat_func(session: AsyncSession): + """ + Test the `map_concat` function + """ + query = parse("SELECT map_concat(map(1, 'a', 2, 'b'), map(1, 'c'))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.MapType( # type: ignore + key_type=ct.IntegerType(), + value_type=ct.StringType(), + ) + + +@pytest.mark.asyncio +async def test_map_contains_key_func(session: AsyncSession): + """ + Test the `map_contains_key` function + """ + query = parse("SELECT map_contains_key(map(1, 'a', 2, 'b'), 1)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_map_entries_func(session: AsyncSession): + """ + Test the `map_entries` function + """ + query = parse("SELECT map_entries(map(1, 'a', 2, 'b'))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType( # type: ignore + element_type=ct.StructType( + ct.NestedField(name="key", field_type=ct.IntegerType()), # type: ignore + ct.NestedField(name="value", field_type=ct.StringType()), # type: ignore + ), + ) # type: ignore + + +@pytest.mark.asyncio +async def test_map_filter_func(session: AsyncSession): + """ + Test the `map_filter` function + """ + query = parse("SELECT map_filter(map(1, 0, 2, 2, 3, -1), (k, v) -> k > v)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert query.select.projection[0].type == ct.MapType( # type: ignore + key_type=ct.IntegerType(), + value_type=ct.IntegerType(), + ) + + +@pytest.mark.asyncio +async def test_map_from_arrays_func(session: AsyncSession): + """ + Test the `map_from_arrays` function + """ + query = parse("SELECT map_from_arrays(array(1.0, 3.0), array('2', '4'))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.MapType( # type: ignore + key_type=ct.FloatType(), + value_type=ct.StringType(), + ) + + +@pytest.mark.asyncio +async def test_map_from_entries_func(session: AsyncSession): + """ + Test the `map_from_entries` function + """ + query = parse("SELECT map_from_entries(array(struct(1, 'a'), struct(2, 'b')))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.MapType( # type: ignore + key_type=ct.IntegerType(), + value_type=ct.StringType(), + ) + + +@pytest.mark.asyncio +async def test_map_keys_func(session: AsyncSession): + """ + Test the `map_keys` function + """ + query = parse("SELECT map_keys(map(1, 'a', 2, 'b'))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + + +@pytest.mark.asyncio +async def test_map_values_func(session: AsyncSession): + """ + Test the `map_values` function + """ + query = parse("SELECT map_values(map(1, 'a', 2, 'b'))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.StringType()) # type: ignore + + +@pytest.mark.asyncio +async def test_max() -> None: + """ + Test ``Max`` function. + """ + assert ( + Max.infer_type(ast.Column(ast.Name("x"), _type=IntegerType())) == IntegerType() + ) + assert Max.infer_type(ast.Column(ast.Name("x"), _type=BigIntType())) == BigIntType() + assert Max.infer_type(ast.Column(ast.Name("x"), _type=FloatType())) == FloatType() + assert Max.infer_type(ast.Column(ast.Name("x"), _type=StringType())) == StringType() + assert Max.infer_type( + ast.Column(ast.Name("x"), _type=DecimalType(8, 6)), + ) == DecimalType(8, 6) + + +@pytest.mark.asyncio +async def test_map_zip_with_func(session: AsyncSession): + """ + Test the `map_zip_with` function + """ + # The third argument to map_zip_with is a function, which needs special handling + # Assuming that we have a function "func" defined elsewhere in the code + query = parse( + "SELECT map_zip_with(map(1, 'a', 2, 'b'), map(1, 'x', 2, 'y'), (k, v1, v2) -> concat(v1, v2))", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert query.select.projection[0].type == ct.MapType( # type: ignore + key_type=ct.IntegerType(), + value_type=ct.StringType(), + ) + + +@pytest.mark.asyncio +async def test_mask_func(session: AsyncSession): + """ + Test the `mask` function + """ + query = parse( + "SELECT mask('abcd-EFGH-8765-4321'), " + "mask('abcd-EFGH-8765-4321', 'Q'), " + "mask('AbCD123-@$#', 'Q', 'q'), " + "mask('AbCD123-@$#', 'Q', 'q', 'd'), " + "mask('AbCD123-@$#', 'Q', 'q', 'd', 'o')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + assert query.select.projection[2].type == ct.StringType() # type: ignore + assert query.select.projection[3].type == ct.StringType() # type: ignore + assert query.select.projection[4].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_max_by_min_by_funcs(session: AsyncSession): + """ + Test the `max_by` function + """ + query = parse( + "SELECT max_by(x, y), min_by(x, y) " + "FROM (SELECT ('a'), ('b'), ('c') AS x, (10), (50), (20) AS y)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_md5_func(session: AsyncSession): + """ + Test the `md5` function + """ + query = parse("SELECT md5('Spark')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_mean_func(session: AsyncSession): + """ + Test the `mean` function + """ + query = parse("SELECT mean(col) FROM (SELECT (1), (2), (3) AS col)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DoubleType() # type: ignore + + +@pytest.mark.asyncio +async def test_median_func(session: AsyncSession): + """ + Test the `median` function + """ + query = parse("SELECT median(col) FROM (SELECT (1), (2), (3) AS col)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DoubleType() # type: ignore + + +@pytest.mark.asyncio +async def test_min() -> None: + """ + Test ``Min`` function. + """ + assert ( + Min.infer_type(ast.Column(ast.Name("x"), _type=IntegerType())) == IntegerType() + ) + assert Min.infer_type(ast.Column(ast.Name("x"), _type=BigIntType())) == BigIntType() + assert Min.infer_type(ast.Column(ast.Name("x"), _type=FloatType())) == FloatType() + assert Min.infer_type(ast.Column(ast.Name("x"), _type=StringType())) == StringType() + assert Min.infer_type( + ast.Column(ast.Name("x"), _type=DecimalType(8, 6)), + ) == DecimalType(8, 6) + + +@pytest.mark.asyncio +async def test_minute(session: AsyncSession): + """ + Test the `minute` function + """ + query = parse( + "SELECT minute('2009-07-30 12:58:59'), minute(cast('2009-07-30 12:58:59' as timestamp))", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_mod(session: AsyncSession): + """ + Test the `mod` function + """ + query = parse( + "SELECT MOD(2, 1.8)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_mode(session: AsyncSession): + """ + Test the `mode` function + """ + query = parse( + "SELECT mode(col) FROM (SELECT (0), (10), (10) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_monotonically_increasing_id(session: AsyncSession): + """ + Test the `monotonically_increasing_id` function + """ + query = parse( + "SELECT monotonically_increasing_id()", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BigIntType() # type: ignore + + +@pytest.mark.asyncio +async def test_months_between(session: AsyncSession): + """ + Test the `months_between` function + """ + query = parse( + "SELECT months_between('1997-02-28 10:30:00', '1996-10-30'), " + "months_between(cast('1997-02-28 10:30:00' as timestamp), cast('1996-10-30' as timestamp)), " + "months_between('1997-02-28 10:30:00', '1996-10-30', false)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_named_struct_func(session: AsyncSession): + """ + Test the `named_struct` function + """ + query = parse("SELECT named_struct('name', 'cactus', 'age', 30)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StructType( # type: ignore + ct.NestedField(name="name", field_type=ct.StringType()), # type: ignore + ct.NestedField(name="age", field_type=ct.IntegerType()), # type: ignore + ) + + +@pytest.mark.asyncio +async def test_nanvl_func(session: AsyncSession): + """ + Test the `nanvl` function + """ + query = parse("SELECT nanvl(cast('NaN' as double), 123)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DoubleType() # type: ignore + + +@pytest.mark.asyncio +async def test_negative_func(session: AsyncSession): + """ + Test the `negative` function + """ + query = parse("SELECT negative(1)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_next_day_func(session: AsyncSession): + """ + Test the `next_day` function + """ + query = parse("SELECT next_day('2015-01-14', 'TU')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DateType() # type: ignore + + +@pytest.mark.asyncio +async def test_not_func(session: AsyncSession): + """ + Test the `not` function + """ + query = parse("SELECT not(true)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_now() -> None: + """ + Test ``Now`` function. + """ + assert Now.infer_type() == ct.TimestampType() + + +@pytest.mark.asyncio +async def test_nth_value_func(session: AsyncSession): + """ + Test the `nth_value` function + """ + query = parse( + "SELECT a, b, nth_value(b, 2) OVER (PARTITION BY a ORDER BY b) FROM " + "(SELECT ('A1'), ('A2'), ('A1') AS a, (2), (1), (3) AS b)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[2].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_ntile_func(session: AsyncSession): + """ + Test the `ntile` function + """ + query = parse( + "SELECT a, b, ntile(2) OVER (PARTITION BY a ORDER BY b) FROM" + "(SELECT ('A1'), ('A2'), ('A1') AS a, (2), (1), (3) AS b)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[2].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_nullif_func(session: AsyncSession): + """ + Test the `nullif` function + """ + query = parse("SELECT nullif(2, 2)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_nvl_func(session: AsyncSession): + """ + Test the `nvl` function + """ + query = parse("SELECT nvl(array('1'), array('2'))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.StringType()) # type: ignore + + +@pytest.mark.asyncio +async def test_nvl2_func(session: AsyncSession): + """ + Test the `nvl2` function + """ + query = parse("SELECT nvl2(3, NULL, 1)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_octet_length_func(session: AsyncSession): + """ + Test the `octet_length` function + """ + query = parse("SELECT octet_length('Spark SQL')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_overlay_func(session: AsyncSession): + """ + Test the `overlay` function + TODO: support syntax like: + SELECT overlay(encode('Spark SQL', 'utf-8') PLACING encode('_', 'utf-8') FROM 6); + """ + query = parse("SELECT overlay('Hello World', 'J', 7)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_percentile(session: AsyncSession): + """ + Test the `percentile` function + """ + query = parse("SELECT percentile(col, 0.3) FROM (SELECT (0), (10) AS col)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + + query = parse( + "SELECT percentile(col, array(0.25, 0.75)) FROM (SELECT (0), (10) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.FloatType()) # type: ignore + + query = parse( + "SELECT percentile(col, 0.5) FROM (" + "SELECT (INTERVAL '0' MONTH), (INTERVAL '10' MONTH) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + + query = parse( + "SELECT percentile(col, 0.5, 3) FROM (" + "SELECT (INTERVAL '0' MONTH), (INTERVAL '10' MONTH) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.FloatType() # type: ignore + + query = parse( + "SELECT percentile(col, array(0.2, 0.5)) " + "FROM (SELECT (INTERVAL '0' MONTH), (INTERVAL '10' MONTH) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(ct.FloatType()) # type: ignore + + query = parse( + "SELECT percentile(col, array(0.2, 0.5), 2) " + "FROM (SELECT (INTERVAL '0' MONTH), (INTERVAL '10' MONTH) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(ct.FloatType()) # type: ignore + + +@pytest.mark.asyncio +async def test_random(session: AsyncSession): + """ + Test `random`, `rand` and `randn` + """ + query = parse( + "SELECT random(), random(0), random(null), rand(), rand(0), " + "rand(null), randn(), randn(0), randn(null)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + for column in query.select.projection: + assert column.type == ct.FloatType() # type: ignore + + +@pytest.mark.asyncio +async def test_rank(session: AsyncSession): + """ + Test `dense_rank`, `rank` + """ + query = parse( + "SELECT " + "rank() OVER (PARTITION BY col ORDER BY col)," + "rank(col) OVER (PARTITION BY col ORDER BY col)," + "dense_rank() OVER (PARTITION BY col ORDER BY col)," + "dense_rank(col) OVER (PARTITION BY col ORDER BY col)" + " FROM (SELECT (1), (2) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + assert query.select.projection[2].type == ct.IntegerType() # type: ignore + assert query.select.projection[3].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_regexp_extract(session: AsyncSession): + """ + Test `regexp_extract` + """ + # w/o position arg + query = parse("SELECT regexp_extract('100-200', '(\\d+)')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + # w/ position arg + query = parse("SELECT regexp_extract('100-200', '(\\d+)', 42)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_regexp_like(session: AsyncSession): + """ + Test `regexp_like` + """ + query = parse( + "SELECT regexp_like('%SystemDrive%\\Users\\John', '%SystemDrive%\\Users.*')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BooleanType() # type: ignore + + +@pytest.mark.asyncio +async def test_regexp_replace(session: AsyncSession): + """ + Test `regexp_replace` + """ + # w/o position arg + query = parse("SELECT regexp_replace('100-200', '(\\d+)', 'num')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + # w/ position arg + query = parse("SELECT regexp_replace('100-200', '(\\d+)', 'num', 42)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_date_add(session: AsyncSession): + """ + Test `date_add` + """ + # first arg: date + query = parse( + "SELECT date_add(CURRENT_DATE(), 42)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DateType() # type: ignore + # first arg: timestamp + query = parse( + "SELECT date_add(CURRENT_TIMESTAMP(), 42)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DateType() # type: ignore + # first arg: string + query = parse( + "SELECT date_add('2020-01-01', 42)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DateType() # type: ignore + + +@pytest.mark.asyncio +async def test_replace(session: AsyncSession): + """ + Test `replace` + """ + # two arguments + query = parse( + "SELECT replace('%SystemDrive%\\Users\\John', '%SystemDrive%\\Users.*')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + # three arguments + query = parse( + "SELECT replace('%SystemDrive%\\Users\\John', '%SystemDrive%\\Users.*', 'C:\\Users\\John')", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_row_number(session: AsyncSession): + """ + Test `row_number` + """ + query = parse( + "SELECT row_number() OVER (PARTITION BY col ORDER BY col) FROM (SELECT (1), (2) AS col)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_round(session: AsyncSession): + """ + Test `round` + """ + query = parse( + "SELECT round(2.5, 0)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + query = parse( + "SELECT round(2.5)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_sequence(session: AsyncSession): + """ + Test `sequence` + """ + query = parse("SELECT sequence(1, 10)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + + query = parse("SELECT sequence(1, 10, 2)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + + query = parse( + "SELECT sequence(to_date('2018-01-01'), to_date('2018-03-01'), interval 1 month)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.DateType()) # type: ignore + + query = parse( + "SELECT sequence(to_timestamp('2018-01-01 00:00:00'), " + "to_timestamp('2018-01-01 12:00:00'), interval 1 hour)", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType( # type: ignore + element_type=ct.TimestampType(), + ) + + +@pytest.mark.asyncio +async def test_size(session: AsyncSession): + """ + Test the `size` Spark function + """ + query = parse("SELECT size(array('b', 'd', 'c', 'a'))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + query = parse("SELECT size(map('a', 1, 'b', 2))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_split(session: AsyncSession): + """ + Test the `split` Spark function + """ + query = parse("SELECT split('oneAtwoBthreeC', '[ABC]')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.ListType(element_type=ct.StringType()) # type: ignore + + +@pytest.mark.asyncio +async def test_strpos(session: AsyncSession): + """ + Test `strpos` + """ + query = parse("SELECT strpos('abcde', 'cde'), strpos('abcde', 'cde', 4)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + assert query.select.projection[1].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_substring(session: AsyncSession): + """ + Test `substring` + """ + query = parse("SELECT substring('Spark SQL', 5), substring('Spark SQL', 5, 1)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + query = parse("SELECT substr('Spark SQL', 5), substring('Spark SQL', 5, 1)") + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + assert query.select.projection[1].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_sum() -> None: + """ + Test ``sum`` function. + """ + assert ( + Sum.infer_type(ast.Column(ast.Name("x"), _type=IntegerType())) == BigIntType() + ) + assert Sum.infer_type(ast.Column(ast.Name("x"), _type=FloatType())) == DoubleType() + assert Sum.infer_type( + ast.Column(ast.Name("x"), _type=DecimalType(8, 6)), + ) == DecimalType(18, 6) + + +@pytest.mark.asyncio +async def test_transform(session: AsyncSession): + """ + Test the `transform` Spark function + """ + query = parse( + """ + SELECT transform(array(1, 2, 3), x -> x + 1) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + + query = parse( + """ + SELECT transform(array(1, 2, 3), (x, i) -> x + i) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.ListType(element_type=ct.IntegerType()) # type: ignore + + +@pytest.mark.asyncio +async def test_transform_keys(session: AsyncSession): + """ + Test the `transform_keys` Spark function + """ + query = parse( + """ + SELECT transform_keys(map_from_arrays(array(1, 2, 3), array(1, 2, 3)), (k, v) -> k + 1.1); + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.MapType( # type: ignore + key_type=ct.FloatType(), + value_type=ct.IntegerType(), + ) + + query = parse( + """ + SELECT transform_keys(map_from_arrays(array('1', '2', '3'), array('1', '2', '3')), (k, v) -> k + v) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.MapType( # type: ignore + key_type=ct.StringType(), + value_type=ct.StringType(), + ) + + +@pytest.mark.asyncio +async def test_transform_values(session: AsyncSession): + """ + Test the `transform_values` Spark function + """ + query = parse( + """ + SELECT transform_values(map_from_arrays(array(1, 2, 3), array(1, 2, 3)), (k, v) -> v*1.0) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.MapType( # type: ignore + key_type=ct.IntegerType(), + value_type=ct.FloatType(), + ) + + query = parse( + """ + SELECT transform_values(map_from_arrays(array('1', '2', '3'), array('1', '2', '3')), (k, v) -> k + v) + """, + ) + ctx = ast.CompileContext(session=session, exception=DJException()) + await query.compile(ctx) + assert query.select.projection[0].type == ct.MapType( # type: ignore + key_type=ct.StringType(), + value_type=ct.StringType(), + ) + + +@pytest.mark.asyncio +async def test_to_date() -> None: + """ + Test ``to_date`` function. + """ + assert ( + ToDate.infer_type(ast.Column(ast.Name("x"), _type=StringType())) == DateType() + ) + + +@pytest.mark.asyncio +async def test_trim(session: AsyncSession): + """ + Test `trim` + """ + query = parse("SELECT trim(' lmgi ')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_unhex_func(session: AsyncSession): + """ + Test the `unhex` function + """ + query = parse("SELECT unhex('4D'), unhex('7953514C')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BinaryType() # type: ignore + assert query.select.projection[1].type == ct.BinaryType() # type: ignore + + +@pytest.mark.asyncio +async def test_upper(session: AsyncSession): + """ + Test `upper` + """ + query = parse("SELECT upper('abcde')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.StringType() # type: ignore + + +@pytest.mark.asyncio +async def test_var_samp(session: AsyncSession): + """ + Test `var_samp` + """ + query = parse("SELECT var_samp(col) FROM (select (1), (2) AS col)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.DoubleType() # type: ignore + + +@pytest.mark.asyncio +async def test_unix_date(session: AsyncSession): + """ + Test the `unix_date` function + """ + query = parse("SELECT unix_date(DATE('1970-01-02'))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.IntegerType() # type: ignore + + +@pytest.mark.asyncio +async def test_unix_micros(session: AsyncSession): + """ + Test the `unix_micros` function + """ + query = parse("SELECT unix_micros(cast('1970-01-01 00:00:01Z' as timestamp))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BigIntType() # type: ignore + + +@pytest.mark.asyncio +async def test_unix_millis(session: AsyncSession): + """ + Test the `unix_millis` function + """ + query = parse("SELECT unix_millis(cast('1970-01-01 00:00:01Z' as timestamp))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BigIntType() # type: ignore + + +@pytest.mark.asyncio +async def test_unix_seconds(session: AsyncSession): + """ + Test the `unix_seconds` function + """ + query = parse("SELECT unix_seconds(cast('1970-01-01 00:00:01Z' as timestamp))") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BigIntType() # type: ignore + + +@pytest.mark.asyncio +async def test_unix_timestamp(session: AsyncSession): + """ + Test the `unix_timestamp` function + """ + query = parse("SELECT unix_timestamp(), unix_timestamp('2016-04-08', 'yyyy-MM-dd')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.BigIntType() # type: ignore + assert query.select.projection[1].type == ct.BigIntType() # type: ignore + + +@pytest.mark.asyncio +async def test_timestamp(session: AsyncSession): + """ + Test the `timestamp` function + """ + query = parse("SELECT timestamp('2016-04-08')") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.TimestampType() # type: ignore + + +@pytest.mark.asyncio +async def test_timestamp_micros(session: AsyncSession): + """ + Test the `timestamp_micros` function + """ + query = parse("SELECT timestamp_micros(1230219000123123)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.TimestampType() # type: ignore + + +@pytest.mark.asyncio +async def test_timestamp_millis(session: AsyncSession): + """ + Test the `timestamp_millis` function + """ + query = parse("SELECT timestamp_millis(1230219000123)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.TimestampType() # type: ignore + + +@pytest.mark.asyncio +async def test_timestamp_seconds(session: AsyncSession): + """ + Test the `timestamp_seconds` function + """ + query = parse("SELECT timestamp_seconds(1230219000.123)") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert query.select.projection[0].type == ct.TimestampType() # type: ignore diff --git a/datajunction-server/tests/sql/parsing/__init__.py b/datajunction-server/tests/sql/parsing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/tests/sql/parsing/backends/__init__.py b/datajunction-server/tests/sql/parsing/backends/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/tests/sql/parsing/backends/antlr4_test.py b/datajunction-server/tests/sql/parsing/backends/antlr4_test.py new file mode 100644 index 000000000..101448c52 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/backends/antlr4_test.py @@ -0,0 +1,130 @@ +""" +Tests for custom antlr4 parser +""" +# mypy: ignore-errors + +import pytest + +from datajunction_server.sql.parsing.backends.antlr4 import parse +from datajunction_server.sql.parsing.backends.exceptions import DJParseException + + +@pytest.mark.parametrize( + "query_string", + [ + """SELECT suit, key, value + FROM suites_and_ranks_arrays + LATERAL VIEW EXPLODE(rankmap) AS key, value + ORDER BY suit;""", + """SELECT suit, exploded_rank + FROM suites_and_ranks_arrays + LATERAL VIEW EXPLODE(rank) exploded_rank + ORDER BY suit;""", + """SELECT suit, exploded_rank, key, value + FROM suites_and_ranks_arrays + LATERAL VIEW EXPLODE(rank) AS exploded_rank + LATERAL VIEW EXPLODE(rankmap) AS key, value + ORDER BY suit;""", + ], +) +def test_antlr4_backend_lateral_view_explode(query_string): + """ + Test LATERAL VIEW EXPLODE queries + """ + parse(query_string) + + +@pytest.mark.parametrize( + "query_string", + [ + """Select suit, exploded_rank, exploded_rank2 + from suites_and_ranks_arrays + CROSS JOIN UNNEST(rank) as t(exploded_rank) + ORDER BY suit;""", + """Select suit, exploded_rank, exploded_rank2 + from suites_and_ranks_arrays + CROSS JOIN UNNEST(rank, rank) as t(exploded_rank, exploded_rank2) + ORDER BY suit;""", + """Select suit, exploded_rank, exploded_rank2 + from suites_and_ranks_arrays + CROSS JOIN UNNEST(rank) as t(exploded_rank) + CROSS JOIN UNNEST(rank) as t(exploded_rank2)""", + """Select suit, key, value + from suites_and_ranks_arrays + CROSS JOIN UNNEST(rankmap) as t(key, value) + ORDER BY suit;""", + """Select suit, exploded_rank, k, value + from suites_and_ranks_arrays + CROSS JOIN UNNEST(rank, rankmap) as t(exploded_rank, key, value) + ORDER BY suit;""", + ], +) +def test_antlr4_backend_cross_join_unnest(query_string): + """ + Test CROSS JOIN UNNEST queries + """ + parse(query_string) + + +def test_antlr4_backend_predicate_like(): + """ + Test LIKE predicate + """ + query = parse("SELECT * FROM person WHERE name LIKE '%$_%';") + assert "LIKE '%$_%'" in str(query) + + +def test_antlr4_backend_predicate_ilike(): + """ + Test ILIKE predicate + """ + query = parse("SELECT * FROM person WHERE name ILIKE '%foo%';") + assert "ILIKE '%foo%'" in str(query) + + +def test_antlr4_backend_predicate_rlike(): + """ + Test RLIKE predicate + """ + query = parse("SELECT * FROM person WHERE name RLIKE 'M+';") + assert "RLIKE 'M+'" in str(query) + + +def test_antlr4_backend_predicate_is_distinct_from(): + """ + Test IS DISTINCT FROM predicate + """ + query = parse("SELECT * FROM person WHERE name IS DISTINCT FROM 'Bob'") + assert "IS DISTINCT FROM 'Bob'" in str(query) + + +def test_antlr4_backend_trim(): + """ + Test trim + """ + query = parse("SELECT TRIM(BOTH FROM ' SparkSQL ');") + assert "TRIM( BOTH FROM ' SparkSQL ')" in str(query) + query = parse("SELECT TRIM(LEADING FROM ' SparkSQL ');") + assert "TRIM( LEADING FROM ' SparkSQL ')" in str(query) + query = parse("SELECT TRIM(TRAILING FROM ' SparkSQL ');") + assert "TRIM( TRAILING FROM ' SparkSQL ')" in str(query) + query = parse("SELECT TRIM(' SparkSQL ');") + assert "TRIM(' SparkSQL ')" in str(query) + + +def test_antlr4_lambda_function(): + """ + Test a lambda function using `->` + """ + query = parse("SELECT FOO('a', 'b', c -> d) AS e;") + assert "FOO('a', 'b', c -> d) AS e" in str(query) + query = parse("SELECT FOO('a', 'b', (c, c2, c3) -> d) AS e;") + assert "FOO('a', 'b', (c, c2, c3) -> d) AS e" in str(query) + + +def test_antlr4_parse_error(): + """ + Test LATERAL VIEW EXPLODE queries + """ + with pytest.raises(DJParseException): + parse("SELECT ** FROM 1_#**") diff --git a/datajunction-server/tests/sql/parsing/backends/types_test.py b/datajunction-server/tests/sql/parsing/backends/types_test.py new file mode 100644 index 000000000..895209402 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/backends/types_test.py @@ -0,0 +1,38 @@ +""" +Tests for types +""" + +import datajunction_server.sql.parsing.types as ct +from datajunction_server.sql.parsing import ast + + +def test_types_compatible(): + """ + Checks whether type compatibility checks work + """ + assert ct.IntegerType().is_compatible(ct.IntegerType()) + assert ct.IntegerType().is_compatible(ct.BigIntType()) + assert ct.BigIntType().is_compatible(ct.IntegerType()) + assert ct.TinyIntType().is_compatible(ct.BigIntType()) + assert ct.BigIntType().is_compatible(ct.BigIntType()) + assert ct.BigIntType().is_compatible(ct.IntegerType()) + assert ct.FloatType().is_compatible(ct.DoubleType()) + assert ct.StringType().is_compatible(ct.VarcharType()) + assert ct.DateType().is_compatible(ct.TimeType()) + + assert not ct.StringType().is_compatible(ct.IntegerType()) + assert not ct.StringType().is_compatible(ct.BooleanType()) + assert not ct.StringType().is_compatible(ct.BinaryType()) + assert not ct.StringType().is_compatible(ct.BigIntType()) + assert not ct.StringType().is_compatible(ct.DateType()) + + +def test_varchar_in_ast(): + """ + Test that varchar types support length as a parameter. + """ + cast_expr = ast.Cast( + data_type=ast.ColumnType("varchar(10)"), + expression=ast.Column(ast.Name("abc")), + ) + assert str(cast_expr) == "CAST(abc AS VARCHAR(10))" diff --git a/datajunction-server/tests/sql/parsing/queries/__init__.py b/datajunction-server/tests/sql/parsing/queries/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query1.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query1.sql new file mode 100644 index 000000000..8808e3ce6 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query1.sql @@ -0,0 +1,23 @@ +-- start query 1 in stream 0 using template query1.tpl +WITH customer_total_return + AS (SELECT sr_customer_sk AS ctr_customer_sk, + sr_store_sk AS ctr_store_sk, + Sum(sr_return_amt) AS ctr_total_return + FROM store_returns, + date_dim + WHERE sr_returned_date_sk = d_date_sk + AND d_year = 2001 + GROUP BY sr_customer_sk, + sr_store_sk) +SELECT c_customer_id +FROM customer_total_return ctr1, + store, + customer +WHERE ctr1.ctr_total_return > (SELECT Avg(ctr_total_return) * 1.2 + FROM customer_total_return ctr2 + WHERE ctr1.ctr_store_sk = ctr2.ctr_store_sk) + AND s_store_sk = ctr1.ctr_store_sk + AND s_state = 'TN' + AND ctr1.ctr_customer_sk = c_customer_sk +ORDER BY c_customer_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query10.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query10.sql new file mode 100644 index 000000000..0cb0ba050 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query10.sql @@ -0,0 +1,62 @@ +-- start query 10 in stream 0 using template query10.tpl +SELECT cd_gender, + cd_marital_status, + cd_education_status, + Count(*) cnt1, + cd_purchase_estimate, + Count(*) cnt2, + cd_credit_rating, + Count(*) cnt3, + cd_dep_count, + Count(*) cnt4, + cd_dep_employed_count, + Count(*) cnt5, + cd_dep_college_count, + Count(*) cnt6 +FROM customer c, + customer_address ca, + customer_demographics +WHERE c.c_current_addr_sk = ca.ca_address_sk + AND ca_county IN ( 'Lycoming County', 'Sheridan County', + 'Kandiyohi County', + 'Pike County', + 'Greene County' ) + AND cd_demo_sk = c.c_current_cdemo_sk + AND EXISTS (SELECT * + FROM store_sales, + date_dim + WHERE c.c_customer_sk = ss_customer_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 2002 + AND d_moy BETWEEN 4 AND 4 + 3) + AND ( EXISTS (SELECT * + FROM web_sales, + date_dim + WHERE c.c_customer_sk = ws_bill_customer_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 2002 + AND d_moy BETWEEN 4 AND 4 + 3) + OR EXISTS (SELECT * + FROM catalog_sales, + date_dim + WHERE c.c_customer_sk = cs_ship_customer_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 2002 + AND d_moy BETWEEN 4 AND 4 + 3) ) +GROUP BY cd_gender, + cd_marital_status, + cd_education_status, + cd_purchase_estimate, + cd_credit_rating, + cd_dep_count, + cd_dep_employed_count, + cd_dep_college_count +ORDER BY cd_gender, + cd_marital_status, + cd_education_status, + cd_purchase_estimate, + cd_credit_rating, + cd_dep_count, + cd_dep_employed_count, + cd_dep_college_count +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query11.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query11.sql new file mode 100644 index 000000000..cf24b9b74 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query11.sql @@ -0,0 +1,97 @@ +-- start query 11 in stream 0 using template query11.tpl +WITH year_total + AS (SELECT c_customer_id customer_id, + c_first_name customer_first_name + , + c_last_name + customer_last_name, + c_preferred_cust_flag + customer_preferred_cust_flag + , + c_birth_country + customer_birth_country, + c_login customer_login, + c_email_address + customer_email_address, + d_year dyear, + Sum(ss_ext_list_price - ss_ext_discount_amt) year_total, + 's' sale_type + FROM customer, + store_sales, + date_dim + WHERE c_customer_sk = ss_customer_sk + AND ss_sold_date_sk = d_date_sk + GROUP BY c_customer_id, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_country, + c_login, + c_email_address, + d_year + UNION ALL + SELECT c_customer_id customer_id, + c_first_name customer_first_name + , + c_last_name + customer_last_name, + c_preferred_cust_flag + customer_preferred_cust_flag + , + c_birth_country + customer_birth_country, + c_login customer_login, + c_email_address + customer_email_address, + d_year dyear, + Sum(ws_ext_list_price - ws_ext_discount_amt) year_total, + 'w' sale_type + FROM customer, + web_sales, + date_dim + WHERE c_customer_sk = ws_bill_customer_sk + AND ws_sold_date_sk = d_date_sk + GROUP BY c_customer_id, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_country, + c_login, + c_email_address, + d_year) +SELECT t_s_secyear.customer_id, + t_s_secyear.customer_first_name, + t_s_secyear.customer_last_name, + t_s_secyear.customer_birth_country +FROM year_total t_s_firstyear, + year_total t_s_secyear, + year_total t_w_firstyear, + year_total t_w_secyear +WHERE t_s_secyear.customer_id = t_s_firstyear.customer_id + AND t_s_firstyear.customer_id = t_w_secyear.customer_id + AND t_s_firstyear.customer_id = t_w_firstyear.customer_id + AND t_s_firstyear.sale_type = 's' + AND t_w_firstyear.sale_type = 'w' + AND t_s_secyear.sale_type = 's' + AND t_w_secyear.sale_type = 'w' + AND t_s_firstyear.dyear = 2001 + AND t_s_secyear.dyear = 2001 + 1 + AND t_w_firstyear.dyear = 2001 + AND t_w_secyear.dyear = 2001 + 1 + AND t_s_firstyear.year_total > 0 + AND t_w_firstyear.year_total > 0 + AND CASE + WHEN t_w_firstyear.year_total > 0 THEN t_w_secyear.year_total / + t_w_firstyear.year_total + ELSE 0.0 + END > CASE + WHEN t_s_firstyear.year_total > 0 THEN + t_s_secyear.year_total / + t_s_firstyear.year_total + ELSE 0.0 + END +ORDER BY t_s_secyear.customer_id, + t_s_secyear.customer_first_name, + t_s_secyear.customer_last_name, + t_s_secyear.customer_birth_country +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query12.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query12.sql new file mode 100644 index 000000000..5f6d721c4 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query12.sql @@ -0,0 +1,30 @@ +-- start query 12 in stream 0 using template query12.tpl +SELECT + i_item_id , + i_item_desc , + i_category , + i_class , + i_current_price , + Sum(ws_ext_sales_price) AS itemrevenue , + Sum(ws_ext_sales_price)*100/Sum(Sum(ws_ext_sales_price)) OVER (partition BY i_class) AS revenueratio +FROM web_sales , + item , + date_dim +WHERE ws_item_sk = i_item_sk +AND i_category IN ('Home', + 'Men', + 'Women') +AND ws_sold_date_sk = d_date_sk +AND d_date BETWEEN Cast('2000-05-11' AS DATE) AND ( + Cast('2000-05-11' AS DATE) + INTERVAL '30' day) +GROUP BY i_item_id , + i_item_desc , + i_category , + i_class , + i_current_price +ORDER BY i_category , + i_class , + i_item_id , + i_item_desc , + revenueratio +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query13.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query13.sql new file mode 100644 index 000000000..2bec54b95 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query13.sql @@ -0,0 +1,44 @@ +-- start query 13 in stream 0 using template query13.tpl +SELECT Avg(ss_quantity), + Avg(ss_ext_sales_price), + Avg(ss_ext_wholesale_cost), + Sum(ss_ext_wholesale_cost) +FROM store_sales, + store, + customer_demographics, + household_demographics, + customer_address, + date_dim +WHERE s_store_sk = ss_store_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 2001 + AND ( ( ss_hdemo_sk = hd_demo_sk + AND cd_demo_sk = ss_cdemo_sk + AND cd_marital_status = 'U' + AND cd_education_status = 'Advanced Degree' + AND ss_sales_price BETWEEN 100.00 AND 150.00 + AND hd_dep_count = 3 ) + OR ( ss_hdemo_sk = hd_demo_sk + AND cd_demo_sk = ss_cdemo_sk + AND cd_marital_status = 'M' + AND cd_education_status = 'Primary' + AND ss_sales_price BETWEEN 50.00 AND 100.00 + AND hd_dep_count = 1 ) + OR ( ss_hdemo_sk = hd_demo_sk + AND cd_demo_sk = ss_cdemo_sk + AND cd_marital_status = 'D' + AND cd_education_status = 'Secondary' + AND ss_sales_price BETWEEN 150.00 AND 200.00 + AND hd_dep_count = 1 ) ) + AND ( ( ss_addr_sk = ca_address_sk + AND ca_country = 'United States' + AND ca_state IN ( 'AZ', 'NE', 'IA' ) + AND ss_net_profit BETWEEN 100 AND 200 ) + OR ( ss_addr_sk = ca_address_sk + AND ca_country = 'United States' + AND ca_state IN ( 'MS', 'CA', 'NV' ) + AND ss_net_profit BETWEEN 150 AND 300 ) + OR ( ss_addr_sk = ca_address_sk + AND ca_country = 'United States' + AND ca_state IN ( 'GA', 'TX', 'NJ' ) + AND ss_net_profit BETWEEN 50 AND 250 ) ); diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query14.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query14.sql new file mode 100644 index 000000000..7a7572bba --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query14.sql @@ -0,0 +1,245 @@ +-- start query 14 in stream 0 using template query14.tpl +WITH cross_items + AS (SELECT i_item_sk ss_item_sk + FROM item, + (SELECT iss.i_brand_id brand_id, + iss.i_class_id class_id, + iss.i_category_id category_id + FROM store_sales, + item iss, + date_dim d1 + WHERE ss_item_sk = iss.i_item_sk + AND ss_sold_date_sk = d1.d_date_sk + AND d1.d_year BETWEEN 1999 AND 1999 + 2 + INTERSECT + SELECT ics.i_brand_id, + ics.i_class_id, + ics.i_category_id + FROM catalog_sales, + item ics, + date_dim d2 + WHERE cs_item_sk = ics.i_item_sk + AND cs_sold_date_sk = d2.d_date_sk + AND d2.d_year BETWEEN 1999 AND 1999 + 2 + INTERSECT + SELECT iws.i_brand_id, + iws.i_class_id, + iws.i_category_id + FROM web_sales, + item iws, + date_dim d3 + WHERE ws_item_sk = iws.i_item_sk + AND ws_sold_date_sk = d3.d_date_sk + AND d3.d_year BETWEEN 1999 AND 1999 + 2) + WHERE i_brand_id = brand_id + AND i_class_id = class_id + AND i_category_id = category_id), + avg_sales + AS (SELECT Avg(quantity * list_price) average_sales + FROM (SELECT ss_quantity quantity, + ss_list_price list_price + FROM store_sales, + date_dim + WHERE ss_sold_date_sk = d_date_sk + AND d_year BETWEEN 1999 AND 1999 + 2 + UNION ALL + SELECT cs_quantity quantity, + cs_list_price list_price + FROM catalog_sales, + date_dim + WHERE cs_sold_date_sk = d_date_sk + AND d_year BETWEEN 1999 AND 1999 + 2 + UNION ALL + SELECT ws_quantity quantity, + ws_list_price list_price + FROM web_sales, + date_dim + WHERE ws_sold_date_sk = d_date_sk + AND d_year BETWEEN 1999 AND 1999 + 2) x) +SELECT channel, + i_brand_id, + i_class_id, + i_category_id, + Sum(sales), + Sum(number_sales) +FROM (SELECT 'store' channel, + i_brand_id, + i_class_id, + i_category_id, + Sum(ss_quantity * ss_list_price) sales, + Count(*) number_sales + FROM store_sales, + item, + date_dim + WHERE ss_item_sk IN (SELECT ss_item_sk + FROM cross_items) + AND ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 1999 + 2 + AND d_moy = 11 + GROUP BY i_brand_id, + i_class_id, + i_category_id + HAVING Sum(ss_quantity * ss_list_price) > (SELECT average_sales + FROM avg_sales) + UNION ALL + SELECT 'catalog' channel, + i_brand_id, + i_class_id, + i_category_id, + Sum(cs_quantity * cs_list_price) sales, + Count(*) number_sales + FROM catalog_sales, + item, + date_dim + WHERE cs_item_sk IN (SELECT ss_item_sk + FROM cross_items) + AND cs_item_sk = i_item_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 1999 + 2 + AND d_moy = 11 + GROUP BY i_brand_id, + i_class_id, + i_category_id + HAVING Sum(cs_quantity * cs_list_price) > (SELECT average_sales + FROM avg_sales) + UNION ALL + SELECT 'web' channel, + i_brand_id, + i_class_id, + i_category_id, + Sum(ws_quantity * ws_list_price) sales, + Count(*) number_sales + FROM web_sales, + item, + date_dim + WHERE ws_item_sk IN (SELECT ss_item_sk + FROM cross_items) + AND ws_item_sk = i_item_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 1999 + 2 + AND d_moy = 11 + GROUP BY i_brand_id, + i_class_id, + i_category_id + HAVING Sum(ws_quantity * ws_list_price) > (SELECT average_sales + FROM avg_sales)) y +GROUP BY rollup ( channel, i_brand_id, i_class_id, i_category_id ) +ORDER BY channel, + i_brand_id, + i_class_id, + i_category_id +LIMIT 100; + +WITH cross_items + AS (SELECT i_item_sk ss_item_sk + FROM item, + (SELECT iss.i_brand_id brand_id, + iss.i_class_id class_id, + iss.i_category_id category_id + FROM store_sales, + item iss, + date_dim d1 + WHERE ss_item_sk = iss.i_item_sk + AND ss_sold_date_sk = d1.d_date_sk + AND d1.d_year BETWEEN 1999 AND 1999 + 2 + INTERSECT + SELECT ics.i_brand_id, + ics.i_class_id, + ics.i_category_id + FROM catalog_sales, + item ics, + date_dim d2 + WHERE cs_item_sk = ics.i_item_sk + AND cs_sold_date_sk = d2.d_date_sk + AND d2.d_year BETWEEN 1999 AND 1999 + 2 + INTERSECT + SELECT iws.i_brand_id, + iws.i_class_id, + iws.i_category_id + FROM web_sales, + item iws, + date_dim d3 + WHERE ws_item_sk = iws.i_item_sk + AND ws_sold_date_sk = d3.d_date_sk + AND d3.d_year BETWEEN 1999 AND 1999 + 2) x + WHERE i_brand_id = brand_id + AND i_class_id = class_id + AND i_category_id = category_id), + avg_sales + AS (SELECT Avg(quantity * list_price) average_sales + FROM (SELECT ss_quantity quantity, + ss_list_price list_price + FROM store_sales, + date_dim + WHERE ss_sold_date_sk = d_date_sk + AND d_year BETWEEN 1999 AND 1999 + 2 + UNION ALL + SELECT cs_quantity quantity, + cs_list_price list_price + FROM catalog_sales, + date_dim + WHERE cs_sold_date_sk = d_date_sk + AND d_year BETWEEN 1999 AND 1999 + 2 + UNION ALL + SELECT ws_quantity quantity, + ws_list_price list_price + FROM web_sales, + date_dim + WHERE ws_sold_date_sk = d_date_sk + AND d_year BETWEEN 1999 AND 1999 + 2) x) +SELECT * +FROM (SELECT 'store' channel, + i_brand_id, + i_class_id, + i_category_id, + Sum(ss_quantity * ss_list_price) sales, + Count(*) number_sales + FROM store_sales, + item, + date_dim + WHERE ss_item_sk IN (SELECT ss_item_sk + FROM cross_items) + AND ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND d_week_seq = (SELECT d_week_seq + FROM date_dim + WHERE d_year = 1999 + 1 + AND d_moy = 12 + AND d_dom = 25) + GROUP BY i_brand_id, + i_class_id, + i_category_id + HAVING Sum(ss_quantity * ss_list_price) > (SELECT average_sales + FROM avg_sales)) this_year, + (SELECT 'store' channel, + i_brand_id, + i_class_id, + i_category_id, + Sum(ss_quantity * ss_list_price) sales, + Count(*) number_sales + FROM store_sales, + item, + date_dim + WHERE ss_item_sk IN (SELECT ss_item_sk + FROM cross_items) + AND ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND d_week_seq = (SELECT d_week_seq + FROM date_dim + WHERE d_year = 1999 + AND d_moy = 12 + AND d_dom = 25) + GROUP BY i_brand_id, + i_class_id, + i_category_id + HAVING Sum(ss_quantity * ss_list_price) > (SELECT average_sales + FROM avg_sales)) last_year +WHERE this_year.i_brand_id = last_year.i_brand_id + AND this_year.i_class_id = last_year.i_class_id + AND this_year.i_category_id = last_year.i_category_id +ORDER BY this_year.channel, + this_year.i_brand_id, + this_year.i_class_id, + this_year.i_category_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query15.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query15.sql new file mode 100644 index 000000000..207ddd719 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query15.sql @@ -0,0 +1,20 @@ +-- start query 15 in stream 0 using template query15.tpl +SELECT ca_zip, + Sum(cs_sales_price) +FROM catalog_sales, + customer, + customer_address, + date_dim +WHERE cs_bill_customer_sk = c_customer_sk + AND c_current_addr_sk = ca_address_sk + AND ( Substr(ca_zip, 1, 5) IN ( '85669', '86197', '88274', '83405', + '86475', '85392', '85460', '80348', + '81792' ) + OR ca_state IN ( 'CA', 'WA', 'GA' ) + OR cs_sales_price > 500 ) + AND cs_sold_date_sk = d_date_sk + AND d_qoy = 1 + AND d_year = 1998 +GROUP BY ca_zip +ORDER BY ca_zip +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query16.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query16.sql new file mode 100644 index 000000000..15149bfd6 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query16.sql @@ -0,0 +1,33 @@ +-- start query 16 in stream 0 using template query16.tpl +SELECT + Count(DISTINCT cs_order_number) AS "order count" , + Sum(cs_ext_ship_cost) AS "total shipping cost" , + Sum(cs_net_profit) AS "total net profit" +FROM catalog_sales cs1 , + date_dim , + customer_address , + call_center +WHERE d_date BETWEEN '2002-3-01' AND ( + Cast('2002-3-01' AS DATE) + INTERVAL '60' day) +AND cs1.cs_ship_date_sk = d_date_sk +AND cs1.cs_ship_addr_sk = ca_address_sk +AND ca_state = 'IA' +AND cs1.cs_call_center_sk = cc_call_center_sk +AND cc_county IN ('Williamson County', + 'Williamson County', + 'Williamson County', + 'Williamson County', + 'Williamson County' ) +AND EXISTS + ( + SELECT * + FROM catalog_sales cs2 + WHERE cs1.cs_order_number = cs2.cs_order_number + AND cs1.cs_warehouse_sk <> cs2.cs_warehouse_sk) +AND NOT EXISTS + ( + SELECT * + FROM catalog_returns cr1 + WHERE cs1.cs_order_number = cr1.cr_order_number) +ORDER BY count(DISTINCT cs_order_number) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query17.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query17.sql new file mode 100644 index 000000000..da916d4a5 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query17.sql @@ -0,0 +1,56 @@ +-- start query 17 in stream 0 using template query17.tpl +SELECT i_item_id, + i_item_desc, + s_state, + Count(ss_quantity) AS + store_sales_quantitycount, + Avg(ss_quantity) AS + store_sales_quantityave, + Stddev_samp(ss_quantity) AS + store_sales_quantitystdev, + Stddev_samp(ss_quantity) / Avg(ss_quantity) AS + store_sales_quantitycov, + Count(sr_return_quantity) AS + store_returns_quantitycount, + Avg(sr_return_quantity) AS + store_returns_quantityave, + Stddev_samp(sr_return_quantity) AS + store_returns_quantitystdev, + Stddev_samp(sr_return_quantity) / Avg(sr_return_quantity) AS + store_returns_quantitycov, + Count(cs_quantity) AS + catalog_sales_quantitycount, + Avg(cs_quantity) AS + catalog_sales_quantityave, + Stddev_samp(cs_quantity) / Avg(cs_quantity) AS + catalog_sales_quantitystdev, + Stddev_samp(cs_quantity) / Avg(cs_quantity) AS + catalog_sales_quantitycov +FROM store_sales, + store_returns, + catalog_sales, + date_dim d1, + date_dim d2, + date_dim d3, + store, + item +WHERE d1.d_quarter_name = '1999Q1' + AND d1.d_date_sk = ss_sold_date_sk + AND i_item_sk = ss_item_sk + AND s_store_sk = ss_store_sk + AND ss_customer_sk = sr_customer_sk + AND ss_item_sk = sr_item_sk + AND ss_ticket_number = sr_ticket_number + AND sr_returned_date_sk = d2.d_date_sk + AND d2.d_quarter_name IN ( '1999Q1', '1999Q2', '1999Q3' ) + AND sr_customer_sk = cs_bill_customer_sk + AND sr_item_sk = cs_item_sk + AND cs_sold_date_sk = d3.d_date_sk + AND d3.d_quarter_name IN ( '1999Q1', '1999Q2', '1999Q3' ) +GROUP BY i_item_id, + i_item_desc, + s_state +ORDER BY i_item_id, + i_item_desc, + s_state +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query18.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query18.sql new file mode 100644 index 000000000..620cd2eb3 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query18.sql @@ -0,0 +1,38 @@ +-- start query 18 in stream 0 using template query18.tpl +SELECT i_item_id, + ca_country, + ca_state, + ca_county, + Avg(Cast(cs_quantity AS NUMERIC(12, 2))) agg1, + Avg(Cast(cs_list_price AS NUMERIC(12, 2))) agg2, + Avg(Cast(cs_coupon_amt AS NUMERIC(12, 2))) agg3, + Avg(Cast(cs_sales_price AS NUMERIC(12, 2))) agg4, + Avg(Cast(cs_net_profit AS NUMERIC(12, 2))) agg5, + Avg(Cast(c_birth_year AS NUMERIC(12, 2))) agg6, + Avg(Cast(cd1.cd_dep_count AS NUMERIC(12, 2))) agg7 +FROM catalog_sales, + customer_demographics cd1, + customer_demographics cd2, + customer, + customer_address, + date_dim, + item +WHERE cs_sold_date_sk = d_date_sk + AND cs_item_sk = i_item_sk + AND cs_bill_cdemo_sk = cd1.cd_demo_sk + AND cs_bill_customer_sk = c_customer_sk + AND cd1.cd_gender = 'F' + AND cd1.cd_education_status = 'Secondary' + AND c_current_cdemo_sk = cd2.cd_demo_sk + AND c_current_addr_sk = ca_address_sk + AND c_birth_month IN ( 8, 4, 2, 5, + 11, 9 ) + AND d_year = 2001 + AND ca_state IN ( 'KS', 'IA', 'AL', 'UT', + 'VA', 'NC', 'TX' ) +GROUP BY rollup ( i_item_id, ca_country, ca_state, ca_county ) +ORDER BY ca_country, + ca_state, + ca_county, + i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query19.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query19.sql new file mode 100644 index 000000000..c3039b2fd --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query19.sql @@ -0,0 +1,31 @@ +-- start query 19 in stream 0 using template query19.tpl +SELECT i_brand_id brand_id, + i_brand brand, + i_manufact_id, + i_manufact, + Sum(ss_ext_sales_price) ext_price +FROM date_dim, + store_sales, + item, + customer, + customer_address, + store +WHERE d_date_sk = ss_sold_date_sk + AND ss_item_sk = i_item_sk + AND i_manager_id = 38 + AND d_moy = 12 + AND d_year = 1998 + AND ss_customer_sk = c_customer_sk + AND c_current_addr_sk = ca_address_sk + AND Substr(ca_zip, 1, 5) <> Substr(s_zip, 1, 5) + AND ss_store_sk = s_store_sk +GROUP BY i_brand, + i_brand_id, + i_manufact_id, + i_manufact +ORDER BY ext_price DESC, + i_brand, + i_brand_id, + i_manufact_id, + i_manufact +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query2.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query2.sql new file mode 100644 index 000000000..c85af4246 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query2.sql @@ -0,0 +1,79 @@ +-- start query 2 in stream 0 using template query2.tpl +WITH wscs + AS (SELECT sold_date_sk, + sales_price + FROM (SELECT ws_sold_date_sk sold_date_sk, + ws_ext_sales_price sales_price + FROM web_sales) + UNION ALL + (SELECT cs_sold_date_sk sold_date_sk, + cs_ext_sales_price sales_price + FROM catalog_sales)), + wswscs + AS (SELECT d_week_seq, + Sum(CASE + WHEN ( d_day_name = 'Sunday' ) THEN sales_price + ELSE NULL + END) sun_sales, + Sum(CASE + WHEN ( d_day_name = 'Monday' ) THEN sales_price + ELSE NULL + END) mon_sales, + Sum(CASE + WHEN ( d_day_name = 'Tuesday' ) THEN sales_price + ELSE NULL + END) tue_sales, + Sum(CASE + WHEN ( d_day_name = 'Wednesday' ) THEN sales_price + ELSE NULL + END) wed_sales, + Sum(CASE + WHEN ( d_day_name = 'Thursday' ) THEN sales_price + ELSE NULL + END) thu_sales, + Sum(CASE + WHEN ( d_day_name = 'Friday' ) THEN sales_price + ELSE NULL + END) fri_sales, + Sum(CASE + WHEN ( d_day_name = 'Saturday' ) THEN sales_price + ELSE NULL + END) sat_sales + FROM wscs, + date_dim + WHERE d_date_sk = sold_date_sk + GROUP BY d_week_seq) +SELECT d_week_seq1, + Round(sun_sales1 / sun_sales2, 2), + Round(mon_sales1 / mon_sales2, 2), + Round(tue_sales1 / tue_sales2, 2), + Round(wed_sales1 / wed_sales2, 2), + Round(thu_sales1 / thu_sales2, 2), + Round(fri_sales1 / fri_sales2, 2), + Round(sat_sales1 / sat_sales2, 2) +FROM (SELECT wswscs.d_week_seq d_week_seq1, + sun_sales sun_sales1, + mon_sales mon_sales1, + tue_sales tue_sales1, + wed_sales wed_sales1, + thu_sales thu_sales1, + fri_sales fri_sales1, + sat_sales sat_sales1 + FROM wswscs, + date_dim + WHERE date_dim.d_week_seq = wswscs.d_week_seq + AND d_year = 1998) y, + (SELECT wswscs.d_week_seq d_week_seq2, + sun_sales sun_sales2, + mon_sales mon_sales2, + tue_sales tue_sales2, + wed_sales wed_sales2, + thu_sales thu_sales2, + fri_sales fri_sales2, + sat_sales sat_sales2 + FROM wswscs, + date_dim + WHERE date_dim.d_week_seq = wswscs.d_week_seq + AND d_year = 1998 + 1) z +WHERE d_week_seq1 = d_week_seq2 - 53 +ORDER BY d_week_seq1; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query20.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query20.sql new file mode 100644 index 000000000..3c73340ea --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query20.sql @@ -0,0 +1,30 @@ +-- start query 20 in stream 0 using template query20.tpl +SELECT + i_item_id , + i_item_desc , + i_category , + i_class , + i_current_price , + Sum(cs_ext_sales_price) AS itemrevenue , + Sum(cs_ext_sales_price)*100/Sum(Sum(cs_ext_sales_price)) OVER (partition BY i_class) AS revenueratio +FROM catalog_sales , + item , + date_dim +WHERE cs_item_sk = i_item_sk +AND i_category IN ('Children', + 'Women', + 'Electronics') +AND cs_sold_date_sk = d_date_sk +AND d_date BETWEEN Cast('2001-02-03' AS DATE) AND ( + Cast('2001-02-03' AS DATE) + INTERVAL '30' day) +GROUP BY i_item_id , + i_item_desc , + i_category , + i_class , + i_current_price +ORDER BY i_category , + i_class , + i_item_id , + i_item_desc , + revenueratio +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query21.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query21.sql new file mode 100644 index 000000000..1811226a8 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query21.sql @@ -0,0 +1,38 @@ +-- start query 21 in stream 0 using template query21.tpl +SELECT + * +FROM ( + SELECT w_warehouse_name , + i_item_id , + Sum( + CASE + WHEN ( + Cast(d_date AS DATE) < Cast ('2000-05-13' AS DATE)) THEN inv_quantity_on_hand + ELSE 0 + END) AS inv_before , + Sum( + CASE + WHEN ( + Cast(d_date AS DATE) >= Cast ('2000-05-13' AS DATE)) THEN inv_quantity_on_hand + ELSE 0 + END) AS inv_after + FROM inventory , + warehouse , + item , + date_dim + WHERE i_current_price BETWEEN 0.99 AND 1.49 + AND i_item_sk = inv_item_sk + AND inv_warehouse_sk = w_warehouse_sk + AND inv_date_sk = d_date_sk + AND d_date BETWEEN (Cast ('2000-05-13' AS DATE) - INTERVAL '30' day) AND ( + cast ('2000-05-13' AS date) + INTERVAL '30' day) + GROUP BY w_warehouse_name, + i_item_id) x +WHERE ( + CASE + WHEN inv_before > 0 THEN inv_after / inv_before + ELSE NULL + END) BETWEEN 2.0/3.0 AND 3.0/2.0 +ORDER BY w_warehouse_name , + i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query22.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query22.sql new file mode 100644 index 000000000..707fc7c85 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query22.sql @@ -0,0 +1,21 @@ +-- start query 22 in stream 0 using template query22.tpl +SELECT i_product_name, + i_brand, + i_class, + i_category, + Avg(inv_quantity_on_hand) qoh +FROM inventory, + date_dim, + item, + warehouse +WHERE inv_date_sk = d_date_sk + AND inv_item_sk = i_item_sk + AND inv_warehouse_sk = w_warehouse_sk + AND d_month_seq BETWEEN 1205 AND 1205 + 11 +GROUP BY rollup( i_product_name, i_brand, i_class, i_category ) +ORDER BY qoh, + i_product_name, + i_brand, + i_class, + i_category +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query23.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query23.sql new file mode 100644 index 000000000..66ffc4412 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query23.sql @@ -0,0 +1,136 @@ +-- start query 23 in stream 0 using template query23.tpl +WITH frequent_ss_items + AS (SELECT Substr(i_item_desc, 1, 30) itemdesc, + i_item_sk item_sk, + d_date solddate, + Count(*) cnt + FROM store_sales, + date_dim, + item + WHERE ss_sold_date_sk = d_date_sk + AND ss_item_sk = i_item_sk + AND d_year IN ( 1998, 1998 + 1, 1998 + 2, 1998 + 3 ) + GROUP BY Substr(i_item_desc, 1, 30), + i_item_sk, + d_date + HAVING Count(*) > 4), + max_store_sales + AS (SELECT Max(csales) tpcds_cmax + FROM (SELECT c_customer_sk, + Sum(ss_quantity * ss_sales_price) csales + FROM store_sales, + customer, + date_dim + WHERE ss_customer_sk = c_customer_sk + AND ss_sold_date_sk = d_date_sk + AND d_year IN ( 1998, 1998 + 1, 1998 + 2, 1998 + 3 ) + GROUP BY c_customer_sk)), + best_ss_customer + AS (SELECT c_customer_sk, + Sum(ss_quantity * ss_sales_price) ssales + FROM store_sales, + customer + WHERE ss_customer_sk = c_customer_sk + GROUP BY c_customer_sk + HAVING Sum(ss_quantity * ss_sales_price) > + ( 95 / 100.0 ) * (SELECT * + FROM max_store_sales)) +SELECT Sum(sales) +FROM (SELECT cs_quantity * cs_list_price sales + FROM catalog_sales, + date_dim + WHERE d_year = 1998 + AND d_moy = 6 + AND cs_sold_date_sk = d_date_sk + AND cs_item_sk IN (SELECT item_sk + FROM frequent_ss_items) + AND cs_bill_customer_sk IN (SELECT c_customer_sk + FROM best_ss_customer) + UNION ALL + SELECT ws_quantity * ws_list_price sales + FROM web_sales, + date_dim + WHERE d_year = 1998 + AND d_moy = 6 + AND ws_sold_date_sk = d_date_sk + AND ws_item_sk IN (SELECT item_sk + FROM frequent_ss_items) + AND ws_bill_customer_sk IN (SELECT c_customer_sk + FROM best_ss_customer)) LIMIT 100; + +WITH frequent_ss_items + AS (SELECT Substr(i_item_desc, 1, 30) itemdesc, + i_item_sk item_sk, + d_date solddate, + Count(*) cnt + FROM store_sales, + date_dim, + item + WHERE ss_sold_date_sk = d_date_sk + AND ss_item_sk = i_item_sk + AND d_year IN ( 1998, 1998 + 1, 1998 + 2, 1998 + 3 ) + GROUP BY Substr(i_item_desc, 1, 30), + i_item_sk, + d_date + HAVING Count(*) > 4), + max_store_sales + AS (SELECT Max(csales) tpcds_cmax + FROM (SELECT c_customer_sk, + Sum(ss_quantity * ss_sales_price) csales + FROM store_sales, + customer, + date_dim + WHERE ss_customer_sk = c_customer_sk + AND ss_sold_date_sk = d_date_sk + AND d_year IN ( 1998, 1998 + 1, 1998 + 2, 1998 + 3 ) + GROUP BY c_customer_sk)), + best_ss_customer + AS (SELECT c_customer_sk, + Sum(ss_quantity * ss_sales_price) ssales + FROM store_sales, + customer + WHERE ss_customer_sk = c_customer_sk + GROUP BY c_customer_sk + HAVING Sum(ss_quantity * ss_sales_price) > + ( 95 / 100.0 ) * (SELECT * + FROM max_store_sales)) +SELECT c_last_name, + c_first_name, + sales +FROM (SELECT c_last_name, + c_first_name, + Sum(cs_quantity * cs_list_price) sales + FROM catalog_sales, + customer, + date_dim + WHERE d_year = 1998 + AND d_moy = 6 + AND cs_sold_date_sk = d_date_sk + AND cs_item_sk IN (SELECT item_sk + FROM frequent_ss_items) + AND cs_bill_customer_sk IN (SELECT c_customer_sk + FROM best_ss_customer) + AND cs_bill_customer_sk = c_customer_sk + GROUP BY c_last_name, + c_first_name + UNION ALL + SELECT c_last_name, + c_first_name, + Sum(ws_quantity * ws_list_price) sales + FROM web_sales, + customer, + date_dim + WHERE d_year = 1998 + AND d_moy = 6 + AND ws_sold_date_sk = d_date_sk + AND ws_item_sk IN (SELECT item_sk + FROM frequent_ss_items) + AND ws_bill_customer_sk IN (SELECT c_customer_sk + FROM best_ss_customer) + AND ws_bill_customer_sk = c_customer_sk + GROUP BY c_last_name, + c_first_name) +ORDER BY c_last_name, + c_first_name, + sales +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query24.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query24.sql new file mode 100644 index 000000000..8382ca81d --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query24.sql @@ -0,0 +1,96 @@ +-- start query 24 in stream 0 using template query24.tpl +WITH ssales + AS (SELECT c_last_name, + c_first_name, + s_store_name, + ca_state, + s_state, + i_color, + i_current_price, + i_manager_id, + i_units, + i_size, + Sum(ss_net_profit) netpaid + FROM store_sales, + store_returns, + store, + item, + customer, + customer_address + WHERE ss_ticket_number = sr_ticket_number + AND ss_item_sk = sr_item_sk + AND ss_customer_sk = c_customer_sk + AND ss_item_sk = i_item_sk + AND ss_store_sk = s_store_sk + AND c_birth_country = Upper(ca_country) + AND s_zip = ca_zip + AND s_market_id = 6 + GROUP BY c_last_name, + c_first_name, + s_store_name, + ca_state, + s_state, + i_color, + i_current_price, + i_manager_id, + i_units, + i_size) +SELECT c_last_name, + c_first_name, + s_store_name, + Sum(netpaid) paid +FROM ssales +WHERE i_color = 'papaya' +GROUP BY c_last_name, + c_first_name, + s_store_name +HAVING Sum(netpaid) > (SELECT 0.05 * Avg(netpaid) + FROM ssales); + +WITH ssales + AS (SELECT c_last_name, + c_first_name, + s_store_name, + ca_state, + s_state, + i_color, + i_current_price, + i_manager_id, + i_units, + i_size, + Sum(ss_net_profit) netpaid + FROM store_sales, + store_returns, + store, + item, + customer, + customer_address + WHERE ss_ticket_number = sr_ticket_number + AND ss_item_sk = sr_item_sk + AND ss_customer_sk = c_customer_sk + AND ss_item_sk = i_item_sk + AND ss_store_sk = s_store_sk + AND c_birth_country = Upper(ca_country) + AND s_zip = ca_zip + AND s_market_id = 6 + GROUP BY c_last_name, + c_first_name, + s_store_name, + ca_state, + s_state, + i_color, + i_current_price, + i_manager_id, + i_units, + i_size) +SELECT c_last_name, + c_first_name, + s_store_name, + Sum(netpaid) paid +FROM ssales +WHERE i_color = 'chartreuse' +GROUP BY c_last_name, + c_first_name, + s_store_name +HAVING Sum(netpaid) > (SELECT 0.05 * Avg(netpaid) + FROM ssales); diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query25.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query25.sql new file mode 100644 index 000000000..fe58702f6 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query25.sql @@ -0,0 +1,41 @@ +-- start query 25 in stream 0 using template query25.tpl +SELECT i_item_id, + i_item_desc, + s_store_id, + s_store_name, + Max(ss_net_profit) AS store_sales_profit, + Max(sr_net_loss) AS store_returns_loss, + Max(cs_net_profit) AS catalog_sales_profit +FROM store_sales, + store_returns, + catalog_sales, + date_dim d1, + date_dim d2, + date_dim d3, + store, + item +WHERE d1.d_moy = 4 + AND d1.d_year = 2001 + AND d1.d_date_sk = ss_sold_date_sk + AND i_item_sk = ss_item_sk + AND s_store_sk = ss_store_sk + AND ss_customer_sk = sr_customer_sk + AND ss_item_sk = sr_item_sk + AND ss_ticket_number = sr_ticket_number + AND sr_returned_date_sk = d2.d_date_sk + AND d2.d_moy BETWEEN 4 AND 10 + AND d2.d_year = 2001 + AND sr_customer_sk = cs_bill_customer_sk + AND sr_item_sk = cs_item_sk + AND cs_sold_date_sk = d3.d_date_sk + AND d3.d_moy BETWEEN 4 AND 10 + AND d3.d_year = 2001 +GROUP BY i_item_id, + i_item_desc, + s_store_id, + s_store_name +ORDER BY i_item_id, + i_item_desc, + s_store_id, + s_store_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query26.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query26.sql new file mode 100644 index 000000000..d4818a37b --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query26.sql @@ -0,0 +1,24 @@ +-- start query 26 in stream 0 using template query26.tpl +SELECT i_item_id, + Avg(cs_quantity) agg1, + Avg(cs_list_price) agg2, + Avg(cs_coupon_amt) agg3, + Avg(cs_sales_price) agg4 +FROM catalog_sales, + customer_demographics, + date_dim, + item, + promotion +WHERE cs_sold_date_sk = d_date_sk + AND cs_item_sk = i_item_sk + AND cs_bill_cdemo_sk = cd_demo_sk + AND cs_promo_sk = p_promo_sk + AND cd_gender = 'F' + AND cd_marital_status = 'W' + AND cd_education_status = 'Secondary' + AND ( p_channel_email = 'N' + OR p_channel_event = 'N' ) + AND d_year = 2000 +GROUP BY i_item_id +ORDER BY i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query27.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query27.sql new file mode 100644 index 000000000..98fe056e5 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query27.sql @@ -0,0 +1,27 @@ +-- start query 27 in stream 0 using template query27.tpl +SELECT i_item_id, + s_state, + Grouping(s_state) g_state, + Avg(ss_quantity) agg1, + Avg(ss_list_price) agg2, + Avg(ss_coupon_amt) agg3, + Avg(ss_sales_price) agg4 +FROM store_sales, + customer_demographics, + date_dim, + store, + item +WHERE ss_sold_date_sk = d_date_sk + AND ss_item_sk = i_item_sk + AND ss_store_sk = s_store_sk + AND ss_cdemo_sk = cd_demo_sk + AND cd_gender = 'M' + AND cd_marital_status = 'D' + AND cd_education_status = 'College' + AND d_year = 2000 + AND s_state IN ( 'TN', 'TN', 'TN', 'TN', + 'TN', 'TN' ) +GROUP BY rollup ( i_item_id, s_state ) +ORDER BY i_item_id, + s_state +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query28.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query28.sql new file mode 100644 index 000000000..3aa74a9d9 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query28.sql @@ -0,0 +1,51 @@ +-- start query 28 in stream 0 using template query28.tpl +SELECT * +FROM (SELECT Avg(ss_list_price) B1_LP, + Count(ss_list_price) B1_CNT, + Count(DISTINCT ss_list_price) B1_CNTD + FROM store_sales + WHERE ss_quantity BETWEEN 0 AND 5 + AND ( ss_list_price BETWEEN 18 AND 18 + 10 + OR ss_coupon_amt BETWEEN 1939 AND 1939 + 1000 + OR ss_wholesale_cost BETWEEN 34 AND 34 + 20 )) B1, + (SELECT Avg(ss_list_price) B2_LP, + Count(ss_list_price) B2_CNT, + Count(DISTINCT ss_list_price) B2_CNTD + FROM store_sales + WHERE ss_quantity BETWEEN 6 AND 10 + AND ( ss_list_price BETWEEN 1 AND 1 + 10 + OR ss_coupon_amt BETWEEN 35 AND 35 + 1000 + OR ss_wholesale_cost BETWEEN 50 AND 50 + 20 )) B2, + (SELECT Avg(ss_list_price) B3_LP, + Count(ss_list_price) B3_CNT, + Count(DISTINCT ss_list_price) B3_CNTD + FROM store_sales + WHERE ss_quantity BETWEEN 11 AND 15 + AND ( ss_list_price BETWEEN 91 AND 91 + 10 + OR ss_coupon_amt BETWEEN 1412 AND 1412 + 1000 + OR ss_wholesale_cost BETWEEN 17 AND 17 + 20 )) B3, + (SELECT Avg(ss_list_price) B4_LP, + Count(ss_list_price) B4_CNT, + Count(DISTINCT ss_list_price) B4_CNTD + FROM store_sales + WHERE ss_quantity BETWEEN 16 AND 20 + AND ( ss_list_price BETWEEN 9 AND 9 + 10 + OR ss_coupon_amt BETWEEN 5270 AND 5270 + 1000 + OR ss_wholesale_cost BETWEEN 29 AND 29 + 20 )) B4, + (SELECT Avg(ss_list_price) B5_LP, + Count(ss_list_price) B5_CNT, + Count(DISTINCT ss_list_price) B5_CNTD + FROM store_sales + WHERE ss_quantity BETWEEN 21 AND 25 + AND ( ss_list_price BETWEEN 45 AND 45 + 10 + OR ss_coupon_amt BETWEEN 826 AND 826 + 1000 + OR ss_wholesale_cost BETWEEN 5 AND 5 + 20 )) B5, + (SELECT Avg(ss_list_price) B6_LP, + Count(ss_list_price) B6_CNT, + Count(DISTINCT ss_list_price) B6_CNTD + FROM store_sales + WHERE ss_quantity BETWEEN 26 AND 30 + AND ( ss_list_price BETWEEN 174 AND 174 + 10 + OR ss_coupon_amt BETWEEN 5548 AND 5548 + 1000 + OR ss_wholesale_cost BETWEEN 42 AND 42 + 20 )) B6 +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query29.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query29.sql new file mode 100644 index 000000000..b685e6179 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query29.sql @@ -0,0 +1,40 @@ +-- start query 29 in stream 0 using template query29.tpl +SELECT i_item_id, + i_item_desc, + s_store_id, + s_store_name, + Avg(ss_quantity) AS store_sales_quantity, + Avg(sr_return_quantity) AS store_returns_quantity, + Avg(cs_quantity) AS catalog_sales_quantity +FROM store_sales, + store_returns, + catalog_sales, + date_dim d1, + date_dim d2, + date_dim d3, + store, + item +WHERE d1.d_moy = 4 + AND d1.d_year = 1998 + AND d1.d_date_sk = ss_sold_date_sk + AND i_item_sk = ss_item_sk + AND s_store_sk = ss_store_sk + AND ss_customer_sk = sr_customer_sk + AND ss_item_sk = sr_item_sk + AND ss_ticket_number = sr_ticket_number + AND sr_returned_date_sk = d2.d_date_sk + AND d2.d_moy BETWEEN 4 AND 4 + 3 + AND d2.d_year = 1998 + AND sr_customer_sk = cs_bill_customer_sk + AND sr_item_sk = cs_item_sk + AND cs_sold_date_sk = d3.d_date_sk + AND d3.d_year IN ( 1998, 1998 + 1, 1998 + 2 ) +GROUP BY i_item_id, + i_item_desc, + s_store_id, + s_store_name +ORDER BY i_item_id, + i_item_desc, + s_store_id, + s_store_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query3.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query3.sql new file mode 100644 index 000000000..711b0fe7c --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query3.sql @@ -0,0 +1,19 @@ +-- start query 3 in stream 0 using template query3.tpl +SELECT dt.d_year, + item.i_brand_id brand_id, + item.i_brand brand, + Sum(ss_ext_discount_amt) sum_agg +FROM date_dim dt, + store_sales, + item +WHERE dt.d_date_sk = store_sales.ss_sold_date_sk + AND store_sales.ss_item_sk = item.i_item_sk + AND item.i_manufact_id = 427 + AND dt.d_moy = 11 +GROUP BY dt.d_year, + item.i_brand, + item.i_brand_id +ORDER BY dt.d_year, + sum_agg DESC, + brand_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query30.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query30.sql new file mode 100644 index 000000000..4b0498f72 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query30.sql @@ -0,0 +1,49 @@ +-- start query 30 in stream 0 using template query30.tpl +WITH customer_total_return + AS (SELECT wr_returning_customer_sk AS ctr_customer_sk, + ca_state AS ctr_state, + Sum(wr_return_amt) AS ctr_total_return + FROM web_returns, + date_dim, + customer_address + WHERE wr_returned_date_sk = d_date_sk + AND d_year = 2000 + AND wr_returning_addr_sk = ca_address_sk + GROUP BY wr_returning_customer_sk, + ca_state) +SELECT c_customer_id, + c_salutation, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_day, + c_birth_month, + c_birth_year, + c_birth_country, + c_login, + c_email_address, + c_last_review_date, + ctr_total_return +FROM customer_total_return ctr1, + customer_address, + customer +WHERE ctr1.ctr_total_return > (SELECT Avg(ctr_total_return) * 1.2 + FROM customer_total_return ctr2 + WHERE ctr1.ctr_state = ctr2.ctr_state) + AND ca_address_sk = c_current_addr_sk + AND ca_state = 'IN' + AND ctr1.ctr_customer_sk = c_customer_sk +ORDER BY c_customer_id, + c_salutation, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_day, + c_birth_month, + c_birth_year, + c_birth_country, + c_login, + c_email_address, + c_last_review_date, + ctr_total_return +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query31.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query31.sql new file mode 100644 index 000000000..8ab3ffb41 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query31.sql @@ -0,0 +1,73 @@ +-- start query 31 in stream 0 using template query31.tpl +WITH ss + AS (SELECT ca_county, + d_qoy, + d_year, + Sum(ss_ext_sales_price) AS store_sales + FROM store_sales, + date_dim, + customer_address + WHERE ss_sold_date_sk = d_date_sk + AND ss_addr_sk = ca_address_sk + GROUP BY ca_county, + d_qoy, + d_year), + ws + AS (SELECT ca_county, + d_qoy, + d_year, + Sum(ws_ext_sales_price) AS web_sales + FROM web_sales, + date_dim, + customer_address + WHERE ws_sold_date_sk = d_date_sk + AND ws_bill_addr_sk = ca_address_sk + GROUP BY ca_county, + d_qoy, + d_year) +SELECT ss1.ca_county, + ss1.d_year, + ws2.web_sales / ws1.web_sales web_q1_q2_increase, + ss2.store_sales / ss1.store_sales store_q1_q2_increase, + ws3.web_sales / ws2.web_sales web_q2_q3_increase, + ss3.store_sales / ss2.store_sales store_q2_q3_increase +FROM ss ss1, + ss ss2, + ss ss3, + ws ws1, + ws ws2, + ws ws3 +WHERE ss1.d_qoy = 1 + AND ss1.d_year = 2001 + AND ss1.ca_county = ss2.ca_county + AND ss2.d_qoy = 2 + AND ss2.d_year = 2001 + AND ss2.ca_county = ss3.ca_county + AND ss3.d_qoy = 3 + AND ss3.d_year = 2001 + AND ss1.ca_county = ws1.ca_county + AND ws1.d_qoy = 1 + AND ws1.d_year = 2001 + AND ws1.ca_county = ws2.ca_county + AND ws2.d_qoy = 2 + AND ws2.d_year = 2001 + AND ws1.ca_county = ws3.ca_county + AND ws3.d_qoy = 3 + AND ws3.d_year = 2001 + AND CASE + WHEN ws1.web_sales > 0 THEN ws2.web_sales / ws1.web_sales + ELSE NULL + END > CASE + WHEN ss1.store_sales > 0 THEN + ss2.store_sales / ss1.store_sales + ELSE NULL + END + AND CASE + WHEN ws2.web_sales > 0 THEN ws3.web_sales / ws2.web_sales + ELSE NULL + END > CASE + WHEN ss2.store_sales > 0 THEN + ss3.store_sales / ss2.store_sales + ELSE NULL + END +ORDER BY ss1.d_year; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query32.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query32.sql new file mode 100644 index 000000000..22b66886a --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query32.sql @@ -0,0 +1,21 @@ +-- start query 32 in stream 0 using template query32.tpl +SELECT + Sum(cs_ext_discount_amt) AS "excess discount amount" +FROM catalog_sales , + item , + date_dim +WHERE i_manufact_id = 610 +AND i_item_sk = cs_item_sk +AND d_date BETWEEN '2001-03-04' AND ( + Cast('2001-03-04' AS DATE) + INTERVAL '90' day) +AND d_date_sk = cs_sold_date_sk +AND cs_ext_discount_amt > + ( + SELECT 1.3 * avg(cs_ext_discount_amt) + FROM catalog_sales , + date_dim + WHERE cs_item_sk = i_item_sk + AND d_date BETWEEN '2001-03-04' AND ( + cast('2001-03-04' AS date) + INTERVAL '90' day) + AND d_date_sk = cs_sold_date_sk ) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query33.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query33.sql new file mode 100644 index 000000000..c4161054a --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query33.sql @@ -0,0 +1,65 @@ +-- start query 33 in stream 0 using template query33.tpl +WITH ss + AS (SELECT i_manufact_id, + Sum(ss_ext_sales_price) total_sales + FROM store_sales, + date_dim, + customer_address, + item + WHERE i_manufact_id IN (SELECT i_manufact_id + FROM item + WHERE i_category IN ( 'Books' )) + AND ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 3 + AND ss_addr_sk = ca_address_sk + AND ca_gmt_offset = -5 + GROUP BY i_manufact_id), + cs + AS (SELECT i_manufact_id, + Sum(cs_ext_sales_price) total_sales + FROM catalog_sales, + date_dim, + customer_address, + item + WHERE i_manufact_id IN (SELECT i_manufact_id + FROM item + WHERE i_category IN ( 'Books' )) + AND cs_item_sk = i_item_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 3 + AND cs_bill_addr_sk = ca_address_sk + AND ca_gmt_offset = -5 + GROUP BY i_manufact_id), + ws + AS (SELECT i_manufact_id, + Sum(ws_ext_sales_price) total_sales + FROM web_sales, + date_dim, + customer_address, + item + WHERE i_manufact_id IN (SELECT i_manufact_id + FROM item + WHERE i_category IN ( 'Books' )) + AND ws_item_sk = i_item_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 3 + AND ws_bill_addr_sk = ca_address_sk + AND ca_gmt_offset = -5 + GROUP BY i_manufact_id) +SELECT i_manufact_id, + Sum(total_sales) total_sales +FROM (SELECT * + FROM ss + UNION ALL + SELECT * + FROM cs + UNION ALL + SELECT * + FROM ws) tmp1 +GROUP BY i_manufact_id +ORDER BY total_sales +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query34.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query34.sql new file mode 100644 index 000000000..613734ffa --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query34.sql @@ -0,0 +1,46 @@ +-- start query 34 in stream 0 using template query34.tpl +SELECT c_last_name, + c_first_name, + c_salutation, + c_preferred_cust_flag, + ss_ticket_number, + cnt +FROM (SELECT ss_ticket_number, + ss_customer_sk, + Count(*) cnt + FROM store_sales, + date_dim, + store, + household_demographics + WHERE store_sales.ss_sold_date_sk = date_dim.d_date_sk + AND store_sales.ss_store_sk = store.s_store_sk + AND store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk + AND ( date_dim.d_dom BETWEEN 1 AND 3 + OR date_dim.d_dom BETWEEN 25 AND 28 ) + AND ( household_demographics.hd_buy_potential = '>10000' + OR household_demographics.hd_buy_potential = 'unknown' ) + AND household_demographics.hd_vehicle_count > 0 + AND ( CASE + WHEN household_demographics.hd_vehicle_count > 0 THEN + household_demographics.hd_dep_count / + household_demographics.hd_vehicle_count + ELSE NULL + END ) > 1.2 + AND date_dim.d_year IN ( 1999, 1999 + 1, 1999 + 2 ) + AND store.s_county IN ( 'Williamson County', 'Williamson County', + 'Williamson County', + 'Williamson County' + , + 'Williamson County', 'Williamson County', + 'Williamson County', + 'Williamson County' + ) + GROUP BY ss_ticket_number, + ss_customer_sk) dn, + customer +WHERE ss_customer_sk = c_customer_sk + AND cnt BETWEEN 15 AND 20 +ORDER BY c_last_name, + c_first_name, + c_salutation, + c_preferred_cust_flag DESC; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query35.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query35.sql new file mode 100644 index 000000000..d5912a411 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query35.sql @@ -0,0 +1,58 @@ +-- start query 35 in stream 0 using template query35.tpl +SELECT ca_state, + cd_gender, + cd_marital_status, + cd_dep_count, + Count(*) cnt1, + Stddev_samp(cd_dep_count), + Avg(cd_dep_count), + Max(cd_dep_count), + cd_dep_employed_count, + Count(*) cnt2, + Stddev_samp(cd_dep_employed_count), + Avg(cd_dep_employed_count), + Max(cd_dep_employed_count), + cd_dep_college_count, + Count(*) cnt3, + Stddev_samp(cd_dep_college_count), + Avg(cd_dep_college_count), + Max(cd_dep_college_count) +FROM customer c, + customer_address ca, + customer_demographics +WHERE c.c_current_addr_sk = ca.ca_address_sk + AND cd_demo_sk = c.c_current_cdemo_sk + AND EXISTS (SELECT * + FROM store_sales, + date_dim + WHERE c.c_customer_sk = ss_customer_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 2001 + AND d_qoy < 4) + AND ( EXISTS (SELECT * + FROM web_sales, + date_dim + WHERE c.c_customer_sk = ws_bill_customer_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 2001 + AND d_qoy < 4) + OR EXISTS (SELECT * + FROM catalog_sales, + date_dim + WHERE c.c_customer_sk = cs_ship_customer_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 2001 + AND d_qoy < 4) ) +GROUP BY ca_state, + cd_gender, + cd_marital_status, + cd_dep_count, + cd_dep_employed_count, + cd_dep_college_count +ORDER BY ca_state, + cd_gender, + cd_marital_status, + cd_dep_count, + cd_dep_employed_count, + cd_dep_college_count +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query36.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query36.sql new file mode 100644 index 000000000..21d69fbbb --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query36.sql @@ -0,0 +1,31 @@ +-- start query 36 in stream 0 using template query36.tpl +SELECT Sum(ss_net_profit) / Sum(ss_ext_sales_price) AS + gross_margin, + i_category, + i_class, + Grouping(i_category) + Grouping(i_class) AS + lochierarchy, + Rank() + OVER ( + partition BY Grouping(i_category)+Grouping(i_class), CASE + WHEN Grouping( + i_class) = 0 THEN i_category END + ORDER BY Sum(ss_net_profit)/Sum(ss_ext_sales_price) ASC) AS + rank_within_parent +FROM store_sales, + date_dim d1, + item, + store +WHERE d1.d_year = 2000 + AND d1.d_date_sk = ss_sold_date_sk + AND i_item_sk = ss_item_sk + AND s_store_sk = ss_store_sk + AND s_state IN ( 'TN', 'TN', 'TN', 'TN', + 'TN', 'TN', 'TN', 'TN' ) +GROUP BY rollup( i_category, i_class ) +ORDER BY lochierarchy DESC, + CASE + WHEN lochierarchy = 0 THEN i_category + END, + rank_within_parent +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query37.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query37.sql new file mode 100644 index 000000000..52cd153a9 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query37.sql @@ -0,0 +1,22 @@ +-- start query 37 in stream 0 using template query37.tpl +SELECT + i_item_id , + i_item_desc , + i_current_price +FROM item, + inventory, + date_dim, + catalog_sales +WHERE i_current_price BETWEEN 20 AND 20 + 30 +AND inv_item_sk = i_item_sk +AND d_date_sk=inv_date_sk +AND d_date BETWEEN Cast('1999-03-06' AS DATE) AND ( + Cast('1999-03-06' AS DATE) + INTERVAL '60' day) +AND i_manufact_id IN (843,815,850,840) +AND inv_quantity_on_hand BETWEEN 100 AND 500 +AND cs_item_sk = i_item_sk +GROUP BY i_item_id, + i_item_desc, + i_current_price +ORDER BY i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query38.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query38.sql new file mode 100644 index 000000000..546068717 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query38.sql @@ -0,0 +1,32 @@ +-- start query 38 in stream 0 using template query38.tpl +SELECT Count(*) +FROM (SELECT DISTINCT c_last_name, + c_first_name, + d_date + FROM store_sales, + date_dim, + customer + WHERE store_sales.ss_sold_date_sk = date_dim.d_date_sk + AND store_sales.ss_customer_sk = customer.c_customer_sk + AND d_month_seq BETWEEN 1188 AND 1188 + 11 + INTERSECT + SELECT DISTINCT c_last_name, + c_first_name, + d_date + FROM catalog_sales, + date_dim, + customer + WHERE catalog_sales.cs_sold_date_sk = date_dim.d_date_sk + AND catalog_sales.cs_bill_customer_sk = customer.c_customer_sk + AND d_month_seq BETWEEN 1188 AND 1188 + 11 + INTERSECT + SELECT DISTINCT c_last_name, + c_first_name, + d_date + FROM web_sales, + date_dim, + customer + WHERE web_sales.ws_sold_date_sk = date_dim.d_date_sk + AND web_sales.ws_bill_customer_sk = customer.c_customer_sk + AND d_month_seq BETWEEN 1188 AND 1188 + 11) hot_cust +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query39.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query39.sql new file mode 100644 index 000000000..4abb67d30 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query39.sql @@ -0,0 +1,117 @@ +-- start query 39 in stream 0 using template query39.tpl +WITH inv + AS (SELECT w_warehouse_name, + w_warehouse_sk, + i_item_sk, + d_moy, + stdev, + mean, + CASE mean + WHEN 0 THEN NULL + ELSE stdev / mean + END cov + FROM (SELECT w_warehouse_name, + w_warehouse_sk, + i_item_sk, + d_moy, + Stddev_samp(inv_quantity_on_hand) stdev, + Avg(inv_quantity_on_hand) mean + FROM inventory, + item, + warehouse, + date_dim + WHERE inv_item_sk = i_item_sk + AND inv_warehouse_sk = w_warehouse_sk + AND inv_date_sk = d_date_sk + AND d_year = 2002 + GROUP BY w_warehouse_name, + w_warehouse_sk, + i_item_sk, + d_moy) foo + WHERE CASE mean + WHEN 0 THEN 0 + ELSE stdev / mean + END > 1) +SELECT inv1.w_warehouse_sk, + inv1.i_item_sk, + inv1.d_moy, + inv1.mean, + inv1.cov, + inv2.w_warehouse_sk, + inv2.i_item_sk, + inv2.d_moy, + inv2.mean, + inv2.cov +FROM inv inv1, + inv inv2 +WHERE inv1.i_item_sk = inv2.i_item_sk + AND inv1.w_warehouse_sk = inv2.w_warehouse_sk + AND inv1.d_moy = 1 + AND inv2.d_moy = 1 + 1 +ORDER BY inv1.w_warehouse_sk, + inv1.i_item_sk, + inv1.d_moy, + inv1.mean, + inv1.cov, + inv2.d_moy, + inv2.mean, + inv2.cov; + +WITH inv + AS (SELECT w_warehouse_name, + w_warehouse_sk, + i_item_sk, + d_moy, + stdev, + mean, + CASE mean + WHEN 0 THEN NULL + ELSE stdev / mean + END cov + FROM (SELECT w_warehouse_name, + w_warehouse_sk, + i_item_sk, + d_moy, + Stddev_samp(inv_quantity_on_hand) stdev, + Avg(inv_quantity_on_hand) mean + FROM inventory, + item, + warehouse, + date_dim + WHERE inv_item_sk = i_item_sk + AND inv_warehouse_sk = w_warehouse_sk + AND inv_date_sk = d_date_sk + AND d_year = 2002 + GROUP BY w_warehouse_name, + w_warehouse_sk, + i_item_sk, + d_moy) foo + WHERE CASE mean + WHEN 0 THEN 0 + ELSE stdev / mean + END > 1) +SELECT inv1.w_warehouse_sk, + inv1.i_item_sk, + inv1.d_moy, + inv1.mean, + inv1.cov, + inv2.w_warehouse_sk, + inv2.i_item_sk, + inv2.d_moy, + inv2.mean, + inv2.cov +FROM inv inv1, + inv inv2 +WHERE inv1.i_item_sk = inv2.i_item_sk + AND inv1.w_warehouse_sk = inv2.w_warehouse_sk + AND inv1.d_moy = 1 + AND inv2.d_moy = 1 + 1 + AND inv1.cov > 1.5 +ORDER BY inv1.w_warehouse_sk, + inv1.i_item_sk, + inv1.d_moy, + inv1.mean, + inv1.cov, + inv2.d_moy, + inv2.mean, + inv2.cov; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query4.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query4.sql new file mode 100644 index 000000000..281b2d5d2 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query4.sql @@ -0,0 +1,152 @@ +-- start query 4 in stream 0 using template query4.tpl +WITH year_total + AS (SELECT c_customer_id customer_id, + c_first_name customer_first_name, + c_last_name customer_last_name, + c_preferred_cust_flag customer_preferred_cust_flag + , + c_birth_country + customer_birth_country, + c_login customer_login, + c_email_address customer_email_address, + d_year dyear, + Sum(( ( ss_ext_list_price - ss_ext_wholesale_cost + - ss_ext_discount_amt + ) + + + ss_ext_sales_price ) / 2) year_total, + 's' sale_type + FROM customer, + store_sales, + date_dim + WHERE c_customer_sk = ss_customer_sk + AND ss_sold_date_sk = d_date_sk + GROUP BY c_customer_id, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_country, + c_login, + c_email_address, + d_year + UNION ALL + SELECT c_customer_id customer_id, + c_first_name customer_first_name, + c_last_name customer_last_name, + c_preferred_cust_flag + customer_preferred_cust_flag, + c_birth_country customer_birth_country + , + c_login + customer_login, + c_email_address customer_email_address + , + d_year dyear + , + Sum(( ( ( cs_ext_list_price + - cs_ext_wholesale_cost + - cs_ext_discount_amt + ) + + cs_ext_sales_price ) / 2 )) year_total, + 'c' sale_type + FROM customer, + catalog_sales, + date_dim + WHERE c_customer_sk = cs_bill_customer_sk + AND cs_sold_date_sk = d_date_sk + GROUP BY c_customer_id, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_country, + c_login, + c_email_address, + d_year + UNION ALL + SELECT c_customer_id customer_id, + c_first_name customer_first_name, + c_last_name customer_last_name, + c_preferred_cust_flag + customer_preferred_cust_flag, + c_birth_country customer_birth_country + , + c_login + customer_login, + c_email_address customer_email_address + , + d_year dyear + , + Sum(( ( ( ws_ext_list_price + - ws_ext_wholesale_cost + - ws_ext_discount_amt + ) + + ws_ext_sales_price ) / 2 )) year_total, + 'w' sale_type + FROM customer, + web_sales, + date_dim + WHERE c_customer_sk = ws_bill_customer_sk + AND ws_sold_date_sk = d_date_sk + GROUP BY c_customer_id, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_country, + c_login, + c_email_address, + d_year) +SELECT t_s_secyear.customer_id, + t_s_secyear.customer_first_name, + t_s_secyear.customer_last_name, + t_s_secyear.customer_preferred_cust_flag +FROM year_total t_s_firstyear, + year_total t_s_secyear, + year_total t_c_firstyear, + year_total t_c_secyear, + year_total t_w_firstyear, + year_total t_w_secyear +WHERE t_s_secyear.customer_id = t_s_firstyear.customer_id + AND t_s_firstyear.customer_id = t_c_secyear.customer_id + AND t_s_firstyear.customer_id = t_c_firstyear.customer_id + AND t_s_firstyear.customer_id = t_w_firstyear.customer_id + AND t_s_firstyear.customer_id = t_w_secyear.customer_id + AND t_s_firstyear.sale_type = 's' + AND t_c_firstyear.sale_type = 'c' + AND t_w_firstyear.sale_type = 'w' + AND t_s_secyear.sale_type = 's' + AND t_c_secyear.sale_type = 'c' + AND t_w_secyear.sale_type = 'w' + AND t_s_firstyear.dyear = 2001 + AND t_s_secyear.dyear = 2001 + 1 + AND t_c_firstyear.dyear = 2001 + AND t_c_secyear.dyear = 2001 + 1 + AND t_w_firstyear.dyear = 2001 + AND t_w_secyear.dyear = 2001 + 1 + AND t_s_firstyear.year_total > 0 + AND t_c_firstyear.year_total > 0 + AND t_w_firstyear.year_total > 0 + AND CASE + WHEN t_c_firstyear.year_total > 0 THEN t_c_secyear.year_total / + t_c_firstyear.year_total + ELSE NULL + END > CASE + WHEN t_s_firstyear.year_total > 0 THEN + t_s_secyear.year_total / + t_s_firstyear.year_total + ELSE NULL + END + AND CASE + WHEN t_c_firstyear.year_total > 0 THEN t_c_secyear.year_total / + t_c_firstyear.year_total + ELSE NULL + END > CASE + WHEN t_w_firstyear.year_total > 0 THEN + t_w_secyear.year_total / + t_w_firstyear.year_total + ELSE NULL + END +ORDER BY t_s_secyear.customer_id, + t_s_secyear.customer_first_name, + t_s_secyear.customer_last_name, + t_s_secyear.customer_preferred_cust_flag +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query40.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query40.sql new file mode 100644 index 000000000..f7f84b873 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query40.sql @@ -0,0 +1,35 @@ +-- start query 40 in stream 0 using template query40.tpl +SELECT + w_state , + i_item_id , + Sum( + CASE + WHEN ( + Cast(d_date AS DATE) < Cast ('2002-06-01' AS DATE)) THEN cs_sales_price - COALESCE(cr_refunded_cash,0) + ELSE 0 + END) AS sales_before , + Sum( + CASE + WHEN ( + Cast(d_date AS DATE) >= Cast ('2002-06-01' AS DATE)) THEN cs_sales_price - COALESCE(cr_refunded_cash,0) + ELSE 0 + END) AS sales_after +FROM catalog_sales +LEFT OUTER JOIN catalog_returns +ON ( + cs_order_number = cr_order_number + AND cs_item_sk = cr_item_sk) , + warehouse , + item , + date_dim +WHERE i_current_price BETWEEN 0.99 AND 1.49 +AND i_item_sk = cs_item_sk +AND cs_warehouse_sk = w_warehouse_sk +AND cs_sold_date_sk = d_date_sk +AND d_date BETWEEN (Cast ('2002-06-01' AS DATE) - INTERVAL '30' day) AND ( + cast ('2002-06-01' AS date) + INTERVAL '30' day) +GROUP BY w_state, + i_item_id +ORDER BY w_state, + i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query41.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query41.sql new file mode 100644 index 000000000..467ed35ae --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query41.sql @@ -0,0 +1,66 @@ +-- start query 41 in stream 0 using template query41.tpl +SELECT Distinct(i_product_name) +FROM item i1 +WHERE i_manufact_id BETWEEN 765 AND 765 + 40 + AND (SELECT Count(*) AS item_cnt + FROM item + WHERE ( i_manufact = i1.i_manufact + AND ( ( i_category = 'Women' + AND ( i_color = 'dim' + OR i_color = 'green' ) + AND ( i_units = 'Gross' + OR i_units = 'Dozen' ) + AND ( i_size = 'economy' + OR i_size = 'petite' ) ) + OR ( i_category = 'Women' + AND ( i_color = 'navajo' + OR i_color = 'aquamarine' ) + AND ( i_units = 'Case' + OR i_units = 'Unknown' ) + AND ( i_size = 'large' + OR i_size = 'N/A' ) ) + OR ( i_category = 'Men' + AND ( i_color = 'indian' + OR i_color = 'dark' ) + AND ( i_units = 'Oz' + OR i_units = 'Lb' ) + AND ( i_size = 'extra large' + OR i_size = 'small' ) ) + OR ( i_category = 'Men' + AND ( i_color = 'peach' + OR i_color = 'purple' ) + AND ( i_units = 'Tbl' + OR i_units = 'Bunch' ) + AND ( i_size = 'economy' + OR i_size = 'petite' ) ) ) ) + OR ( i_manufact = i1.i_manufact + AND ( ( i_category = 'Women' + AND ( i_color = 'orchid' + OR i_color = 'peru' ) + AND ( i_units = 'Carton' + OR i_units = 'Cup' ) + AND ( i_size = 'economy' + OR i_size = 'petite' ) ) + OR ( i_category = 'Women' + AND ( i_color = 'violet' + OR i_color = 'papaya' ) + AND ( i_units = 'Ounce' + OR i_units = 'Box' ) + AND ( i_size = 'large' + OR i_size = 'N/A' ) ) + OR ( i_category = 'Men' + AND ( i_color = 'drab' + OR i_color = 'grey' ) + AND ( i_units = 'Each' + OR i_units = 'N/A' ) + AND ( i_size = 'extra large' + OR i_size = 'small' ) ) + OR ( i_category = 'Men' + AND ( i_color = 'chocolate' + OR i_color = 'antique' ) + AND ( i_units = 'Dram' + OR i_units = 'Gram' ) + AND ( i_size = 'economy' + OR i_size = 'petite' ) ) ) )) > 0 +ORDER BY i_product_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query42.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query42.sql new file mode 100644 index 000000000..706d3c6f1 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query42.sql @@ -0,0 +1,21 @@ +-- start query 42 in stream 0 using template query42.tpl +SELECT dt.d_year, + item.i_category_id, + item.i_category, + Sum(ss_ext_sales_price) +FROM date_dim dt, + store_sales, + item +WHERE dt.d_date_sk = store_sales.ss_sold_date_sk + AND store_sales.ss_item_sk = item.i_item_sk + AND item.i_manager_id = 1 + AND dt.d_moy = 12 + AND dt.d_year = 2000 +GROUP BY dt.d_year, + item.i_category_id, + item.i_category +ORDER BY Sum(ss_ext_sales_price) DESC, + dt.d_year, + item.i_category_id, + item.i_category +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query43.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query43.sql new file mode 100644 index 000000000..e7624a19c --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query43.sql @@ -0,0 +1,50 @@ +-- start query 43 in stream 0 using template query43.tpl +SELECT s_store_name, + s_store_id, + Sum(CASE + WHEN ( d_day_name = 'Sunday' ) THEN ss_sales_price + ELSE NULL + END) sun_sales, + Sum(CASE + WHEN ( d_day_name = 'Monday' ) THEN ss_sales_price + ELSE NULL + END) mon_sales, + Sum(CASE + WHEN ( d_day_name = 'Tuesday' ) THEN ss_sales_price + ELSE NULL + END) tue_sales, + Sum(CASE + WHEN ( d_day_name = 'Wednesday' ) THEN ss_sales_price + ELSE NULL + END) wed_sales, + Sum(CASE + WHEN ( d_day_name = 'Thursday' ) THEN ss_sales_price + ELSE NULL + END) thu_sales, + Sum(CASE + WHEN ( d_day_name = 'Friday' ) THEN ss_sales_price + ELSE NULL + END) fri_sales, + Sum(CASE + WHEN ( d_day_name = 'Saturday' ) THEN ss_sales_price + ELSE NULL + END) sat_sales +FROM date_dim, + store_sales, + store +WHERE d_date_sk = ss_sold_date_sk + AND s_store_sk = ss_store_sk + AND s_gmt_offset = -5 + AND d_year = 2002 +GROUP BY s_store_name, + s_store_id +ORDER BY s_store_name, + s_store_id, + sun_sales, + mon_sales, + tue_sales, + wed_sales, + thu_sales, + fri_sales, + sat_sales +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query44.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query44.sql new file mode 100644 index 000000000..ba869d3ce --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query44.sql @@ -0,0 +1,51 @@ +-- start query 44 in stream 0 using template query44.tpl +SELECT asceding.rnk, + i1.i_product_name best_performing, + i2.i_product_name worst_performing +FROM (SELECT * + FROM (SELECT item_sk, + Rank() + OVER ( + ORDER BY rank_col ASC) rnk + FROM (SELECT ss_item_sk item_sk, + Avg(ss_net_profit) rank_col + FROM store_sales ss1 + WHERE ss_store_sk = 4 + GROUP BY ss_item_sk + HAVING Avg(ss_net_profit) > 0.9 * + (SELECT Avg(ss_net_profit) + rank_col + FROM store_sales + WHERE ss_store_sk = 4 + AND ss_cdemo_sk IS + NULL + GROUP BY ss_store_sk))V1) + V11 + WHERE rnk < 11) asceding, + (SELECT * + FROM (SELECT item_sk, + Rank() + OVER ( + ORDER BY rank_col DESC) rnk + FROM (SELECT ss_item_sk item_sk, + Avg(ss_net_profit) rank_col + FROM store_sales ss1 + WHERE ss_store_sk = 4 + GROUP BY ss_item_sk + HAVING Avg(ss_net_profit) > 0.9 * + (SELECT Avg(ss_net_profit) + rank_col + FROM store_sales + WHERE ss_store_sk = 4 + AND ss_cdemo_sk IS + NULL + GROUP BY ss_store_sk))V2) + V21 + WHERE rnk < 11) descending, + item i1, + item i2 +WHERE asceding.rnk = descending.rnk + AND i1.i_item_sk = asceding.item_sk + AND i2.i_item_sk = descending.item_sk +ORDER BY asceding.rnk +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query45.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query45.sql new file mode 100644 index 000000000..e8d35c1c0 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query45.sql @@ -0,0 +1,28 @@ +-- start query 45 in stream 0 using template query45.tpl +SELECT ca_zip, + ca_state, + Sum(ws_sales_price) +FROM web_sales, + customer, + customer_address, + date_dim, + item +WHERE ws_bill_customer_sk = c_customer_sk + AND c_current_addr_sk = ca_address_sk + AND ws_item_sk = i_item_sk + AND ( Substr(ca_zip, 1, 5) IN ( '85669', '86197', '88274', '83405', + '86475', '85392', '85460', '80348', + '81792' ) + OR i_item_id IN (SELECT i_item_id + FROM item + WHERE i_item_sk IN ( 2, 3, 5, 7, + 11, 13, 17, 19, + 23, 29 )) ) + AND ws_sold_date_sk = d_date_sk + AND d_qoy = 1 + AND d_year = 2000 +GROUP BY ca_zip, + ca_state +ORDER BY ca_zip, + ca_state +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query46.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query46.sql new file mode 100644 index 000000000..b88b36400 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query46.sql @@ -0,0 +1,44 @@ +-- start query 46 in stream 0 using template query46.tpl +SELECT c_last_name, + c_first_name, + ca_city, + bought_city, + ss_ticket_number, + amt, + profit +FROM (SELECT ss_ticket_number, + ss_customer_sk, + ca_city bought_city, + Sum(ss_coupon_amt) amt, + Sum(ss_net_profit) profit + FROM store_sales, + date_dim, + store, + household_demographics, + customer_address + WHERE store_sales.ss_sold_date_sk = date_dim.d_date_sk + AND store_sales.ss_store_sk = store.s_store_sk + AND store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk + AND store_sales.ss_addr_sk = customer_address.ca_address_sk + AND ( household_demographics.hd_dep_count = 6 + OR household_demographics.hd_vehicle_count = 0 ) + AND date_dim.d_dow IN ( 6, 0 ) + AND date_dim.d_year IN ( 2000, 2000 + 1, 2000 + 2 ) + AND store.s_city IN ( 'Midway', 'Fairview', 'Fairview', + 'Fairview', + 'Fairview' ) + GROUP BY ss_ticket_number, + ss_customer_sk, + ss_addr_sk, + ca_city) dn, + customer, + customer_address current_addr +WHERE ss_customer_sk = c_customer_sk + AND customer.c_current_addr_sk = current_addr.ca_address_sk + AND current_addr.ca_city <> bought_city +ORDER BY c_last_name, + c_first_name, + ca_city, + bought_city, + ss_ticket_number +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query47.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query47.sql new file mode 100644 index 000000000..36ddcbbcb --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query47.sql @@ -0,0 +1,72 @@ +-- start query 47 in stream 0 using template query47.tpl +WITH v1 + AS (SELECT i_category, + i_brand, + s_store_name, + s_company_name, + d_year, + d_moy, + Sum(ss_sales_price) sum_sales, + Avg(Sum(ss_sales_price)) + OVER ( + partition BY i_category, i_brand, s_store_name, + s_company_name, + d_year) + avg_monthly_sales, + Rank() + OVER ( + partition BY i_category, i_brand, s_store_name, + s_company_name + ORDER BY d_year, d_moy) rn + FROM item, + store_sales, + date_dim, + store + WHERE ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND ss_store_sk = s_store_sk + AND ( d_year = 1999 + OR ( d_year = 1999 - 1 + AND d_moy = 12 ) + OR ( d_year = 1999 + 1 + AND d_moy = 1 ) ) + GROUP BY i_category, + i_brand, + s_store_name, + s_company_name, + d_year, + d_moy), + v2 + AS (SELECT v1.i_category, + v1.d_year, + v1.d_moy, + v1.avg_monthly_sales, + v1.sum_sales, + v1_lag.sum_sales psum, + v1_lead.sum_sales nsum + FROM v1, + v1 v1_lag, + v1 v1_lead + WHERE v1.i_category = v1_lag.i_category + AND v1.i_category = v1_lead.i_category + AND v1.i_brand = v1_lag.i_brand + AND v1.i_brand = v1_lead.i_brand + AND v1.s_store_name = v1_lag.s_store_name + AND v1.s_store_name = v1_lead.s_store_name + AND v1.s_company_name = v1_lag.s_company_name + AND v1.s_company_name = v1_lead.s_company_name + AND v1.rn = v1_lag.rn + 1 + AND v1.rn = v1_lead.rn - 1) +SELECT * +FROM v2 +WHERE d_year = 1999 + AND avg_monthly_sales > 0 + AND CASE + WHEN avg_monthly_sales > 0 THEN Abs(sum_sales - avg_monthly_sales) + / + avg_monthly_sales + ELSE NULL + END > 0.1 +ORDER BY sum_sales - avg_monthly_sales, + 3 +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query48.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query48.sql new file mode 100644 index 000000000..aa8375382 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query48.sql @@ -0,0 +1,34 @@ +-- start query 48 in stream 0 using template query48.tpl +SELECT Sum (ss_quantity) +FROM store_sales, + store, + customer_demographics, + customer_address, + date_dim +WHERE s_store_sk = ss_store_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 1999 + AND ( ( cd_demo_sk = ss_cdemo_sk + AND cd_marital_status = 'W' + AND cd_education_status = 'Secondary' + AND ss_sales_price BETWEEN 100.00 AND 150.00 ) + OR ( cd_demo_sk = ss_cdemo_sk + AND cd_marital_status = 'M' + AND cd_education_status = 'Advanced Degree' + AND ss_sales_price BETWEEN 50.00 AND 100.00 ) + OR ( cd_demo_sk = ss_cdemo_sk + AND cd_marital_status = 'D' + AND cd_education_status = '2 yr Degree' + AND ss_sales_price BETWEEN 150.00 AND 200.00 ) ) + AND ( ( ss_addr_sk = ca_address_sk + AND ca_country = 'United States' + AND ca_state IN ( 'TX', 'NE', 'MO' ) + AND ss_net_profit BETWEEN 0 AND 2000 ) + OR ( ss_addr_sk = ca_address_sk + AND ca_country = 'United States' + AND ca_state IN ( 'CO', 'TN', 'ND' ) + AND ss_net_profit BETWEEN 150 AND 3000 ) + OR ( ss_addr_sk = ca_address_sk + AND ca_country = 'United States' + AND ca_state IN ( 'OK', 'PA', 'CA' ) + AND ss_net_profit BETWEEN 50 AND 25000 ) ); diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query49.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query49.sql new file mode 100644 index 000000000..bade54bc0 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query49.sql @@ -0,0 +1,133 @@ +-- start query 49 in stream 0 using template query49.tpl +SELECT 'web' AS channel, + web.item, + web.return_ratio, + web.return_rank, + web.currency_rank +FROM (SELECT item, + return_ratio, + currency_ratio, + Rank() + OVER ( + ORDER BY return_ratio) AS return_rank, + Rank() + OVER ( + ORDER BY currency_ratio) AS currency_rank + FROM (SELECT ws.ws_item_sk AS + item, + ( Cast(Sum(COALESCE(wr.wr_return_quantity, 0)) AS DEC(15, + 4)) / + Cast( + Sum(COALESCE(ws.ws_quantity, 0)) AS DEC(15, 4)) ) AS + return_ratio, + ( Cast(Sum(COALESCE(wr.wr_return_amt, 0)) AS DEC(15, 4)) + / Cast( + Sum( + COALESCE(ws.ws_net_paid, 0)) AS DEC(15, + 4)) ) AS + currency_ratio + FROM web_sales ws + LEFT OUTER JOIN web_returns wr + ON ( ws.ws_order_number = wr.wr_order_number + AND ws.ws_item_sk = wr.wr_item_sk ), + date_dim + WHERE wr.wr_return_amt > 10000 + AND ws.ws_net_profit > 1 + AND ws.ws_net_paid > 0 + AND ws.ws_quantity > 0 + AND ws_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 12 + GROUP BY ws.ws_item_sk) in_web) web +WHERE ( web.return_rank <= 10 + OR web.currency_rank <= 10 ) +UNION +SELECT 'catalog' AS channel, + catalog.item, + catalog.return_ratio, + catalog.return_rank, + catalog.currency_rank +FROM (SELECT item, + return_ratio, + currency_ratio, + Rank() + OVER ( + ORDER BY return_ratio) AS return_rank, + Rank() + OVER ( + ORDER BY currency_ratio) AS currency_rank + FROM (SELECT cs.cs_item_sk AS + item, + ( Cast(Sum(COALESCE(cr.cr_return_quantity, 0)) AS DEC(15, + 4)) / + Cast( + Sum(COALESCE(cs.cs_quantity, 0)) AS DEC(15, 4)) ) AS + return_ratio, + ( Cast(Sum(COALESCE(cr.cr_return_amount, 0)) AS DEC(15, 4 + )) / + Cast(Sum( + COALESCE(cs.cs_net_paid, 0)) AS DEC( + 15, 4)) ) AS + currency_ratio + FROM catalog_sales cs + LEFT OUTER JOIN catalog_returns cr + ON ( cs.cs_order_number = cr.cr_order_number + AND cs.cs_item_sk = cr.cr_item_sk ), + date_dim + WHERE cr.cr_return_amount > 10000 + AND cs.cs_net_profit > 1 + AND cs.cs_net_paid > 0 + AND cs.cs_quantity > 0 + AND cs_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 12 + GROUP BY cs.cs_item_sk) in_cat) catalog +WHERE ( catalog.return_rank <= 10 + OR catalog.currency_rank <= 10 ) +UNION +SELECT 'store' AS channel, + store.item, + store.return_ratio, + store.return_rank, + store.currency_rank +FROM (SELECT item, + return_ratio, + currency_ratio, + Rank() + OVER ( + ORDER BY return_ratio) AS return_rank, + Rank() + OVER ( + ORDER BY currency_ratio) AS currency_rank + FROM (SELECT sts.ss_item_sk AS + item, + ( Cast(Sum(COALESCE(sr.sr_return_quantity, 0)) AS DEC(15, + 4)) / + Cast( + Sum(COALESCE(sts.ss_quantity, 0)) AS DEC(15, 4)) ) AS + return_ratio, + ( Cast(Sum(COALESCE(sr.sr_return_amt, 0)) AS DEC(15, 4)) + / Cast( + Sum( + COALESCE(sts.ss_net_paid, 0)) AS DEC(15, 4)) ) AS + currency_ratio + FROM store_sales sts + LEFT OUTER JOIN store_returns sr + ON ( sts.ss_ticket_number = + sr.sr_ticket_number + AND sts.ss_item_sk = sr.sr_item_sk ), + date_dim + WHERE sr.sr_return_amt > 10000 + AND sts.ss_net_profit > 1 + AND sts.ss_net_paid > 0 + AND sts.ss_quantity > 0 + AND ss_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 12 + GROUP BY sts.ss_item_sk) in_store) store +WHERE ( store.return_rank <= 10 + OR store.currency_rank <= 10 ) +ORDER BY 1, + 4, + 5 +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query5.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query5.sql new file mode 100644 index 000000000..0c03b0ddc --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query5.sql @@ -0,0 +1,127 @@ +-- start query 5 in stream 0 using template query5.tpl +WITH ssr AS +( + SELECT s_store_id, + Sum(sales_price) AS sales, + Sum(profit) AS profit, + Sum(return_amt) AS returns1, + Sum(net_loss) AS profit_loss + FROM ( + SELECT ss_store_sk AS store_sk, + ss_sold_date_sk AS date_sk, + ss_ext_sales_price AS sales_price, + ss_net_profit AS profit, + Cast(0 AS DECIMAL(7,2)) AS return_amt, + Cast(0 AS DECIMAL(7,2)) AS net_loss + FROM store_sales + UNION ALL + SELECT sr_store_sk AS store_sk, + sr_returned_date_sk AS date_sk, + Cast(0 AS DECIMAL(7,2)) AS sales_price, + Cast(0 AS DECIMAL(7,2)) AS profit, + sr_return_amt AS return_amt, + sr_net_loss AS net_loss + FROM store_returns ) salesreturns, + date_dim, + store + WHERE date_sk = d_date_sk + AND d_date BETWEEN Cast('2002-08-22' AS DATE) AND ( + Cast('2002-08-22' AS DATE) + INTERVAL '14' day) + AND store_sk = s_store_sk + GROUP BY s_store_id) , csr AS +( + SELECT cp_catalog_page_id, + sum(sales_price) AS sales, + sum(profit) AS profit, + sum(return_amt) AS returns1, + sum(net_loss) AS profit_loss + FROM ( + SELECT cs_catalog_page_sk AS page_sk, + cs_sold_date_sk AS date_sk, + cs_ext_sales_price AS sales_price, + cs_net_profit AS profit, + cast(0 AS decimal(7,2)) AS return_amt, + cast(0 AS decimal(7,2)) AS net_loss + FROM catalog_sales + UNION ALL + SELECT cr_catalog_page_sk AS page_sk, + cr_returned_date_sk AS date_sk, + cast(0 AS decimal(7,2)) AS sales_price, + cast(0 AS decimal(7,2)) AS profit, + cr_return_amount AS return_amt, + cr_net_loss AS net_loss + FROM catalog_returns ) salesreturns, + date_dim, + catalog_page + WHERE date_sk = d_date_sk + AND d_date BETWEEN cast('2002-08-22' AS date) AND ( + cast('2002-08-22' AS date) + INTERVAL '14' day) + AND page_sk = cp_catalog_page_sk + GROUP BY cp_catalog_page_id) , wsr AS +( + SELECT web_site_id, + sum(sales_price) AS sales, + sum(profit) AS profit, + sum(return_amt) AS returns1, + sum(net_loss) AS profit_loss + FROM ( + SELECT ws_web_site_sk AS wsr_web_site_sk, + ws_sold_date_sk AS date_sk, + ws_ext_sales_price AS sales_price, + ws_net_profit AS profit, + cast(0 AS decimal(7,2)) AS return_amt, + cast(0 AS decimal(7,2)) AS net_loss + FROM web_sales + UNION ALL + SELECT ws_web_site_sk AS wsr_web_site_sk, + wr_returned_date_sk AS date_sk, + cast(0 AS decimal(7,2)) AS sales_price, + cast(0 AS decimal(7,2)) AS profit, + wr_return_amt AS return_amt, + wr_net_loss AS net_loss + FROM web_returns + LEFT OUTER JOIN web_sales + ON ( + wr_item_sk = ws_item_sk + AND wr_order_number = ws_order_number) ) salesreturns, + date_dim, + web_site + WHERE date_sk = d_date_sk + AND d_date BETWEEN cast('2002-08-22' AS date) AND ( + cast('2002-08-22' AS date) + INTERVAL '14' day) + AND wsr_web_site_sk = web_site_sk + GROUP BY web_site_id) +SELECT + channel , + id , + sum(sales) AS sales , + sum(returns1) AS returns1 , + sum(profit) AS profit +FROM ( + SELECT 'store channel' AS channel , + 'store' + || s_store_id AS id , + sales , + returns1 , + (profit - profit_loss) AS profit + FROM ssr + UNION ALL + SELECT 'catalog channel' AS channel , + 'catalog_page' + || cp_catalog_page_id AS id , + sales , + returns1 , + (profit - profit_loss) AS profit + FROM csr + UNION ALL + SELECT 'web channel' AS channel , + 'web_site' + || web_site_id AS id , + sales , + returns1 , + (profit - profit_loss) AS profit + FROM wsr ) x +GROUP BY rollup (channel, id) +ORDER BY channel , + id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query50.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query50.sql new file mode 100644 index 000000000..a4c108469 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query50.sql @@ -0,0 +1,71 @@ +-- start query 50 in stream 0 using template query50.tpl +SELECT s_store_name, + s_company_id, + s_street_number, + s_street_name, + s_street_type, + s_suite_number, + s_city, + s_county, + s_state, + s_zip, + Sum(CASE + WHEN ( sr_returned_date_sk - ss_sold_date_sk <= 30 ) THEN 1 + ELSE 0 + END) AS "30 days", + Sum(CASE + WHEN ( sr_returned_date_sk - ss_sold_date_sk > 30 ) + AND ( sr_returned_date_sk - ss_sold_date_sk <= 60 ) + THEN 1 + ELSE 0 + END) AS "31-60 days", + Sum(CASE + WHEN ( sr_returned_date_sk - ss_sold_date_sk > 60 ) + AND ( sr_returned_date_sk - ss_sold_date_sk <= 90 ) + THEN 1 + ELSE 0 + END) AS "61-90 days", + Sum(CASE + WHEN ( sr_returned_date_sk - ss_sold_date_sk > 90 ) + AND ( sr_returned_date_sk - ss_sold_date_sk <= 120 ) + THEN 1 + ELSE 0 + END) AS "91-120 days", + Sum(CASE + WHEN ( sr_returned_date_sk - ss_sold_date_sk > 120 ) THEN 1 + ELSE 0 + END) AS ">120 days" +FROM store_sales, + store_returns, + store, + date_dim d1, + date_dim d2 +WHERE d2.d_year = 2002 + AND d2.d_moy = 9 + AND ss_ticket_number = sr_ticket_number + AND ss_item_sk = sr_item_sk + AND ss_sold_date_sk = d1.d_date_sk + AND sr_returned_date_sk = d2.d_date_sk + AND ss_customer_sk = sr_customer_sk + AND ss_store_sk = s_store_sk +GROUP BY s_store_name, + s_company_id, + s_street_number, + s_street_name, + s_street_type, + s_suite_number, + s_city, + s_county, + s_state, + s_zip +ORDER BY s_store_name, + s_company_id, + s_street_number, + s_street_name, + s_street_type, + s_suite_number, + s_city, + s_county, + s_state, + s_zip +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query51.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query51.sql new file mode 100644 index 000000000..2c73e7d60 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query51.sql @@ -0,0 +1,54 @@ +-- start query 51 in stream 0 using template query51.tpl +WITH web_v1 AS +( + SELECT ws_item_sk item_sk, + d_date, + sum(Sum(ws_sales_price)) OVER (partition BY ws_item_sk ORDER BY d_date rows BETWEEN UNBOUNDED PRECEDING AND CURRENT row) cume_sales + FROM web_sales , + date_dim + WHERE ws_sold_date_sk=d_date_sk + AND d_month_seq BETWEEN 1192 AND 1192+11 + AND ws_item_sk IS NOT NULL + GROUP BY ws_item_sk, + d_date), store_v1 AS +( + SELECT ss_item_sk item_sk, + d_date, + sum(sum(ss_sales_price)) OVER (partition BY ss_item_sk ORDER BY d_date rows BETWEEN UNBOUNDED PRECEDING AND CURRENT row) cume_sales + FROM store_sales , + date_dim + WHERE ss_sold_date_sk=d_date_sk + AND d_month_seq BETWEEN 1192 AND 1192+11 + AND ss_item_sk IS NOT NULL + GROUP BY ss_item_sk, + d_date) +SELECT + * +FROM ( + SELECT item_sk , + d_date , + web_sales , + store_sales , + max(web_sales) OVER (partition BY item_sk ORDER BY d_date rows BETWEEN UNBOUNDED PRECEDING AND CURRENT row) web_cumulative , + max(store_sales) OVER (partition BY item_sk ORDER BY d_date rows BETWEEN UNBOUNDED PRECEDING AND CURRENT row) store_cumulative + FROM ( + SELECT + CASE + WHEN web.item_sk IS NOT NULL THEN web.item_sk + ELSE store.item_sk + END item_sk , + CASE + WHEN web.d_date IS NOT NULL THEN web.d_date + ELSE store.d_date + END d_date , + web.cume_sales web_sales , + store.cume_sales store_sales + FROM web_v1 web + FULL OUTER JOIN store_v1 store + ON ( + web.item_sk = store.item_sk + AND web.d_date = store.d_date) )x )y +WHERE web_cumulative > store_cumulative +ORDER BY item_sk , + d_date +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query52.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query52.sql new file mode 100644 index 000000000..065f3f169 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query52.sql @@ -0,0 +1,20 @@ +-- start query 52 in stream 0 using template query52.tpl +SELECT dt.d_year, + item.i_brand_id brand_id, + item.i_brand brand, + Sum(ss_ext_sales_price) ext_price +FROM date_dim dt, + store_sales, + item +WHERE dt.d_date_sk = store_sales.ss_sold_date_sk + AND store_sales.ss_item_sk = item.i_item_sk + AND item.i_manager_id = 1 + AND dt.d_moy = 11 + AND dt.d_year = 1999 +GROUP BY dt.d_year, + item.i_brand, + item.i_brand_id +ORDER BY dt.d_year, + ext_price DESC, + brand_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query53.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query53.sql new file mode 100644 index 000000000..4c0452a32 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query53.sql @@ -0,0 +1,46 @@ +-- start query 53 in stream 0 using template query53.tpl +SELECT * +FROM (SELECT i_manufact_id, + Sum(ss_sales_price) sum_sales, + Avg(Sum(ss_sales_price)) + OVER ( + partition BY i_manufact_id) avg_quarterly_sales + FROM item, + store_sales, + date_dim, + store + WHERE ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND ss_store_sk = s_store_sk + AND d_month_seq IN ( 1199, 1199 + 1, 1199 + 2, 1199 + 3, + 1199 + 4, 1199 + 5, 1199 + 6, 1199 + 7, + 1199 + 8, 1199 + 9, 1199 + 10, 1199 + 11 ) + AND ( ( i_category IN ( 'Books', 'Children', 'Electronics' ) + AND i_class IN ( 'personal', 'portable', 'reference', + 'self-help' ) + AND i_brand IN ( 'scholaramalgamalg #14', + 'scholaramalgamalg #7' + , + 'exportiunivamalg #9', + 'scholaramalgamalg #9' ) + ) + OR ( i_category IN ( 'Women', 'Music', 'Men' ) + AND i_class IN ( 'accessories', 'classical', + 'fragrances', + 'pants' ) + AND i_brand IN ( 'amalgimporto #1', + 'edu packscholar #1', + 'exportiimporto #1', + 'importoamalg #1' ) ) ) + GROUP BY i_manufact_id, + d_qoy) tmp1 +WHERE CASE + WHEN avg_quarterly_sales > 0 THEN Abs (sum_sales - avg_quarterly_sales) + / + avg_quarterly_sales + ELSE NULL + END > 0.1 +ORDER BY avg_quarterly_sales, + sum_sales, + i_manufact_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query54.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query54.sql new file mode 100644 index 000000000..b776c9590 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query54.sql @@ -0,0 +1,57 @@ +-- start query 54 in stream 0 using template query54.tpl +WITH my_customers + AS (SELECT DISTINCT c_customer_sk, + c_current_addr_sk + FROM (SELECT cs_sold_date_sk sold_date_sk, + cs_bill_customer_sk customer_sk, + cs_item_sk item_sk + FROM catalog_sales + UNION ALL + SELECT ws_sold_date_sk sold_date_sk, + ws_bill_customer_sk customer_sk, + ws_item_sk item_sk + FROM web_sales) cs_or_ws_sales, + item, + date_dim, + customer + WHERE sold_date_sk = d_date_sk + AND item_sk = i_item_sk + AND i_category = 'Sports' + AND i_class = 'fitness' + AND c_customer_sk = cs_or_ws_sales.customer_sk + AND d_moy = 5 + AND d_year = 2000), + my_revenue + AS (SELECT c_customer_sk, + Sum(ss_ext_sales_price) AS revenue + FROM my_customers, + store_sales, + customer_address, + store, + date_dim + WHERE c_current_addr_sk = ca_address_sk + AND ca_county = s_county + AND ca_state = s_state + AND ss_sold_date_sk = d_date_sk + AND c_customer_sk = ss_customer_sk + AND d_month_seq BETWEEN (SELECT DISTINCT d_month_seq + 1 + FROM date_dim + WHERE d_year = 2000 + AND d_moy = 5) AND + (SELECT DISTINCT + d_month_seq + 3 + FROM date_dim + WHERE d_year = 2000 + AND d_moy = 5) + GROUP BY c_customer_sk), + segments + AS (SELECT Cast(( revenue / 50 ) AS INT) AS segment + FROM my_revenue) +SELECT segment, + Count(*) AS num_customers, + segment * 50 AS segment_base +FROM segments +GROUP BY segment +ORDER BY segment, + num_customers +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query55.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query55.sql new file mode 100644 index 000000000..37ed2563f --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query55.sql @@ -0,0 +1,17 @@ +-- start query 55 in stream 0 using template query55.tpl +SELECT i_brand_id brand_id, + i_brand brand, + Sum(ss_ext_sales_price) ext_price +FROM date_dim, + store_sales, + item +WHERE d_date_sk = ss_sold_date_sk + AND ss_item_sk = i_item_sk + AND i_manager_id = 33 + AND d_moy = 12 + AND d_year = 1998 +GROUP BY i_brand, + i_brand_id +ORDER BY ext_price DESC, + i_brand_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query56.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query56.sql new file mode 100644 index 000000000..7d2ad0629 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query56.sql @@ -0,0 +1,68 @@ +-- start query 56 in stream 0 using template query56.tpl +WITH ss + AS (SELECT i_item_id, + Sum(ss_ext_sales_price) total_sales + FROM store_sales, + date_dim, + customer_address, + item + WHERE i_item_id IN (SELECT i_item_id + FROM item + WHERE i_color IN ( 'firebrick', 'rosy', 'white' ) + ) + AND ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 1998 + AND d_moy = 3 + AND ss_addr_sk = ca_address_sk + AND ca_gmt_offset = -6 + GROUP BY i_item_id), + cs + AS (SELECT i_item_id, + Sum(cs_ext_sales_price) total_sales + FROM catalog_sales, + date_dim, + customer_address, + item + WHERE i_item_id IN (SELECT i_item_id + FROM item + WHERE i_color IN ( 'firebrick', 'rosy', 'white' ) + ) + AND cs_item_sk = i_item_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 1998 + AND d_moy = 3 + AND cs_bill_addr_sk = ca_address_sk + AND ca_gmt_offset = -6 + GROUP BY i_item_id), + ws + AS (SELECT i_item_id, + Sum(ws_ext_sales_price) total_sales + FROM web_sales, + date_dim, + customer_address, + item + WHERE i_item_id IN (SELECT i_item_id + FROM item + WHERE i_color IN ( 'firebrick', 'rosy', 'white' ) + ) + AND ws_item_sk = i_item_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 1998 + AND d_moy = 3 + AND ws_bill_addr_sk = ca_address_sk + AND ca_gmt_offset = -6 + GROUP BY i_item_id) +SELECT i_item_id, + Sum(total_sales) total_sales +FROM (SELECT * + FROM ss + UNION ALL + SELECT * + FROM cs + UNION ALL + SELECT * + FROM ws) tmp1 +GROUP BY i_item_id +ORDER BY total_sales +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query57.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query57.sql new file mode 100644 index 000000000..5cb73bce4 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query57.sql @@ -0,0 +1,66 @@ +-- start query 57 in stream 0 using template query57.tpl +WITH v1 + AS (SELECT i_category, + i_brand, + cc_name, + d_year, + d_moy, + Sum(cs_sales_price) sum_sales + , + Avg(Sum(cs_sales_price)) + OVER ( + partition BY i_category, i_brand, cc_name, d_year) + avg_monthly_sales + , + Rank() + OVER ( + partition BY i_category, i_brand, cc_name + ORDER BY d_year, d_moy) rn + FROM item, + catalog_sales, + date_dim, + call_center + WHERE cs_item_sk = i_item_sk + AND cs_sold_date_sk = d_date_sk + AND cc_call_center_sk = cs_call_center_sk + AND ( d_year = 2000 + OR ( d_year = 2000 - 1 + AND d_moy = 12 ) + OR ( d_year = 2000 + 1 + AND d_moy = 1 ) ) + GROUP BY i_category, + i_brand, + cc_name, + d_year, + d_moy), + v2 + AS (SELECT v1.i_brand, + v1.d_year, + v1.avg_monthly_sales, + v1.sum_sales, + v1_lag.sum_sales psum, + v1_lead.sum_sales nsum + FROM v1, + v1 v1_lag, + v1 v1_lead + WHERE v1.i_category = v1_lag.i_category + AND v1.i_category = v1_lead.i_category + AND v1.i_brand = v1_lag.i_brand + AND v1.i_brand = v1_lead.i_brand + AND v1. cc_name = v1_lag. cc_name + AND v1. cc_name = v1_lead. cc_name + AND v1.rn = v1_lag.rn + 1 + AND v1.rn = v1_lead.rn - 1) +SELECT * +FROM v2 +WHERE d_year = 2000 + AND avg_monthly_sales > 0 + AND CASE + WHEN avg_monthly_sales > 0 THEN Abs(sum_sales - avg_monthly_sales) + / + avg_monthly_sales + ELSE NULL + END > 0.1 +ORDER BY sum_sales - avg_monthly_sales, + 3 +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query58.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query58.sql new file mode 100644 index 000000000..8611390de --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query58.sql @@ -0,0 +1,72 @@ +-- start query 58 in stream 0 using template query58.tpl +WITH ss_items + AS (SELECT i_item_id item_id, + Sum(ss_ext_sales_price) ss_item_rev + FROM store_sales, + item, + date_dim + WHERE ss_item_sk = i_item_sk + AND d_date IN (SELECT d_date + FROM date_dim + WHERE d_week_seq = (SELECT d_week_seq + FROM date_dim + WHERE d_date = '2002-02-25' + )) + AND ss_sold_date_sk = d_date_sk + GROUP BY i_item_id), + cs_items + AS (SELECT i_item_id item_id, + Sum(cs_ext_sales_price) cs_item_rev + FROM catalog_sales, + item, + date_dim + WHERE cs_item_sk = i_item_sk + AND d_date IN (SELECT d_date + FROM date_dim + WHERE d_week_seq = (SELECT d_week_seq + FROM date_dim + WHERE d_date = '2002-02-25' + )) + AND cs_sold_date_sk = d_date_sk + GROUP BY i_item_id), + ws_items + AS (SELECT i_item_id item_id, + Sum(ws_ext_sales_price) ws_item_rev + FROM web_sales, + item, + date_dim + WHERE ws_item_sk = i_item_sk + AND d_date IN (SELECT d_date + FROM date_dim + WHERE d_week_seq = (SELECT d_week_seq + FROM date_dim + WHERE d_date = '2002-02-25' + )) + AND ws_sold_date_sk = d_date_sk + GROUP BY i_item_id) +SELECT ss_items.item_id, + ss_item_rev, + ss_item_rev / ( ss_item_rev + cs_item_rev + ws_item_rev ) / 3 * + 100 ss_dev, + cs_item_rev, + cs_item_rev / ( ss_item_rev + cs_item_rev + ws_item_rev ) / 3 * + 100 cs_dev, + ws_item_rev, + ws_item_rev / ( ss_item_rev + cs_item_rev + ws_item_rev ) / 3 * + 100 ws_dev, + ( ss_item_rev + cs_item_rev + ws_item_rev ) / 3 + average +FROM ss_items, + cs_items, + ws_items +WHERE ss_items.item_id = cs_items.item_id + AND ss_items.item_id = ws_items.item_id + AND ss_item_rev BETWEEN 0.9 * cs_item_rev AND 1.1 * cs_item_rev + AND ss_item_rev BETWEEN 0.9 * ws_item_rev AND 1.1 * ws_item_rev + AND cs_item_rev BETWEEN 0.9 * ss_item_rev AND 1.1 * ss_item_rev + AND cs_item_rev BETWEEN 0.9 * ws_item_rev AND 1.1 * ws_item_rev + AND ws_item_rev BETWEEN 0.9 * ss_item_rev AND 1.1 * ss_item_rev + AND ws_item_rev BETWEEN 0.9 * cs_item_rev AND 1.1 * cs_item_rev +ORDER BY item_id, + ss_item_rev +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query59.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query59.sql new file mode 100644 index 000000000..3caa54048 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query59.sql @@ -0,0 +1,85 @@ +-- start query 59 in stream 0 using template query59.tpl +WITH wss + AS (SELECT d_week_seq, + ss_store_sk, + Sum(CASE + WHEN ( d_day_name = 'Sunday' ) THEN ss_sales_price + ELSE NULL + END) sun_sales, + Sum(CASE + WHEN ( d_day_name = 'Monday' ) THEN ss_sales_price + ELSE NULL + END) mon_sales, + Sum(CASE + WHEN ( d_day_name = 'Tuesday' ) THEN ss_sales_price + ELSE NULL + END) tue_sales, + Sum(CASE + WHEN ( d_day_name = 'Wednesday' ) THEN ss_sales_price + ELSE NULL + END) wed_sales, + Sum(CASE + WHEN ( d_day_name = 'Thursday' ) THEN ss_sales_price + ELSE NULL + END) thu_sales, + Sum(CASE + WHEN ( d_day_name = 'Friday' ) THEN ss_sales_price + ELSE NULL + END) fri_sales, + Sum(CASE + WHEN ( d_day_name = 'Saturday' ) THEN ss_sales_price + ELSE NULL + END) sat_sales + FROM store_sales, + date_dim + WHERE d_date_sk = ss_sold_date_sk + GROUP BY d_week_seq, + ss_store_sk) +SELECT s_store_name1, + s_store_id1, + d_week_seq1, + sun_sales1 / sun_sales2, + mon_sales1 / mon_sales2, + tue_sales1 / tue_sales2, + wed_sales1 / wed_sales2, + thu_sales1 / thu_sales2, + fri_sales1 / fri_sales2, + sat_sales1 / sat_sales2 +FROM (SELECT s_store_name s_store_name1, + wss.d_week_seq d_week_seq1, + s_store_id s_store_id1, + sun_sales sun_sales1, + mon_sales mon_sales1, + tue_sales tue_sales1, + wed_sales wed_sales1, + thu_sales thu_sales1, + fri_sales fri_sales1, + sat_sales sat_sales1 + FROM wss, + store, + date_dim d + WHERE d.d_week_seq = wss.d_week_seq + AND ss_store_sk = s_store_sk + AND d_month_seq BETWEEN 1196 AND 1196 + 11) y, + (SELECT s_store_name s_store_name2, + wss.d_week_seq d_week_seq2, + s_store_id s_store_id2, + sun_sales sun_sales2, + mon_sales mon_sales2, + tue_sales tue_sales2, + wed_sales wed_sales2, + thu_sales thu_sales2, + fri_sales fri_sales2, + sat_sales sat_sales2 + FROM wss, + store, + date_dim d + WHERE d.d_week_seq = wss.d_week_seq + AND ss_store_sk = s_store_sk + AND d_month_seq BETWEEN 1196 + 12 AND 1196 + 23) x +WHERE s_store_id1 = s_store_id2 + AND d_week_seq1 = d_week_seq2 - 52 +ORDER BY s_store_name1, + s_store_id1, + d_week_seq1 +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query6.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query6.sql new file mode 100644 index 000000000..1739ea575 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query6.sql @@ -0,0 +1,23 @@ +-- start query 6 in stream 0 using template query6.tpl +SELECT a.ca_state state, + Count(*) cnt +FROM customer_address a, + customer c, + store_sales s, + date_dim d, + item i +WHERE a.ca_address_sk = c.c_current_addr_sk + AND c.c_customer_sk = s.ss_customer_sk + AND s.ss_sold_date_sk = d.d_date_sk + AND s.ss_item_sk = i.i_item_sk + AND d.d_month_seq = (SELECT DISTINCT ( d_month_seq ) + FROM date_dim + WHERE d_year = 1998 + AND d_moy = 7) + AND i.i_current_price > 1.2 * (SELECT Avg(j.i_current_price) + FROM item j + WHERE j.i_category = i.i_category) +GROUP BY a.ca_state +HAVING Count(*) >= 10 +ORDER BY cnt +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query60.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query60.sql new file mode 100644 index 000000000..541108dea --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query60.sql @@ -0,0 +1,66 @@ +-- start query 60 in stream 0 using template query60.tpl +WITH ss + AS (SELECT i_item_id, + Sum(ss_ext_sales_price) total_sales + FROM store_sales, + date_dim, + customer_address, + item + WHERE i_item_id IN (SELECT i_item_id + FROM item + WHERE i_category IN ( 'Jewelry' )) + AND ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 8 + AND ss_addr_sk = ca_address_sk + AND ca_gmt_offset = -6 + GROUP BY i_item_id), + cs + AS (SELECT i_item_id, + Sum(cs_ext_sales_price) total_sales + FROM catalog_sales, + date_dim, + customer_address, + item + WHERE i_item_id IN (SELECT i_item_id + FROM item + WHERE i_category IN ( 'Jewelry' )) + AND cs_item_sk = i_item_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 8 + AND cs_bill_addr_sk = ca_address_sk + AND ca_gmt_offset = -6 + GROUP BY i_item_id), + ws + AS (SELECT i_item_id, + Sum(ws_ext_sales_price) total_sales + FROM web_sales, + date_dim, + customer_address, + item + WHERE i_item_id IN (SELECT i_item_id + FROM item + WHERE i_category IN ( 'Jewelry' )) + AND ws_item_sk = i_item_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 8 + AND ws_bill_addr_sk = ca_address_sk + AND ca_gmt_offset = -6 + GROUP BY i_item_id) +SELECT i_item_id, + Sum(total_sales) total_sales +FROM (SELECT * + FROM ss + UNION ALL + SELECT * + FROM cs + UNION ALL + SELECT * + FROM ws) tmp1 +GROUP BY i_item_id +ORDER BY i_item_id, + total_sales +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query61.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query61.sql new file mode 100644 index 000000000..d6d50753c --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query61.sql @@ -0,0 +1,47 @@ +-- start query 61 in stream 0 using template query61.tpl +SELECT promotions, + total, + Cast(promotions AS DECIMAL(15, 4)) / + Cast(total AS DECIMAL(15, 4)) * 100 +FROM (SELECT Sum(ss_ext_sales_price) promotions + FROM store_sales, + store, + promotion, + date_dim, + customer, + customer_address, + item + WHERE ss_sold_date_sk = d_date_sk + AND ss_store_sk = s_store_sk + AND ss_promo_sk = p_promo_sk + AND ss_customer_sk = c_customer_sk + AND ca_address_sk = c_current_addr_sk + AND ss_item_sk = i_item_sk + AND ca_gmt_offset = -7 + AND i_category = 'Books' + AND ( p_channel_dmail = 'Y' + OR p_channel_email = 'Y' + OR p_channel_tv = 'Y' ) + AND s_gmt_offset = -7 + AND d_year = 2001 + AND d_moy = 12) promotional_sales, + (SELECT Sum(ss_ext_sales_price) total + FROM store_sales, + store, + date_dim, + customer, + customer_address, + item + WHERE ss_sold_date_sk = d_date_sk + AND ss_store_sk = s_store_sk + AND ss_customer_sk = c_customer_sk + AND ca_address_sk = c_current_addr_sk + AND ss_item_sk = i_item_sk + AND ca_gmt_offset = -7 + AND i_category = 'Books' + AND s_gmt_offset = -7 + AND d_year = 2001 + AND d_moy = 12) all_sales +ORDER BY promotions, + total +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query62.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query62.sql new file mode 100644 index 000000000..54df88ed2 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query62.sql @@ -0,0 +1,45 @@ +-- start query 62 in stream 0 using template query62.tpl +SELECT Substr(w_warehouse_name, 1, 20), + sm_type, + web_name, + Sum(CASE + WHEN ( ws_ship_date_sk - ws_sold_date_sk <= 30 ) THEN 1 + ELSE 0 + END) AS "30 days", + Sum(CASE + WHEN ( ws_ship_date_sk - ws_sold_date_sk > 30 ) + AND ( ws_ship_date_sk - ws_sold_date_sk <= 60 ) THEN 1 + ELSE 0 + END) AS "31-60 days", + Sum(CASE + WHEN ( ws_ship_date_sk - ws_sold_date_sk > 60 ) + AND ( ws_ship_date_sk - ws_sold_date_sk <= 90 ) THEN 1 + ELSE 0 + END) AS "61-90 days", + Sum(CASE + WHEN ( ws_ship_date_sk - ws_sold_date_sk > 90 ) + AND ( ws_ship_date_sk - ws_sold_date_sk <= 120 ) THEN + 1 + ELSE 0 + END) AS "91-120 days", + Sum(CASE + WHEN ( ws_ship_date_sk - ws_sold_date_sk > 120 ) THEN 1 + ELSE 0 + END) AS ">120 days" +FROM web_sales, + warehouse, + ship_mode, + web_site, + date_dim +WHERE d_month_seq BETWEEN 1222 AND 1222 + 11 + AND ws_ship_date_sk = d_date_sk + AND ws_warehouse_sk = w_warehouse_sk + AND ws_ship_mode_sk = sm_ship_mode_sk + AND ws_web_site_sk = web_site_sk +GROUP BY Substr(w_warehouse_name, 1, 20), + sm_type, + web_name +ORDER BY Substr(w_warehouse_name, 1, 20), + sm_type, + web_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query63.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query63.sql new file mode 100644 index 000000000..8706bb492 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query63.sql @@ -0,0 +1,45 @@ +-- start query 63 in stream 0 using template query63.tpl +SELECT * +FROM (SELECT i_manager_id, + Sum(ss_sales_price) sum_sales, + Avg(Sum(ss_sales_price)) + OVER ( + partition BY i_manager_id) avg_monthly_sales + FROM item, + store_sales, + date_dim, + store + WHERE ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND ss_store_sk = s_store_sk + AND d_month_seq IN ( 1200, 1200 + 1, 1200 + 2, 1200 + 3, + 1200 + 4, 1200 + 5, 1200 + 6, 1200 + 7, + 1200 + 8, 1200 + 9, 1200 + 10, 1200 + 11 ) + AND ( ( i_category IN ( 'Books', 'Children', 'Electronics' ) + AND i_class IN ( 'personal', 'portable', 'reference', + 'self-help' ) + AND i_brand IN ( 'scholaramalgamalg #14', + 'scholaramalgamalg #7' + , + 'exportiunivamalg #9', + 'scholaramalgamalg #9' ) + ) + OR ( i_category IN ( 'Women', 'Music', 'Men' ) + AND i_class IN ( 'accessories', 'classical', + 'fragrances', + 'pants' ) + AND i_brand IN ( 'amalgimporto #1', + 'edu packscholar #1', + 'exportiimporto #1', + 'importoamalg #1' ) ) ) + GROUP BY i_manager_id, + d_moy) tmp1 +WHERE CASE + WHEN avg_monthly_sales > 0 THEN Abs (sum_sales - avg_monthly_sales) / + avg_monthly_sales + ELSE NULL + END > 0.1 +ORDER BY i_manager_id, + avg_monthly_sales, + sum_sales +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query64.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query64.sql new file mode 100644 index 000000000..0c5d1d3d8 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query64.sql @@ -0,0 +1,122 @@ +-- start query 64 in stream 0 using template query64.tpl +WITH cs_ui + AS (SELECT cs_item_sk, + Sum(cs_ext_list_price) AS sale, + Sum(cr_refunded_cash + cr_reversed_charge + + cr_store_credit) AS refund + FROM catalog_sales, + catalog_returns + WHERE cs_item_sk = cr_item_sk + AND cs_order_number = cr_order_number + GROUP BY cs_item_sk + HAVING Sum(cs_ext_list_price) > 2 * Sum( + cr_refunded_cash + cr_reversed_charge + + cr_store_credit)), + cross_sales + AS (SELECT i_product_name product_name, + i_item_sk item_sk, + s_store_name store_name, + s_zip store_zip, + ad1.ca_street_number b_street_number, + ad1.ca_street_name b_streen_name, + ad1.ca_city b_city, + ad1.ca_zip b_zip, + ad2.ca_street_number c_street_number, + ad2.ca_street_name c_street_name, + ad2.ca_city c_city, + ad2.ca_zip c_zip, + d1.d_year AS syear, + d2.d_year AS fsyear, + d3.d_year s2year, + Count(*) cnt, + Sum(ss_wholesale_cost) s1, + Sum(ss_list_price) s2, + Sum(ss_coupon_amt) s3 + FROM store_sales, + store_returns, + cs_ui, + date_dim d1, + date_dim d2, + date_dim d3, + store, + customer, + customer_demographics cd1, + customer_demographics cd2, + promotion, + household_demographics hd1, + household_demographics hd2, + customer_address ad1, + customer_address ad2, + income_band ib1, + income_band ib2, + item + WHERE ss_store_sk = s_store_sk + AND ss_sold_date_sk = d1.d_date_sk + AND ss_customer_sk = c_customer_sk + AND ss_cdemo_sk = cd1.cd_demo_sk + AND ss_hdemo_sk = hd1.hd_demo_sk + AND ss_addr_sk = ad1.ca_address_sk + AND ss_item_sk = i_item_sk + AND ss_item_sk = sr_item_sk + AND ss_ticket_number = sr_ticket_number + AND ss_item_sk = cs_ui.cs_item_sk + AND c_current_cdemo_sk = cd2.cd_demo_sk + AND c_current_hdemo_sk = hd2.hd_demo_sk + AND c_current_addr_sk = ad2.ca_address_sk + AND c_first_sales_date_sk = d2.d_date_sk + AND c_first_shipto_date_sk = d3.d_date_sk + AND ss_promo_sk = p_promo_sk + AND hd1.hd_income_band_sk = ib1.ib_income_band_sk + AND hd2.hd_income_band_sk = ib2.ib_income_band_sk + AND cd1.cd_marital_status <> cd2.cd_marital_status + AND i_color IN ( 'cyan', 'peach', 'blush', 'frosted', + 'powder', 'orange' ) + AND i_current_price BETWEEN 58 AND 58 + 10 + AND i_current_price BETWEEN 58 + 1 AND 58 + 15 + GROUP BY i_product_name, + i_item_sk, + s_store_name, + s_zip, + ad1.ca_street_number, + ad1.ca_street_name, + ad1.ca_city, + ad1.ca_zip, + ad2.ca_street_number, + ad2.ca_street_name, + ad2.ca_city, + ad2.ca_zip, + d1.d_year, + d2.d_year, + d3.d_year) +SELECT cs1.product_name, + cs1.store_name, + cs1.store_zip, + cs1.b_street_number, + cs1.b_streen_name, + cs1.b_city, + cs1.b_zip, + cs1.c_street_number, + cs1.c_street_name, + cs1.c_city, + cs1.c_zip, + cs1.syear, + cs1.cnt, + cs1.s1, + cs1.s2, + cs1.s3, + cs2.s1, + cs2.s2, + cs2.s3, + cs2.syear, + cs2.cnt +FROM cross_sales cs1, + cross_sales cs2 +WHERE cs1.item_sk = cs2.item_sk + AND cs1.syear = 2001 + AND cs2.syear = 2001 + 1 + AND cs2.cnt <= cs1.cnt + AND cs1.store_name = cs2.store_name + AND cs1.store_zip = cs2.store_zip +ORDER BY cs1.product_name, + cs1.store_name, + cs2.cnt; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query65.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query65.sql new file mode 100644 index 000000000..399f128c3 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query65.sql @@ -0,0 +1,37 @@ +-- start query 65 in stream 0 using template query65.tpl +SELECT s_store_name, + i_item_desc, + sc.revenue, + i_current_price, + i_wholesale_cost, + i_brand +FROM store, + item, + (SELECT ss_store_sk, + Avg(revenue) AS ave + FROM (SELECT ss_store_sk, + ss_item_sk, + Sum(ss_sales_price) AS revenue + FROM store_sales, + date_dim + WHERE ss_sold_date_sk = d_date_sk + AND d_month_seq BETWEEN 1199 AND 1199 + 11 + GROUP BY ss_store_sk, + ss_item_sk) sa + GROUP BY ss_store_sk) sb, + (SELECT ss_store_sk, + ss_item_sk, + Sum(ss_sales_price) AS revenue + FROM store_sales, + date_dim + WHERE ss_sold_date_sk = d_date_sk + AND d_month_seq BETWEEN 1199 AND 1199 + 11 + GROUP BY ss_store_sk, + ss_item_sk) sc +WHERE sb.ss_store_sk = sc.ss_store_sk + AND sc.revenue <= 0.1 * sb.ave + AND s_store_sk = sc.ss_store_sk + AND i_item_sk = sc.ss_item_sk +ORDER BY s_store_name, + i_item_desc +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query66.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query66.sql new file mode 100644 index 000000000..79446555f --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query66.sql @@ -0,0 +1,306 @@ +-- start query 66 in stream 0 using template query66.tpl +SELECT w_warehouse_name, + w_warehouse_sq_ft, + w_city, + w_county, + w_state, + w_country, + ship_carriers, + year1, + Sum(jan_sales) AS jan_sales, + Sum(feb_sales) AS feb_sales, + Sum(mar_sales) AS mar_sales, + Sum(apr_sales) AS apr_sales, + Sum(may_sales) AS may_sales, + Sum(jun_sales) AS jun_sales, + Sum(jul_sales) AS jul_sales, + Sum(aug_sales) AS aug_sales, + Sum(sep_sales) AS sep_sales, + Sum(oct_sales) AS oct_sales, + Sum(nov_sales) AS nov_sales, + Sum(dec_sales) AS dec_sales, + Sum(jan_sales / w_warehouse_sq_ft) AS jan_sales_per_sq_foot, + Sum(feb_sales / w_warehouse_sq_ft) AS feb_sales_per_sq_foot, + Sum(mar_sales / w_warehouse_sq_ft) AS mar_sales_per_sq_foot, + Sum(apr_sales / w_warehouse_sq_ft) AS apr_sales_per_sq_foot, + Sum(may_sales / w_warehouse_sq_ft) AS may_sales_per_sq_foot, + Sum(jun_sales / w_warehouse_sq_ft) AS jun_sales_per_sq_foot, + Sum(jul_sales / w_warehouse_sq_ft) AS jul_sales_per_sq_foot, + Sum(aug_sales / w_warehouse_sq_ft) AS aug_sales_per_sq_foot, + Sum(sep_sales / w_warehouse_sq_ft) AS sep_sales_per_sq_foot, + Sum(oct_sales / w_warehouse_sq_ft) AS oct_sales_per_sq_foot, + Sum(nov_sales / w_warehouse_sq_ft) AS nov_sales_per_sq_foot, + Sum(dec_sales / w_warehouse_sq_ft) AS dec_sales_per_sq_foot, + Sum(jan_net) AS jan_net, + Sum(feb_net) AS feb_net, + Sum(mar_net) AS mar_net, + Sum(apr_net) AS apr_net, + Sum(may_net) AS may_net, + Sum(jun_net) AS jun_net, + Sum(jul_net) AS jul_net, + Sum(aug_net) AS aug_net, + Sum(sep_net) AS sep_net, + Sum(oct_net) AS oct_net, + Sum(nov_net) AS nov_net, + Sum(dec_net) AS dec_net +FROM (SELECT w_warehouse_name, + w_warehouse_sq_ft, + w_city, + w_county, + w_state, + w_country, + 'ZOUROS' + || ',' + || 'ZHOU' AS ship_carriers, + d_year AS year1, + Sum(CASE + WHEN d_moy = 1 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS jan_sales, + Sum(CASE + WHEN d_moy = 2 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS feb_sales, + Sum(CASE + WHEN d_moy = 3 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS mar_sales, + Sum(CASE + WHEN d_moy = 4 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS apr_sales, + Sum(CASE + WHEN d_moy = 5 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS may_sales, + Sum(CASE + WHEN d_moy = 6 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS jun_sales, + Sum(CASE + WHEN d_moy = 7 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS jul_sales, + Sum(CASE + WHEN d_moy = 8 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS aug_sales, + Sum(CASE + WHEN d_moy = 9 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS sep_sales, + Sum(CASE + WHEN d_moy = 10 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS oct_sales, + Sum(CASE + WHEN d_moy = 11 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS nov_sales, + Sum(CASE + WHEN d_moy = 12 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS dec_sales, + Sum(CASE + WHEN d_moy = 1 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS jan_net, + Sum(CASE + WHEN d_moy = 2 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS feb_net, + Sum(CASE + WHEN d_moy = 3 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS mar_net, + Sum(CASE + WHEN d_moy = 4 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS apr_net, + Sum(CASE + WHEN d_moy = 5 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS may_net, + Sum(CASE + WHEN d_moy = 6 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS jun_net, + Sum(CASE + WHEN d_moy = 7 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS jul_net, + Sum(CASE + WHEN d_moy = 8 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS aug_net, + Sum(CASE + WHEN d_moy = 9 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS sep_net, + Sum(CASE + WHEN d_moy = 10 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS oct_net, + Sum(CASE + WHEN d_moy = 11 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS nov_net, + Sum(CASE + WHEN d_moy = 12 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS dec_net + FROM web_sales, + warehouse, + date_dim, + time_dim, + ship_mode + WHERE ws_warehouse_sk = w_warehouse_sk + AND ws_sold_date_sk = d_date_sk + AND ws_sold_time_sk = t_time_sk + AND ws_ship_mode_sk = sm_ship_mode_sk + AND d_year = 1998 + AND t_time BETWEEN 7249 AND 7249 + 28800 + AND sm_carrier IN ( 'ZOUROS', 'ZHOU' ) + GROUP BY w_warehouse_name, + w_warehouse_sq_ft, + w_city, + w_county, + w_state, + w_country, + d_year + UNION ALL + SELECT w_warehouse_name, + w_warehouse_sq_ft, + w_city, + w_county, + w_state, + w_country, + 'ZOUROS' + || ',' + || 'ZHOU' AS ship_carriers, + d_year AS year1, + Sum(CASE + WHEN d_moy = 1 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS jan_sales, + Sum(CASE + WHEN d_moy = 2 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS feb_sales, + Sum(CASE + WHEN d_moy = 3 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS mar_sales, + Sum(CASE + WHEN d_moy = 4 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS apr_sales, + Sum(CASE + WHEN d_moy = 5 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS may_sales, + Sum(CASE + WHEN d_moy = 6 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS jun_sales, + Sum(CASE + WHEN d_moy = 7 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS jul_sales, + Sum(CASE + WHEN d_moy = 8 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS aug_sales, + Sum(CASE + WHEN d_moy = 9 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS sep_sales, + Sum(CASE + WHEN d_moy = 10 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS oct_sales, + Sum(CASE + WHEN d_moy = 11 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS nov_sales, + Sum(CASE + WHEN d_moy = 12 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS dec_sales, + Sum(CASE + WHEN d_moy = 1 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS jan_net, + Sum(CASE + WHEN d_moy = 2 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS feb_net, + Sum(CASE + WHEN d_moy = 3 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS mar_net, + Sum(CASE + WHEN d_moy = 4 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS apr_net, + Sum(CASE + WHEN d_moy = 5 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS may_net, + Sum(CASE + WHEN d_moy = 6 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS jun_net, + Sum(CASE + WHEN d_moy = 7 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS jul_net, + Sum(CASE + WHEN d_moy = 8 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS aug_net, + Sum(CASE + WHEN d_moy = 9 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS sep_net, + Sum(CASE + WHEN d_moy = 10 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS oct_net, + Sum(CASE + WHEN d_moy = 11 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS nov_net, + Sum(CASE + WHEN d_moy = 12 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS dec_net + FROM catalog_sales, + warehouse, + date_dim, + time_dim, + ship_mode + WHERE cs_warehouse_sk = w_warehouse_sk + AND cs_sold_date_sk = d_date_sk + AND cs_sold_time_sk = t_time_sk + AND cs_ship_mode_sk = sm_ship_mode_sk + AND d_year = 1998 + AND t_time BETWEEN 7249 AND 7249 + 28800 + AND sm_carrier IN ( 'ZOUROS', 'ZHOU' ) + GROUP BY w_warehouse_name, + w_warehouse_sq_ft, + w_city, + w_county, + w_state, + w_country, + d_year) x +GROUP BY w_warehouse_name, + w_warehouse_sq_ft, + w_city, + w_county, + w_state, + w_country, + ship_carriers, + year1 +ORDER BY w_warehouse_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query67.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query67.sql new file mode 100644 index 000000000..18c39edb5 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query67.sql @@ -0,0 +1,43 @@ +-- start query 67 in stream 0 using template query67.tpl +select * +from (select i_category + ,i_class + ,i_brand + ,i_product_name + ,d_year + ,d_qoy + ,d_moy + ,s_store_id + ,sumsales + ,rank() over (partition by i_category order by sumsales desc) rk + from (select i_category + ,i_class + ,i_brand + ,i_product_name + ,d_year + ,d_qoy + ,d_moy + ,s_store_id + ,sum(coalesce(ss_sales_price*ss_quantity,0)) sumsales + from store_sales + ,date_dim + ,store + ,item + where ss_sold_date_sk=d_date_sk + and ss_item_sk=i_item_sk + and ss_store_sk = s_store_sk + and d_month_seq between 1181 and 1181+11 + group by rollup(i_category, i_class, i_brand, i_product_name, d_year, d_qoy, d_moy,s_store_id))dw1) dw2 +where rk <= 100 +order by i_category + ,i_class + ,i_brand + ,i_product_name + ,d_year + ,d_qoy + ,d_moy + ,s_store_id + ,sumsales + ,rk +limit 100 +; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query68.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query68.sql new file mode 100644 index 000000000..f943603f8 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query68.sql @@ -0,0 +1,41 @@ +-- start query 68 in stream 0 using template query68.tpl +SELECT c_last_name, + c_first_name, + ca_city, + bought_city, + ss_ticket_number, + extended_price, + extended_tax, + list_price +FROM (SELECT ss_ticket_number, + ss_customer_sk, + ca_city bought_city, + Sum(ss_ext_sales_price) extended_price, + Sum(ss_ext_list_price) list_price, + Sum(ss_ext_tax) extended_tax + FROM store_sales, + date_dim, + store, + household_demographics, + customer_address + WHERE store_sales.ss_sold_date_sk = date_dim.d_date_sk + AND store_sales.ss_store_sk = store.s_store_sk + AND store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk + AND store_sales.ss_addr_sk = customer_address.ca_address_sk + AND date_dim.d_dom BETWEEN 1 AND 2 + AND ( household_demographics.hd_dep_count = 8 + OR household_demographics.hd_vehicle_count = 3 ) + AND date_dim.d_year IN ( 1998, 1998 + 1, 1998 + 2 ) + AND store.s_city IN ( 'Fairview', 'Midway' ) + GROUP BY ss_ticket_number, + ss_customer_sk, + ss_addr_sk, + ca_city) dn, + customer, + customer_address current_addr +WHERE ss_customer_sk = c_customer_sk + AND customer.c_current_addr_sk = current_addr.ca_address_sk + AND current_addr.ca_city <> bought_city +ORDER BY c_last_name, + ss_ticket_number +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query69.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query69.sql new file mode 100644 index 000000000..e22094bd8 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query69.sql @@ -0,0 +1,46 @@ +SELECT cd_gender, + cd_marital_status, + cd_education_status, + Count(*) cnt1, + cd_purchase_estimate, + Count(*) cnt2, + cd_credit_rating, + Count(*) cnt3 +FROM customer c, + customer_address ca, + customer_demographics +WHERE c.c_current_addr_sk = ca.ca_address_sk + AND ca_state IN ( 'KS', 'AZ', 'NE' ) + AND cd_demo_sk = c.c_current_cdemo_sk + AND EXISTS (SELECT * + FROM store_sales, + date_dim + WHERE c.c_customer_sk = ss_customer_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 2004 + AND d_moy BETWEEN 3 AND 3 + 2) + AND ( NOT EXISTS (SELECT * + FROM web_sales, + date_dim + WHERE c.c_customer_sk = ws_bill_customer_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 2004 + AND d_moy BETWEEN 3 AND 3 + 2) + AND NOT EXISTS (SELECT * + FROM catalog_sales, + date_dim + WHERE c.c_customer_sk = cs_ship_customer_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 2004 + AND d_moy BETWEEN 3 AND 3 + 2) ) +GROUP BY cd_gender, + cd_marital_status, + cd_education_status, + cd_purchase_estimate, + cd_credit_rating +ORDER BY cd_gender, + cd_marital_status, + cd_education_status, + cd_purchase_estimate, + cd_credit_rating +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query7.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query7.sql new file mode 100644 index 000000000..6e28b46c8 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query7.sql @@ -0,0 +1,24 @@ +-- start query 7 in stream 0 using template query7.tpl +SELECT i_item_id, + Avg(ss_quantity) agg1, + Avg(ss_list_price) agg2, + Avg(ss_coupon_amt) agg3, + Avg(ss_sales_price) agg4 +FROM store_sales, + customer_demographics, + date_dim, + item, + promotion +WHERE ss_sold_date_sk = d_date_sk + AND ss_item_sk = i_item_sk + AND ss_cdemo_sk = cd_demo_sk + AND ss_promo_sk = p_promo_sk + AND cd_gender = 'F' + AND cd_marital_status = 'W' + AND cd_education_status = '2 yr Degree' + AND ( p_channel_email = 'N' + OR p_channel_event = 'N' ) + AND d_year = 1998 +GROUP BY i_item_id +ORDER BY i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query70.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query70.sql new file mode 100644 index 000000000..34c5e0656 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query70.sql @@ -0,0 +1,40 @@ +-- start query 70 in stream 0 using template query70.tpl +SELECT Sum(ss_net_profit) AS total_sum, + s_state, + s_county, + Grouping(s_state) + Grouping(s_county) AS lochierarchy, + Rank() + OVER ( + partition BY Grouping(s_state)+Grouping(s_county), CASE WHEN + Grouping( + s_county) = 0 THEN s_state END + ORDER BY Sum(ss_net_profit) DESC) AS rank_within_parent +FROM store_sales, + date_dim d1, + store +WHERE d1.d_month_seq BETWEEN 1200 AND 1200 + 11 + AND d1.d_date_sk = ss_sold_date_sk + AND s_store_sk = ss_store_sk + AND s_state IN (SELECT s_state + FROM (SELECT s_state AS + s_state, + Rank() + OVER ( + partition BY s_state + ORDER BY Sum(ss_net_profit) DESC) AS + ranking + FROM store_sales, + store, + date_dim + WHERE d_month_seq BETWEEN 1200 AND 1200 + 11 + AND d_date_sk = ss_sold_date_sk + AND s_store_sk = ss_store_sk + GROUP BY s_state) tmp1 + WHERE ranking <= 5) +GROUP BY rollup( s_state, s_county ) +ORDER BY lochierarchy DESC, + CASE + WHEN lochierarchy = 0 THEN s_state + END, + rank_within_parent +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query71.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query71.sql new file mode 100644 index 000000000..42075471e --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query71.sql @@ -0,0 +1,48 @@ +-- start query 71 in stream 0 using template query71.tpl +SELECT i_brand_id brand_id, + i_brand brand, + t_hour, + t_minute, + Sum(ext_price) ext_price +FROM item, + (SELECT ws_ext_sales_price AS ext_price, + ws_sold_date_sk AS sold_date_sk, + ws_item_sk AS sold_item_sk, + ws_sold_time_sk AS time_sk + FROM web_sales, + date_dim + WHERE d_date_sk = ws_sold_date_sk + AND d_moy = 11 + AND d_year = 2001 + UNION ALL + SELECT cs_ext_sales_price AS ext_price, + cs_sold_date_sk AS sold_date_sk, + cs_item_sk AS sold_item_sk, + cs_sold_time_sk AS time_sk + FROM catalog_sales, + date_dim + WHERE d_date_sk = cs_sold_date_sk + AND d_moy = 11 + AND d_year = 2001 + UNION ALL + SELECT ss_ext_sales_price AS ext_price, + ss_sold_date_sk AS sold_date_sk, + ss_item_sk AS sold_item_sk, + ss_sold_time_sk AS time_sk + FROM store_sales, + date_dim + WHERE d_date_sk = ss_sold_date_sk + AND d_moy = 11 + AND d_year = 2001) AS tmp, + time_dim +WHERE sold_item_sk = i_item_sk + AND i_manager_id = 1 + AND time_sk = t_time_sk + AND ( t_meal_time = 'breakfast' + OR t_meal_time = 'dinner' ) +GROUP BY i_brand, + i_brand_id, + t_hour, + t_minute +ORDER BY ext_price DESC, + i_brand_id; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query72.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query72.sql new file mode 100644 index 000000000..e0e082cb4 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query72.sql @@ -0,0 +1,49 @@ +-- start query 72 in stream 0 using template query72.tpl +SELECT i_item_desc, + w_warehouse_name, + d1.d_week_seq, + Sum(CASE + WHEN p_promo_sk IS NULL THEN 1 + ELSE 0 + END) no_promo, + Sum(CASE + WHEN p_promo_sk IS NOT NULL THEN 1 + ELSE 0 + END) promo, + Count(*) total_cnt +FROM catalog_sales + JOIN inventory + ON ( cs_item_sk = inv_item_sk ) + JOIN warehouse + ON ( w_warehouse_sk = inv_warehouse_sk ) + JOIN item + ON ( i_item_sk = cs_item_sk ) + JOIN customer_demographics + ON ( cs_bill_cdemo_sk = cd_demo_sk ) + JOIN household_demographics + ON ( cs_bill_hdemo_sk = hd_demo_sk ) + JOIN date_dim d1 + ON ( cs_sold_date_sk = d1.d_date_sk ) + JOIN date_dim d2 + ON ( inv_date_sk = d2.d_date_sk ) + JOIN date_dim d3 + ON ( cs_ship_date_sk = d3.d_date_sk ) + LEFT OUTER JOIN promotion + ON ( cs_promo_sk = p_promo_sk ) + LEFT OUTER JOIN catalog_returns + ON ( cr_item_sk = cs_item_sk + AND cr_order_number = cs_order_number ) +WHERE d1.d_week_seq = d2.d_week_seq + AND inv_quantity_on_hand < cs_quantity + AND d3.d_date > d1.d_date + INTERVAL '5' day + AND hd_buy_potential = '501-1000' + AND d1.d_year = 2002 + AND cd_marital_status = 'M' +GROUP BY i_item_desc, + w_warehouse_name, + d1.d_week_seq +ORDER BY total_cnt DESC, + i_item_desc, + w_warehouse_name, + d_week_seq +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query73.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query73.sql new file mode 100644 index 000000000..d56d682c3 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query73.sql @@ -0,0 +1,39 @@ +-- start query 73 in stream 0 using template query73.tpl +SELECT c_last_name, + c_first_name, + c_salutation, + c_preferred_cust_flag, + ss_ticket_number, + cnt +FROM (SELECT ss_ticket_number, + ss_customer_sk, + Count(*) cnt + FROM store_sales, + date_dim, + store, + household_demographics + WHERE store_sales.ss_sold_date_sk = date_dim.d_date_sk + AND store_sales.ss_store_sk = store.s_store_sk + AND store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk + AND date_dim.d_dom BETWEEN 1 AND 2 + AND ( household_demographics.hd_buy_potential = '>10000' + OR household_demographics.hd_buy_potential = '0-500' ) + AND household_demographics.hd_vehicle_count > 0 + AND CASE + WHEN household_demographics.hd_vehicle_count > 0 THEN + household_demographics.hd_dep_count / + household_demographics.hd_vehicle_count + ELSE NULL + END > 1 + AND date_dim.d_year IN ( 2000, 2000 + 1, 2000 + 2 ) + AND store.s_county IN ( 'Williamson County', 'Williamson County', + 'Williamson County', + 'Williamson County' + ) + GROUP BY ss_ticket_number, + ss_customer_sk) dj, + customer +WHERE ss_customer_sk = c_customer_sk + AND cnt BETWEEN 1 AND 5 +ORDER BY cnt DESC, + c_last_name ASC; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query74.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query74.sql new file mode 100644 index 000000000..f8d781f79 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query74.sql @@ -0,0 +1,69 @@ +-- start query 74 in stream 0 using template query74.tpl +WITH year_total + AS (SELECT c_customer_id customer_id, + c_first_name customer_first_name, + c_last_name customer_last_name, + d_year AS year1, + Sum(ss_net_paid) year_total, + 's' sale_type + FROM customer, + store_sales, + date_dim + WHERE c_customer_sk = ss_customer_sk + AND ss_sold_date_sk = d_date_sk + AND d_year IN ( 1999, 1999 + 1 ) + GROUP BY c_customer_id, + c_first_name, + c_last_name, + d_year + UNION ALL + SELECT c_customer_id customer_id, + c_first_name customer_first_name, + c_last_name customer_last_name, + d_year AS year1, + Sum(ws_net_paid) year_total, + 'w' sale_type + FROM customer, + web_sales, + date_dim + WHERE c_customer_sk = ws_bill_customer_sk + AND ws_sold_date_sk = d_date_sk + AND d_year IN ( 1999, 1999 + 1 ) + GROUP BY c_customer_id, + c_first_name, + c_last_name, + d_year) +SELECT t_s_secyear.customer_id, + t_s_secyear.customer_first_name, + t_s_secyear.customer_last_name +FROM year_total t_s_firstyear, + year_total t_s_secyear, + year_total t_w_firstyear, + year_total t_w_secyear +WHERE t_s_secyear.customer_id = t_s_firstyear.customer_id + AND t_s_firstyear.customer_id = t_w_secyear.customer_id + AND t_s_firstyear.customer_id = t_w_firstyear.customer_id + AND t_s_firstyear.sale_type = 's' + AND t_w_firstyear.sale_type = 'w' + AND t_s_secyear.sale_type = 's' + AND t_w_secyear.sale_type = 'w' + AND t_s_firstyear.year1 = 1999 + AND t_s_secyear.year1 = 1999 + 1 + AND t_w_firstyear.year1 = 1999 + AND t_w_secyear.year1 = 1999 + 1 + AND t_s_firstyear.year_total > 0 + AND t_w_firstyear.year_total > 0 + AND CASE + WHEN t_w_firstyear.year_total > 0 THEN t_w_secyear.year_total / + t_w_firstyear.year_total + ELSE NULL + END > CASE + WHEN t_s_firstyear.year_total > 0 THEN + t_s_secyear.year_total / + t_s_firstyear.year_total + ELSE NULL + END +ORDER BY 1, + 2, + 3 +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query75.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query75.sql new file mode 100644 index 000000000..25d1ccbd4 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query75.sql @@ -0,0 +1,93 @@ +-- start query 75 in stream 0 using template query75.tpl +WITH all_sales + AS (SELECT d_year, + i_brand_id, + i_class_id, + i_category_id, + i_manufact_id, + Sum(sales_cnt) AS sales_cnt, + Sum(sales_amt) AS sales_amt + FROM (SELECT d_year, + i_brand_id, + i_class_id, + i_category_id, + i_manufact_id, + cs_quantity - COALESCE(cr_return_quantity, 0) AS + sales_cnt, + cs_ext_sales_price - COALESCE(cr_return_amount, 0.0) AS + sales_amt + FROM catalog_sales + JOIN item + ON i_item_sk = cs_item_sk + JOIN date_dim + ON d_date_sk = cs_sold_date_sk + LEFT JOIN catalog_returns + ON ( cs_order_number = cr_order_number + AND cs_item_sk = cr_item_sk ) + WHERE i_category = 'Men' + UNION + SELECT d_year, + i_brand_id, + i_class_id, + i_category_id, + i_manufact_id, + ss_quantity - COALESCE(sr_return_quantity, 0) AS + sales_cnt, + ss_ext_sales_price - COALESCE(sr_return_amt, 0.0) AS + sales_amt + FROM store_sales + JOIN item + ON i_item_sk = ss_item_sk + JOIN date_dim + ON d_date_sk = ss_sold_date_sk + LEFT JOIN store_returns + ON ( ss_ticket_number = sr_ticket_number + AND ss_item_sk = sr_item_sk ) + WHERE i_category = 'Men' + UNION + SELECT d_year, + i_brand_id, + i_class_id, + i_category_id, + i_manufact_id, + ws_quantity - COALESCE(wr_return_quantity, 0) AS + sales_cnt, + ws_ext_sales_price - COALESCE(wr_return_amt, 0.0) AS + sales_amt + FROM web_sales + JOIN item + ON i_item_sk = ws_item_sk + JOIN date_dim + ON d_date_sk = ws_sold_date_sk + LEFT JOIN web_returns + ON ( ws_order_number = wr_order_number + AND ws_item_sk = wr_item_sk ) + WHERE i_category = 'Men') sales_detail + GROUP BY d_year, + i_brand_id, + i_class_id, + i_category_id, + i_manufact_id) +SELECT prev_yr.d_year AS prev_year, + curr_yr.d_year AS year1, + curr_yr.i_brand_id, + curr_yr.i_class_id, + curr_yr.i_category_id, + curr_yr.i_manufact_id, + prev_yr.sales_cnt AS prev_yr_cnt, + curr_yr.sales_cnt AS curr_yr_cnt, + curr_yr.sales_cnt - prev_yr.sales_cnt AS sales_cnt_diff, + curr_yr.sales_amt - prev_yr.sales_amt AS sales_amt_diff +FROM all_sales curr_yr, + all_sales prev_yr +WHERE curr_yr.i_brand_id = prev_yr.i_brand_id + AND curr_yr.i_class_id = prev_yr.i_class_id + AND curr_yr.i_category_id = prev_yr.i_category_id + AND curr_yr.i_manufact_id = prev_yr.i_manufact_id + AND curr_yr.d_year = 2002 + AND prev_yr.d_year = 2002 - 1 + AND Cast(curr_yr.sales_cnt AS DECIMAL(17, 2)) / Cast(prev_yr.sales_cnt AS + DECIMAL(17, 2)) + < 0.9 +ORDER BY sales_cnt_diff +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query76.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query76.sql new file mode 100644 index 000000000..f298aa015 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query76.sql @@ -0,0 +1,57 @@ +-- start query 76 in stream 0 using template query76.tpl +SELECT channel, + col_name, + d_year, + d_qoy, + i_category, + Count(*) sales_cnt, + Sum(ext_sales_price) sales_amt +FROM (SELECT 'store' AS channel, + 'ss_hdemo_sk' col_name, + d_year, + d_qoy, + i_category, + ss_ext_sales_price ext_sales_price + FROM store_sales, + item, + date_dim + WHERE ss_hdemo_sk IS NULL + AND ss_sold_date_sk = d_date_sk + AND ss_item_sk = i_item_sk + UNION ALL + SELECT 'web' AS channel, + 'ws_ship_hdemo_sk' col_name, + d_year, + d_qoy, + i_category, + ws_ext_sales_price ext_sales_price + FROM web_sales, + item, + date_dim + WHERE ws_ship_hdemo_sk IS NULL + AND ws_sold_date_sk = d_date_sk + AND ws_item_sk = i_item_sk + UNION ALL + SELECT 'catalog' AS channel, + 'cs_warehouse_sk' col_name, + d_year, + d_qoy, + i_category, + cs_ext_sales_price ext_sales_price + FROM catalog_sales, + item, + date_dim + WHERE cs_warehouse_sk IS NULL + AND cs_sold_date_sk = d_date_sk + AND cs_item_sk = i_item_sk) foo +GROUP BY channel, + col_name, + d_year, + d_qoy, + i_category +ORDER BY channel, + col_name, + d_year, + d_qoy, + i_category +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query77.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query77.sql new file mode 100644 index 000000000..ebcea2b12 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query77.sql @@ -0,0 +1,107 @@ + +-- start query 77 in stream 0 using template query77.tpl +WITH ss AS +( + SELECT s_store_sk, + Sum(ss_ext_sales_price) AS sales, + Sum(ss_net_profit) AS profit + FROM store_sales, + date_dim, + store + WHERE ss_sold_date_sk = d_date_sk + AND d_date BETWEEN Cast('2001-08-16' AS DATE) AND ( + Cast('2001-08-16' AS DATE) + INTERVAL '30' day) + AND ss_store_sk = s_store_sk + GROUP BY s_store_sk) , sr AS +( + SELECT s_store_sk, + sum(sr_return_amt) AS returns1, + sum(sr_net_loss) AS profit_loss + FROM store_returns, + date_dim, + store + WHERE sr_returned_date_sk = d_date_sk + AND d_date BETWEEN cast('2001-08-16' AS date) AND ( + cast('2001-08-16' AS date) + INTERVAL '30' day) + AND sr_store_sk = s_store_sk + GROUP BY s_store_sk), cs AS +( + SELECT cs_call_center_sk, + sum(cs_ext_sales_price) AS sales, + sum(cs_net_profit) AS profit + FROM catalog_sales, + date_dim + WHERE cs_sold_date_sk = d_date_sk + AND d_date BETWEEN cast('2001-08-16' AS date) AND ( + cast('2001-08-16' AS date) + INTERVAL '30' day) + GROUP BY cs_call_center_sk ), cr AS +( + SELECT cr_call_center_sk, + sum(cr_return_amount) AS returns1, + sum(cr_net_loss) AS profit_loss + FROM catalog_returns, + date_dim + WHERE cr_returned_date_sk = d_date_sk + AND d_date BETWEEN cast('2001-08-16' AS date) AND ( + cast('2001-08-16' AS date) + INTERVAL '30' day) + GROUP BY cr_call_center_sk ), ws AS +( + SELECT wp_web_page_sk, + sum(ws_ext_sales_price) AS sales, + sum(ws_net_profit) AS profit + FROM web_sales, + date_dim, + web_page + WHERE ws_sold_date_sk = d_date_sk + AND d_date BETWEEN cast('2001-08-16' AS date) AND ( + cast('2001-08-16' AS date) + INTERVAL '30' day) + AND ws_web_page_sk = wp_web_page_sk + GROUP BY wp_web_page_sk), wr AS +( + SELECT wp_web_page_sk, + sum(wr_return_amt) AS returns1, + sum(wr_net_loss) AS profit_loss + FROM web_returns, + date_dim, + web_page + WHERE wr_returned_date_sk = d_date_sk + AND d_date BETWEEN cast('2001-08-16' AS date) AND ( + cast('2001-08-16' AS date) + INTERVAL '30' day) + AND wr_web_page_sk = wp_web_page_sk + GROUP BY wp_web_page_sk) +SELECT + channel , + id , + sum(sales) AS sales , + sum(returns1) AS returns1 , + sum(profit) AS profit +FROM ( + SELECT 'store channel' AS channel , + ss.s_store_sk AS id , + sales , + COALESCE(returns1, 0) AS returns1 , + (profit - COALESCE(profit_loss,0)) AS profit + FROM ss + LEFT JOIN sr + ON ss.s_store_sk = sr.s_store_sk + UNION ALL + SELECT 'catalog channel' AS channel , + cs_call_center_sk AS id , + sales , + returns1 , + (profit - profit_loss) AS profit + FROM cs , + cr + UNION ALL + SELECT 'web channel' AS channel , + ws.wp_web_page_sk AS id , + sales , + COALESCE(returns1, 0) returns1 , + (profit - COALESCE(profit_loss,0)) AS profit + FROM ws + LEFT JOIN wr + ON ws.wp_web_page_sk = wr.wp_web_page_sk ) x +GROUP BY rollup (channel, id) +ORDER BY channel , + id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query78.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query78.sql new file mode 100644 index 000000000..b57b52389 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query78.sql @@ -0,0 +1,86 @@ +-- start query 78 in stream 0 using template query78.tpl +WITH ws + AS (SELECT d_year AS ws_sold_year, + ws_item_sk, + ws_bill_customer_sk ws_customer_sk, + Sum(ws_quantity) ws_qty, + Sum(ws_wholesale_cost) ws_wc, + Sum(ws_sales_price) ws_sp + FROM web_sales + LEFT JOIN web_returns + ON wr_order_number = ws_order_number + AND ws_item_sk = wr_item_sk + JOIN date_dim + ON ws_sold_date_sk = d_date_sk + WHERE wr_order_number IS NULL + GROUP BY d_year, + ws_item_sk, + ws_bill_customer_sk), + cs + AS (SELECT d_year AS cs_sold_year, + cs_item_sk, + cs_bill_customer_sk cs_customer_sk, + Sum(cs_quantity) cs_qty, + Sum(cs_wholesale_cost) cs_wc, + Sum(cs_sales_price) cs_sp + FROM catalog_sales + LEFT JOIN catalog_returns + ON cr_order_number = cs_order_number + AND cs_item_sk = cr_item_sk + JOIN date_dim + ON cs_sold_date_sk = d_date_sk + WHERE cr_order_number IS NULL + GROUP BY d_year, + cs_item_sk, + cs_bill_customer_sk), + ss + AS (SELECT d_year AS ss_sold_year, + ss_item_sk, + ss_customer_sk, + Sum(ss_quantity) ss_qty, + Sum(ss_wholesale_cost) ss_wc, + Sum(ss_sales_price) ss_sp + FROM store_sales + LEFT JOIN store_returns + ON sr_ticket_number = ss_ticket_number + AND ss_item_sk = sr_item_sk + JOIN date_dim + ON ss_sold_date_sk = d_date_sk + WHERE sr_ticket_number IS NULL + GROUP BY d_year, + ss_item_sk, + ss_customer_sk) +SELECT ss_item_sk, + Round(ss_qty / ( COALESCE(ws_qty + cs_qty, 1) ), 2) ratio, + ss_qty store_qty, + ss_wc + store_wholesale_cost, + ss_sp + store_sales_price, + COALESCE(ws_qty, 0) + COALESCE(cs_qty, 0) + other_chan_qty, + COALESCE(ws_wc, 0) + COALESCE(cs_wc, 0) + other_chan_wholesale_cost, + COALESCE(ws_sp, 0) + COALESCE(cs_sp, 0) + other_chan_sales_price +FROM ss + LEFT JOIN ws + ON ( ws_sold_year = ss_sold_year + AND ws_item_sk = ss_item_sk + AND ws_customer_sk = ss_customer_sk ) + LEFT JOIN cs + ON ( cs_sold_year = ss_sold_year + AND cs_item_sk = cs_item_sk + AND cs_customer_sk = ss_customer_sk ) +WHERE COALESCE(ws_qty, 0) > 0 + AND COALESCE(cs_qty, 0) > 0 + AND ss_sold_year = 1999 +ORDER BY ss_item_sk, + ss_qty DESC, + ss_wc DESC, + ss_sp DESC, + other_chan_qty, + other_chan_wholesale_cost, + other_chan_sales_price, + Round(ss_qty / ( COALESCE(ws_qty + cs_qty, 1) ), 2) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query79.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query79.sql new file mode 100644 index 000000000..7e8f52d09 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query79.sql @@ -0,0 +1,35 @@ +-- start query 79 in stream 0 using template query79.tpl +SELECT c_last_name, + c_first_name, + Substr(s_city, 1, 30), + ss_ticket_number, + amt, + profit +FROM (SELECT ss_ticket_number, + ss_customer_sk, + store.s_city, + Sum(ss_coupon_amt) amt, + Sum(ss_net_profit) profit + FROM store_sales, + date_dim, + store, + household_demographics + WHERE store_sales.ss_sold_date_sk = date_dim.d_date_sk + AND store_sales.ss_store_sk = store.s_store_sk + AND store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk + AND ( household_demographics.hd_dep_count = 8 + OR household_demographics.hd_vehicle_count > 4 ) + AND date_dim.d_dow = 1 + AND date_dim.d_year IN ( 2000, 2000 + 1, 2000 + 2 ) + AND store.s_number_employees BETWEEN 200 AND 295 + GROUP BY ss_ticket_number, + ss_customer_sk, + ss_addr_sk, + store.s_city) ms, + customer +WHERE ss_customer_sk = c_customer_sk +ORDER BY c_last_name, + c_first_name, + Substr(s_city, 1, 30), + profit +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query8.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query8.sql new file mode 100644 index 000000000..8d98a76a4 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query8.sql @@ -0,0 +1,227 @@ +-- start query 8 in stream 0 using template query8.tpl +SELECT s_store_name, + Sum(ss_net_profit) +FROM store_sales, + date_dim, + store, + (SELECT ca_zip + FROM (SELECT Substr(ca_zip, 1, 5) ca_zip + FROM customer_address + WHERE Substr(ca_zip, 1, 5) IN ( '67436', '26121', '38443', + '63157', + '68856', '19485', '86425', + '26741', + '70991', '60899', '63573', + '47556', + '56193', '93314', '87827', + '62017', + '85067', '95390', '48091', + '10261', + '81845', '41790', '42853', + '24675', + '12840', '60065', '84430', + '57451', + '24021', '91735', '75335', + '71935', + '34482', '56943', '70695', + '52147', + '56251', '28411', '86653', + '23005', + '22478', '29031', '34398', + '15365', + '42460', '33337', '59433', + '73943', + '72477', '74081', '74430', + '64605', + '39006', '11226', '49057', + '97308', + '42663', '18187', '19768', + '43454', + '32147', '76637', '51975', + '11181', + '45630', '33129', '45995', + '64386', + '55522', '26697', '20963', + '35154', + '64587', '49752', '66386', + '30586', + '59286', '13177', '66646', + '84195', + '74316', '36853', '32927', + '12469', + '11904', '36269', '17724', + '55346', + '12595', '53988', '65439', + '28015', + '63268', '73590', '29216', + '82575', + '69267', '13805', '91678', + '79460', + '94152', '14961', '15419', + '48277', + '62588', '55493', '28360', + '14152', + '55225', '18007', '53705', + '56573', + '80245', '71769', '57348', + '36845', + '13039', '17270', '22363', + '83474', + '25294', '43269', '77666', + '15488', + '99146', '64441', '43338', + '38736', + '62754', '48556', '86057', + '23090', + '38114', '66061', '18910', + '84385', + '23600', '19975', '27883', + '65719', + '19933', '32085', '49731', + '40473', + '27190', '46192', '23949', + '44738', + '12436', '64794', '68741', + '15333', + '24282', '49085', '31844', + '71156', + '48441', '17100', '98207', + '44982', + '20277', '71496', '96299', + '37583', + '22206', '89174', '30589', + '61924', + '53079', '10976', '13104', + '42794', + '54772', '15809', '56434', + '39975', + '13874', '30753', '77598', + '78229', + '59478', '12345', '55547', + '57422', + '42600', '79444', '29074', + '29752', + '21676', '32096', '43044', + '39383', + '37296', '36295', '63077', + '16572', + '31275', '18701', '40197', + '48242', + '27219', '49865', '84175', + '30446', + '25165', '13807', '72142', + '70499', + '70464', '71429', '18111', + '70857', + '29545', '36425', '52706', + '36194', + '42963', '75068', '47921', + '74763', + '90990', '89456', '62073', + '88397', + '73963', '75885', '62657', + '12530', + '81146', '57434', '25099', + '41429', + '98441', '48713', '52552', + '31667', + '14072', '13903', '44709', + '85429', + '58017', '38295', '44875', + '73541', + '30091', '12707', '23762', + '62258', + '33247', '78722', '77431', + '14510', + '35656', '72428', '92082', + '35267', + '43759', '24354', '90952', + '11512', + '21242', '22579', '56114', + '32339', + '52282', '41791', '24484', + '95020', + '28408', '99710', '11899', + '43344', + '72915', '27644', '62708', + '74479', + '17177', '32619', '12351', + '91339', + '31169', '57081', '53522', + '16712', + '34419', '71779', '44187', + '46206', + '96099', '61910', '53664', + '12295', + '31837', '33096', '10813', + '63048', + '31732', '79118', '73084', + '72783', + '84952', '46965', '77956', + '39815', + '32311', '75329', '48156', + '30826', + '49661', '13736', '92076', + '74865', + '88149', '92397', '52777', + '68453', + '32012', '21222', '52721', + '24626', + '18210', '42177', '91791', + '75251', + '82075', '44372', '45542', + '20609', + '60115', '17362', '22750', + '90434', + '31852', '54071', '33762', + '14705', + '40718', '56433', '30996', + '40657', + '49056', '23585', '66455', + '41021', + '74736', '72151', '37007', + '21729', + '60177', '84558', '59027', + '93855', + '60022', '86443', '19541', + '86886', + '30532', '39062', '48532', + '34713', + '52077', '22564', '64638', + '15273', + '31677', '36138', '62367', + '60261', + '80213', '42818', '25113', + '72378', + '69802', '69096', '55443', + '28820', + '13848', '78258', '37490', + '30556', + '77380', '28447', '44550', + '26791', + '70609', '82182', '33306', + '43224', + '22322', '86959', '68519', + '14308', + '46501', '81131', '34056', + '61991', + '19896', '87804', '65774', + '92564' ) + INTERSECT + SELECT ca_zip + FROM (SELECT Substr(ca_zip, 1, 5) ca_zip, + Count(*) cnt + FROM customer_address, + customer + WHERE ca_address_sk = c_current_addr_sk + AND c_preferred_cust_flag = 'Y' + GROUP BY ca_zip + HAVING Count(*) > 10)A1)A2) V1 +WHERE ss_store_sk = s_store_sk + AND ss_sold_date_sk = d_date_sk + AND d_qoy = 2 + AND d_year = 2000 + AND ( Substr(s_zip, 1, 2) = Substr(V1.ca_zip, 1, 2) ) +GROUP BY s_store_name +ORDER BY s_store_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query80.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query80.sql new file mode 100644 index 000000000..4fcb9cb2f --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query80.sql @@ -0,0 +1,105 @@ +-- start query 80 in stream 0 using template query80.tpl +WITH ssr AS +( + SELECT s_store_id AS store_id, + Sum(ss_ext_sales_price) AS sales, + Sum(COALESCE(sr_return_amt, 0)) AS returns1, + Sum(ss_net_profit - COALESCE(sr_net_loss, 0)) AS profit + FROM store_sales + LEFT OUTER JOIN store_returns + ON ( + ss_item_sk = sr_item_sk + AND ss_ticket_number = sr_ticket_number), + date_dim, + store, + item, + promotion + WHERE ss_sold_date_sk = d_date_sk + AND d_date BETWEEN Cast('2000-08-26' AS DATE) AND ( + Cast('2000-08-26' AS DATE) + INTERVAL '30' day) + AND ss_store_sk = s_store_sk + AND ss_item_sk = i_item_sk + AND i_current_price > 50 + AND ss_promo_sk = p_promo_sk + AND p_channel_tv = 'N' + GROUP BY s_store_id) , csr AS +( + SELECT cp_catalog_page_id AS catalog_page_id, + sum(cs_ext_sales_price) AS sales, + sum(COALESCE(cr_return_amount, 0)) AS returns1, + sum(cs_net_profit - COALESCE(cr_net_loss, 0)) AS profit + FROM catalog_sales + LEFT OUTER JOIN catalog_returns + ON ( + cs_item_sk = cr_item_sk + AND cs_order_number = cr_order_number), + date_dim, + catalog_page, + item, + promotion + WHERE cs_sold_date_sk = d_date_sk + AND d_date BETWEEN cast('2000-08-26' AS date) AND ( + cast('2000-08-26' AS date) + INTERVAL '30' day) + AND cs_catalog_page_sk = cp_catalog_page_sk + AND cs_item_sk = i_item_sk + AND i_current_price > 50 + AND cs_promo_sk = p_promo_sk + AND p_channel_tv = 'N' + GROUP BY cp_catalog_page_id) , wsr AS +( + SELECT web_site_id, + sum(ws_ext_sales_price) AS sales, + sum(COALESCE(wr_return_amt, 0)) AS returns1, + sum(ws_net_profit - COALESCE(wr_net_loss, 0)) AS profit + FROM web_sales + LEFT OUTER JOIN web_returns + ON ( + ws_item_sk = wr_item_sk + AND ws_order_number = wr_order_number), + date_dim, + web_site, + item, + promotion + WHERE ws_sold_date_sk = d_date_sk + AND d_date BETWEEN cast('2000-08-26' AS date) AND ( + cast('2000-08-26' AS date) + INTERVAL '30' day) + AND ws_web_site_sk = web_site_sk + AND ws_item_sk = i_item_sk + AND i_current_price > 50 + AND ws_promo_sk = p_promo_sk + AND p_channel_tv = 'N' + GROUP BY web_site_id) +SELECT + channel , + id , + sum(sales) AS sales , + sum(returns1) AS returns1 , + sum(profit) AS profit +FROM ( + SELECT 'store channel' AS channel , + 'store' + || store_id AS id , + sales , + returns1 , + profit + FROM ssr + UNION ALL + SELECT 'catalog channel' AS channel , + 'catalog_page' + || catalog_page_id AS id , + sales , + returns1 , + profit + FROM csr + UNION ALL + SELECT 'web channel' AS channel , + 'web_site' + || web_site_id AS id , + sales , + returns1 , + profit + FROM wsr ) x +GROUP BY rollup (channel, id) +ORDER BY channel , + id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query81.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query81.sql new file mode 100644 index 000000000..9be95c5d2 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query81.sql @@ -0,0 +1,56 @@ + +-- start query 81 in stream 0 using template query81.tpl +WITH customer_total_return + AS (SELECT cr_returning_customer_sk AS ctr_customer_sk, + ca_state AS ctr_state, + Sum(cr_return_amt_inc_tax) AS ctr_total_return + FROM catalog_returns, + date_dim, + customer_address + WHERE cr_returned_date_sk = d_date_sk + AND d_year = 1999 + AND cr_returning_addr_sk = ca_address_sk + GROUP BY cr_returning_customer_sk, + ca_state) +SELECT c_customer_id, + c_salutation, + c_first_name, + c_last_name, + ca_street_number, + ca_street_name, + ca_street_type, + ca_suite_number, + ca_city, + ca_county, + ca_state, + ca_zip, + ca_country, + ca_gmt_offset, + ca_location_type, + ctr_total_return +FROM customer_total_return ctr1, + customer_address, + customer +WHERE ctr1.ctr_total_return > (SELECT Avg(ctr_total_return) * 1.2 + FROM customer_total_return ctr2 + WHERE ctr1.ctr_state = ctr2.ctr_state) + AND ca_address_sk = c_current_addr_sk + AND ca_state = 'TX' + AND ctr1.ctr_customer_sk = c_customer_sk +ORDER BY c_customer_id, + c_salutation, + c_first_name, + c_last_name, + ca_street_number, + ca_street_name, + ca_street_type, + ca_suite_number, + ca_city, + ca_county, + ca_state, + ca_zip, + ca_country, + ca_gmt_offset, + ca_location_type, + ctr_total_return +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query82.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query82.sql new file mode 100644 index 000000000..27295a5fb --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query82.sql @@ -0,0 +1,23 @@ + +-- start query 82 in stream 0 using template query82.tpl +SELECT + i_item_id , + i_item_desc , + i_current_price +FROM item, + inventory, + date_dim, + store_sales +WHERE i_current_price BETWEEN 63 AND 63+30 +AND inv_item_sk = i_item_sk +AND d_date_sk=inv_date_sk +AND d_date BETWEEN Cast('1998-04-27' AS DATE) AND ( + Cast('1998-04-27' AS DATE) + INTERVAL '60' day) +AND i_manufact_id IN (57,293,427,320) +AND inv_quantity_on_hand BETWEEN 100 AND 500 +AND ss_item_sk = i_item_sk +GROUP BY i_item_id, + i_item_desc, + i_current_price +ORDER BY i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query83.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query83.sql new file mode 100644 index 000000000..b5c4378fa --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query83.sql @@ -0,0 +1,75 @@ +-- start query 83 in stream 0 using template query83.tpl +WITH sr_items + AS (SELECT i_item_id item_id, + Sum(sr_return_quantity) sr_item_qty + FROM store_returns, + item, + date_dim + WHERE sr_item_sk = i_item_sk + AND d_date IN (SELECT d_date + FROM date_dim + WHERE d_week_seq IN (SELECT d_week_seq + FROM date_dim + WHERE + d_date IN ( '1999-06-30', + '1999-08-28', + '1999-11-18' + ))) + AND sr_returned_date_sk = d_date_sk + GROUP BY i_item_id), + cr_items + AS (SELECT i_item_id item_id, + Sum(cr_return_quantity) cr_item_qty + FROM catalog_returns, + item, + date_dim + WHERE cr_item_sk = i_item_sk + AND d_date IN (SELECT d_date + FROM date_dim + WHERE d_week_seq IN (SELECT d_week_seq + FROM date_dim + WHERE + d_date IN ( '1999-06-30', + '1999-08-28', + '1999-11-18' + ))) + AND cr_returned_date_sk = d_date_sk + GROUP BY i_item_id), + wr_items + AS (SELECT i_item_id item_id, + Sum(wr_return_quantity) wr_item_qty + FROM web_returns, + item, + date_dim + WHERE wr_item_sk = i_item_sk + AND d_date IN (SELECT d_date + FROM date_dim + WHERE d_week_seq IN (SELECT d_week_seq + FROM date_dim + WHERE + d_date IN ( '1999-06-30', + '1999-08-28', + '1999-11-18' + ))) + AND wr_returned_date_sk = d_date_sk + GROUP BY i_item_id) +SELECT sr_items.item_id, + sr_item_qty, + sr_item_qty / ( sr_item_qty + cr_item_qty + wr_item_qty ) / 3.0 * + 100 sr_dev, + cr_item_qty, + cr_item_qty / ( sr_item_qty + cr_item_qty + wr_item_qty ) / 3.0 * + 100 cr_dev, + wr_item_qty, + wr_item_qty / ( sr_item_qty + cr_item_qty + wr_item_qty ) / 3.0 * + 100 wr_dev, + ( sr_item_qty + cr_item_qty + wr_item_qty ) / 3.0 + average +FROM sr_items, + cr_items, + wr_items +WHERE sr_items.item_id = cr_items.item_id + AND sr_items.item_id = wr_items.item_id +ORDER BY sr_items.item_id, + sr_item_qty +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query84.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query84.sql new file mode 100644 index 000000000..f7eae1a7e --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query84.sql @@ -0,0 +1,21 @@ +-- start query 84 in stream 0 using template query84.tpl +SELECT c_customer_id AS customer_id, + c_last_name + || ', ' + || c_first_name AS customername +FROM customer, + customer_address, + customer_demographics, + household_demographics, + income_band, + store_returns +WHERE ca_city = 'Green Acres' + AND c_current_addr_sk = ca_address_sk + AND ib_lower_bound >= 54986 + AND ib_upper_bound <= 54986 + 50000 + AND ib_income_band_sk = hd_income_band_sk + AND cd_demo_sk = c_current_cdemo_sk + AND hd_demo_sk = c_current_hdemo_sk + AND sr_cdemo_sk = cd_demo_sk +ORDER BY c_customer_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query85.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query85.sql new file mode 100644 index 000000000..be2f68d48 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query85.sql @@ -0,0 +1,52 @@ +-- start query 85 in stream 0 using template query85.tpl +SELECT Substr(r_reason_desc, 1, 20), + Avg(ws_quantity), + Avg(wr_refunded_cash), + Avg(wr_fee) +FROM web_sales, + web_returns, + web_page, + customer_demographics cd1, + customer_demographics cd2, + customer_address, + date_dim, + reason +WHERE ws_web_page_sk = wp_web_page_sk + AND ws_item_sk = wr_item_sk + AND ws_order_number = wr_order_number + AND ws_sold_date_sk = d_date_sk + AND d_year = 2001 + AND cd1.cd_demo_sk = wr_refunded_cdemo_sk + AND cd2.cd_demo_sk = wr_returning_cdemo_sk + AND ca_address_sk = wr_refunded_addr_sk + AND r_reason_sk = wr_reason_sk + AND ( ( cd1.cd_marital_status = 'W' + AND cd1.cd_marital_status = cd2.cd_marital_status + AND cd1.cd_education_status = 'Primary' + AND cd1.cd_education_status = cd2.cd_education_status + AND ws_sales_price BETWEEN 100.00 AND 150.00 ) + OR ( cd1.cd_marital_status = 'D' + AND cd1.cd_marital_status = cd2.cd_marital_status + AND cd1.cd_education_status = 'Secondary' + AND cd1.cd_education_status = cd2.cd_education_status + AND ws_sales_price BETWEEN 50.00 AND 100.00 ) + OR ( cd1.cd_marital_status = 'M' + AND cd1.cd_marital_status = cd2.cd_marital_status + AND cd1.cd_education_status = 'Advanced Degree' + AND cd1.cd_education_status = cd2.cd_education_status + AND ws_sales_price BETWEEN 150.00 AND 200.00 ) ) + AND ( ( ca_country = 'United States' + AND ca_state IN ( 'KY', 'ME', 'IL' ) + AND ws_net_profit BETWEEN 100 AND 200 ) + OR ( ca_country = 'United States' + AND ca_state IN ( 'OK', 'NE', 'MN' ) + AND ws_net_profit BETWEEN 150 AND 300 ) + OR ( ca_country = 'United States' + AND ca_state IN ( 'FL', 'WI', 'KS' ) + AND ws_net_profit BETWEEN 50 AND 250 ) ) +GROUP BY r_reason_desc +ORDER BY Substr(r_reason_desc, 1, 20), + Avg(ws_quantity), + Avg(wr_refunded_cash), + Avg(wr_fee) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query86.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query86.sql new file mode 100644 index 000000000..ec513d402 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query86.sql @@ -0,0 +1,24 @@ +-- start query 86 in stream 0 using template query86.tpl +SELECT Sum(ws_net_paid) AS total_sum, + i_category, + i_class, + Grouping(i_category) + Grouping(i_class) AS lochierarchy, + Rank() + OVER ( + partition BY Grouping(i_category)+Grouping(i_class), CASE + WHEN Grouping( + i_class) = 0 THEN i_category END + ORDER BY Sum(ws_net_paid) DESC) AS rank_within_parent +FROM web_sales, + date_dim d1, + item +WHERE d1.d_month_seq BETWEEN 1183 AND 1183 + 11 + AND d1.d_date_sk = ws_sold_date_sk + AND i_item_sk = ws_item_sk +GROUP BY rollup( i_category, i_class ) +ORDER BY lochierarchy DESC, + CASE + WHEN lochierarchy = 0 THEN i_category + END, + rank_within_parent +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query87.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query87.sql new file mode 100644 index 000000000..6f58f2e09 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query87.sql @@ -0,0 +1,21 @@ +-- start query 87 in stream 0 using template query87.tpl +select count(*) +from ((select distinct c_last_name, c_first_name, d_date + from store_sales, date_dim, customer + where store_sales.ss_sold_date_sk = date_dim.d_date_sk + and store_sales.ss_customer_sk = customer.c_customer_sk + and d_month_seq between 1188 and 1188+11) + except + (select distinct c_last_name, c_first_name, d_date + from catalog_sales, date_dim, customer + where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk + and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk + and d_month_seq between 1188 and 1188+11) + except + (select distinct c_last_name, c_first_name, d_date + from web_sales, date_dim, customer + where web_sales.ws_sold_date_sk = date_dim.d_date_sk + and web_sales.ws_bill_customer_sk = customer.c_customer_sk + and d_month_seq between 1188 and 1188+11) +) cool_cust +; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query88.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query88.sql new file mode 100644 index 000000000..d1945f341 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query88.sql @@ -0,0 +1,92 @@ +-- start query 88 in stream 0 using template query88.tpl +select * +from + (select count(*) h8_30_to_9 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 8 + and time_dim.t_minute >= 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s1, + (select count(*) h9_to_9_30 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 9 + and time_dim.t_minute < 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s2, + (select count(*) h9_30_to_10 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 9 + and time_dim.t_minute >= 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s3, + (select count(*) h10_to_10_30 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 10 + and time_dim.t_minute < 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s4, + (select count(*) h10_30_to_11 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 10 + and time_dim.t_minute >= 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s5, + (select count(*) h11_to_11_30 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 11 + and time_dim.t_minute < 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s6, + (select count(*) h11_30_to_12 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 11 + and time_dim.t_minute >= 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s7, + (select count(*) h12_to_12_30 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 12 + and time_dim.t_minute < 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s8 +; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query89.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query89.sql new file mode 100644 index 000000000..1459f9cf9 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query89.sql @@ -0,0 +1,40 @@ +-- start query 89 in stream 0 using template query89.tpl +SELECT * +FROM (SELECT i_category, + i_class, + i_brand, + s_store_name, + s_company_name, + d_moy, + Sum(ss_sales_price) sum_sales, + Avg(Sum(ss_sales_price)) + OVER ( + partition BY i_category, i_brand, s_store_name, s_company_name + ) + avg_monthly_sales + FROM item, + store_sales, + date_dim, + store + WHERE ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND ss_store_sk = s_store_sk + AND d_year IN ( 2002 ) + AND ( ( i_category IN ( 'Home', 'Men', 'Sports' ) + AND i_class IN ( 'paint', 'accessories', 'fitness' ) ) + OR ( i_category IN ( 'Shoes', 'Jewelry', 'Women' ) + AND i_class IN ( 'mens', 'pendants', 'swimwear' ) ) ) + GROUP BY i_category, + i_class, + i_brand, + s_store_name, + s_company_name, + d_moy) tmp1 +WHERE CASE + WHEN ( avg_monthly_sales <> 0 ) THEN ( + Abs(sum_sales - avg_monthly_sales) / avg_monthly_sales ) + ELSE NULL + END > 0.1 +ORDER BY sum_sales - avg_monthly_sales, + s_store_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query9.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query9.sql new file mode 100644 index 000000000..8073df2f9 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query9.sql @@ -0,0 +1,63 @@ +-- start query 9 in stream 0 using template query9.tpl +SELECT CASE + WHEN (SELECT Count(*) + FROM store_sales + WHERE ss_quantity BETWEEN 1 AND 20) > 3672 THEN + (SELECT Avg(ss_ext_list_price) + FROM store_sales + WHERE + ss_quantity BETWEEN 1 AND 20) + ELSE (SELECT Avg(ss_net_profit) + FROM store_sales + WHERE ss_quantity BETWEEN 1 AND 20) + END bucket1, + CASE + WHEN (SELECT Count(*) + FROM store_sales + WHERE ss_quantity BETWEEN 21 AND 40) > 3392 THEN + (SELECT Avg(ss_ext_list_price) + FROM store_sales + WHERE + ss_quantity BETWEEN 21 AND 40) + ELSE (SELECT Avg(ss_net_profit) + FROM store_sales + WHERE ss_quantity BETWEEN 21 AND 40) + END bucket2, + CASE + WHEN (SELECT Count(*) + FROM store_sales + WHERE ss_quantity BETWEEN 41 AND 60) > 32784 THEN + (SELECT Avg(ss_ext_list_price) + FROM store_sales + WHERE + ss_quantity BETWEEN 41 AND 60) + ELSE (SELECT Avg(ss_net_profit) + FROM store_sales + WHERE ss_quantity BETWEEN 41 AND 60) + END bucket3, + CASE + WHEN (SELECT Count(*) + FROM store_sales + WHERE ss_quantity BETWEEN 61 AND 80) > 26032 THEN + (SELECT Avg(ss_ext_list_price) + FROM store_sales + WHERE + ss_quantity BETWEEN 61 AND 80) + ELSE (SELECT Avg(ss_net_profit) + FROM store_sales + WHERE ss_quantity BETWEEN 61 AND 80) + END bucket4, + CASE + WHEN (SELECT Count(*) + FROM store_sales + WHERE ss_quantity BETWEEN 81 AND 100) > 23982 THEN + (SELECT Avg(ss_ext_list_price) + FROM store_sales + WHERE + ss_quantity BETWEEN 81 AND 100) + ELSE (SELECT Avg(ss_net_profit) + FROM store_sales + WHERE ss_quantity BETWEEN 81 AND 100) + END bucket5 +FROM reason +WHERE r_reason_sk = 1; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query90.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query90.sql new file mode 100644 index 000000000..bc117f29e --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query90.sql @@ -0,0 +1,28 @@ + +-- start query 90 in stream 0 using template query90.tpl +SELECT Cast(amc AS DECIMAL(15, 4)) / Cast(pmc AS DECIMAL(15, 4)) + am_pm_ratio +FROM (SELECT Count(*) amc + FROM web_sales, + household_demographics, + time_dim, + web_page + WHERE ws_sold_time_sk = time_dim.t_time_sk + AND ws_ship_hdemo_sk = household_demographics.hd_demo_sk + AND ws_web_page_sk = web_page.wp_web_page_sk + AND time_dim.t_hour BETWEEN 12 AND 12 + 1 + AND household_demographics.hd_dep_count = 8 + AND web_page.wp_char_count BETWEEN 5000 AND 5200) at1, + (SELECT Count(*) pmc + FROM web_sales, + household_demographics, + time_dim, + web_page + WHERE ws_sold_time_sk = time_dim.t_time_sk + AND ws_ship_hdemo_sk = household_demographics.hd_demo_sk + AND ws_web_page_sk = web_page.wp_web_page_sk + AND time_dim.t_hour BETWEEN 20 AND 20 + 1 + AND household_demographics.hd_dep_count = 8 + AND web_page.wp_char_count BETWEEN 5000 AND 5200) pt +ORDER BY am_pm_ratio +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query91.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query91.sql new file mode 100644 index 000000000..457aa8b45 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query91.sql @@ -0,0 +1,32 @@ +-- start query 91 in stream 0 using template query91.tpl +SELECT cc_call_center_id Call_Center, + cc_name Call_Center_Name, + cc_manager Manager, + Sum(cr_net_loss) Returns_Loss +FROM call_center, + catalog_returns, + date_dim, + customer, + customer_address, + customer_demographics, + household_demographics +WHERE cr_call_center_sk = cc_call_center_sk + AND cr_returned_date_sk = d_date_sk + AND cr_returning_customer_sk = c_customer_sk + AND cd_demo_sk = c_current_cdemo_sk + AND hd_demo_sk = c_current_hdemo_sk + AND ca_address_sk = c_current_addr_sk + AND d_year = 1999 + AND d_moy = 12 + AND ( ( cd_marital_status = 'M' + AND cd_education_status = 'Unknown' ) + OR ( cd_marital_status = 'W' + AND cd_education_status = 'Advanced Degree' ) ) + AND hd_buy_potential LIKE 'Unknown%' + AND ca_gmt_offset = -7 +GROUP BY cc_call_center_id, + cc_name, + cc_manager, + cd_marital_status, + cd_education_status +ORDER BY Sum(cr_net_loss) DESC; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query92.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query92.sql new file mode 100644 index 000000000..2c18f4219 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query92.sql @@ -0,0 +1,22 @@ +-- start query 92 in stream 0 using template query92.tpl +SELECT + Sum(ws_ext_discount_amt) AS "Excess Discount Amount" +FROM web_sales , + item , + date_dim +WHERE i_manufact_id = 718 +AND i_item_sk = ws_item_sk +AND d_date BETWEEN '2002-03-29' AND ( + Cast('2002-03-29' AS DATE) + INTERVAL '90' day) +AND d_date_sk = ws_sold_date_sk +AND ws_ext_discount_amt > + ( + SELECT 1.3 * avg(ws_ext_discount_amt) + FROM web_sales , + date_dim + WHERE ws_item_sk = i_item_sk + AND d_date BETWEEN '2002-03-29' AND ( + cast('2002-03-29' AS date) + INTERVAL '90' day) + AND d_date_sk = ws_sold_date_sk ) +ORDER BY sum(ws_ext_discount_amt) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query93.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query93.sql new file mode 100644 index 000000000..f7fdc3296 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query93.sql @@ -0,0 +1,22 @@ +-- start query 93 in stream 0 using template query93.tpl +SELECT ss_customer_sk, + Sum(act_sales) sumsales +FROM (SELECT ss_item_sk, + ss_ticket_number, + ss_customer_sk, + CASE + WHEN sr_return_quantity IS NOT NULL THEN + ( ss_quantity - sr_return_quantity ) * ss_sales_price + ELSE ( ss_quantity * ss_sales_price ) + END act_sales + FROM store_sales + LEFT OUTER JOIN store_returns + ON ( sr_item_sk = ss_item_sk + AND sr_ticket_number = ss_ticket_number ), + reason + WHERE sr_reason_sk = r_reason_sk + AND r_reason_desc = 'reason 38') t +GROUP BY ss_customer_sk +ORDER BY sumsales, + ss_customer_sk +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query94.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query94.sql new file mode 100644 index 000000000..773fa37aa --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query94.sql @@ -0,0 +1,29 @@ +-- start query 94 in stream 0 using template query94.tpl +SELECT + Count(DISTINCT ws_order_number) AS "order count" , + Sum(ws_ext_ship_cost) AS "total shipping cost" , + Sum(ws_net_profit) AS "total net profit" +FROM web_sales ws1 , + date_dim , + customer_address , + web_site +WHERE d_date BETWEEN '2000-3-01' AND ( + Cast('2000-3-01' AS DATE) + INTERVAL '60' day) +AND ws1.ws_ship_date_sk = d_date_sk +AND ws1.ws_ship_addr_sk = ca_address_sk +AND ca_state = 'MT' +AND ws1.ws_web_site_sk = web_site_sk +AND web_company_name = 'pri' +AND EXISTS + ( + SELECT * + FROM web_sales ws2 + WHERE ws1.ws_order_number = ws2.ws_order_number + AND ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk) +AND NOT EXISTS + ( + SELECT * + FROM web_returns wr1 + WHERE ws1.ws_order_number = wr1.wr_order_number) +ORDER BY count(DISTINCT ws_order_number) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query95.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query95.sql new file mode 100644 index 000000000..334a40529 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query95.sql @@ -0,0 +1,37 @@ +-- start query 95 in stream 0 using template query95.tpl +WITH ws_wh AS +( + SELECT ws1.ws_order_number, + ws1.ws_warehouse_sk wh1, + ws2.ws_warehouse_sk wh2 + FROM web_sales ws1, + web_sales ws2 + WHERE ws1.ws_order_number = ws2.ws_order_number + AND ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk) +SELECT + Count(DISTINCT ws_order_number) AS "order count" , + Sum(ws_ext_ship_cost) AS "total shipping cost" , + Sum(ws_net_profit) AS "total net profit" +FROM web_sales ws1 , + date_dim , + customer_address , + web_site +WHERE d_date BETWEEN '2000-4-01' AND ( + Cast('2000-4-01' AS DATE) + INTERVAL '60' day) +AND ws1.ws_ship_date_sk = d_date_sk +AND ws1.ws_ship_addr_sk = ca_address_sk +AND ca_state = 'IN' +AND ws1.ws_web_site_sk = web_site_sk +AND web_company_name = 'pri' +AND ws1.ws_order_number IN + ( + SELECT ws_order_number + FROM ws_wh) +AND ws1.ws_order_number IN + ( + SELECT wr_order_number + FROM web_returns, + ws_wh + WHERE wr_order_number = ws_wh.ws_order_number) +ORDER BY count(DISTINCT ws_order_number) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query96.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query96.sql new file mode 100644 index 000000000..1f7731524 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query96.sql @@ -0,0 +1,15 @@ +-- start query 96 in stream 0 using template query96.tpl +SELECT Count(*) +FROM store_sales, + household_demographics, + time_dim, + store +WHERE ss_sold_time_sk = time_dim.t_time_sk + AND ss_hdemo_sk = household_demographics.hd_demo_sk + AND ss_store_sk = s_store_sk + AND time_dim.t_hour = 15 + AND time_dim.t_minute >= 30 + AND household_demographics.hd_dep_count = 7 + AND store.s_store_name = 'ese' +ORDER BY Count(*) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query97.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query97.sql new file mode 100644 index 000000000..6a6be875a --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query97.sql @@ -0,0 +1,40 @@ + +-- start query 97 in stream 0 using template query97.tpl +WITH ssci + AS (SELECT ss_customer_sk customer_sk, + ss_item_sk item_sk + FROM store_sales, + date_dim + WHERE ss_sold_date_sk = d_date_sk + AND d_month_seq BETWEEN 1196 AND 1196 + 11 + GROUP BY ss_customer_sk, + ss_item_sk), + csci + AS (SELECT cs_bill_customer_sk customer_sk, + cs_item_sk item_sk + FROM catalog_sales, + date_dim + WHERE cs_sold_date_sk = d_date_sk + AND d_month_seq BETWEEN 1196 AND 1196 + 11 + GROUP BY cs_bill_customer_sk, + cs_item_sk) +SELECT Sum(CASE + WHEN ssci.customer_sk IS NOT NULL + AND csci.customer_sk IS NULL THEN 1 + ELSE 0 + END) store_only, + Sum(CASE + WHEN ssci.customer_sk IS NULL + AND csci.customer_sk IS NOT NULL THEN 1 + ELSE 0 + END) catalog_only, + Sum(CASE + WHEN ssci.customer_sk IS NOT NULL + AND csci.customer_sk IS NOT NULL THEN 1 + ELSE 0 + END) store_and_catalog +FROM ssci + FULL OUTER JOIN csci + ON ( ssci.customer_sk = csci.customer_sk + AND ssci.item_sk = csci.item_sk ) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query98.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query98.sql new file mode 100644 index 000000000..62eaaa518 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query98.sql @@ -0,0 +1,29 @@ + +-- start query 98 in stream 0 using template query98.tpl +SELECT i_item_id, + i_item_desc, + i_category, + i_class, + i_current_price, + Sum(ss_ext_sales_price) AS itemrevenue, + Sum(ss_ext_sales_price) * 100 / Sum(Sum(ss_ext_sales_price)) + OVER ( + PARTITION BY i_class) AS revenueratio +FROM store_sales, + item, + date_dim +WHERE ss_item_sk = i_item_sk + AND i_category IN ( 'Men', 'Home', 'Electronics' ) + AND ss_sold_date_sk = d_date_sk + AND d_date BETWEEN CAST('2000-05-18' AS DATE) AND ( + CAST('2000-05-18' AS DATE) + INTERVAL '30' DAY ) +GROUP BY i_item_id, + i_item_desc, + i_category, + i_class, + i_current_price +ORDER BY i_category, + i_class, + i_item_id, + i_item_desc, + revenueratio; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query99.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query99.sql new file mode 100644 index 000000000..73b6739e5 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/ansi/query99.sql @@ -0,0 +1,47 @@ + + +-- start query 99 in stream 0 using template query99.tpl +SELECT Substr(w_warehouse_name, 1, 20), + sm_type, + cc_name, + Sum(CASE + WHEN ( cs_ship_date_sk - cs_sold_date_sk <= 30 ) THEN 1 + ELSE 0 + END) AS "30 days", + Sum(CASE + WHEN ( cs_ship_date_sk - cs_sold_date_sk > 30 ) + AND ( cs_ship_date_sk - cs_sold_date_sk <= 60 ) THEN 1 + ELSE 0 + END) AS "31-60 days", + Sum(CASE + WHEN ( cs_ship_date_sk - cs_sold_date_sk > 60 ) + AND ( cs_ship_date_sk - cs_sold_date_sk <= 90 ) THEN 1 + ELSE 0 + END) AS "61-90 days", + Sum(CASE + WHEN ( cs_ship_date_sk - cs_sold_date_sk > 90 ) + AND ( cs_ship_date_sk - cs_sold_date_sk <= 120 ) THEN + 1 + ELSE 0 + END) AS "91-120 days", + Sum(CASE + WHEN ( cs_ship_date_sk - cs_sold_date_sk > 120 ) THEN 1 + ELSE 0 + END) AS ">120 days" +FROM catalog_sales, + warehouse, + ship_mode, + call_center, + date_dim +WHERE d_month_seq BETWEEN 1200 AND 1200 + 11 + AND cs_ship_date_sk = d_date_sk + AND cs_warehouse_sk = w_warehouse_sk + AND cs_ship_mode_sk = sm_ship_mode_sk + AND cs_call_center_sk = cc_call_center_sk +GROUP BY Substr(w_warehouse_name, 1, 20), + sm_type, + cc_name +ORDER BY Substr(w_warehouse_name, 1, 20), + sm_type, + cc_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query1.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query1.sql new file mode 100644 index 000000000..8808e3ce6 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query1.sql @@ -0,0 +1,23 @@ +-- start query 1 in stream 0 using template query1.tpl +WITH customer_total_return + AS (SELECT sr_customer_sk AS ctr_customer_sk, + sr_store_sk AS ctr_store_sk, + Sum(sr_return_amt) AS ctr_total_return + FROM store_returns, + date_dim + WHERE sr_returned_date_sk = d_date_sk + AND d_year = 2001 + GROUP BY sr_customer_sk, + sr_store_sk) +SELECT c_customer_id +FROM customer_total_return ctr1, + store, + customer +WHERE ctr1.ctr_total_return > (SELECT Avg(ctr_total_return) * 1.2 + FROM customer_total_return ctr2 + WHERE ctr1.ctr_store_sk = ctr2.ctr_store_sk) + AND s_store_sk = ctr1.ctr_store_sk + AND s_state = 'TN' + AND ctr1.ctr_customer_sk = c_customer_sk +ORDER BY c_customer_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query10.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query10.sql new file mode 100644 index 000000000..0cb0ba050 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query10.sql @@ -0,0 +1,62 @@ +-- start query 10 in stream 0 using template query10.tpl +SELECT cd_gender, + cd_marital_status, + cd_education_status, + Count(*) cnt1, + cd_purchase_estimate, + Count(*) cnt2, + cd_credit_rating, + Count(*) cnt3, + cd_dep_count, + Count(*) cnt4, + cd_dep_employed_count, + Count(*) cnt5, + cd_dep_college_count, + Count(*) cnt6 +FROM customer c, + customer_address ca, + customer_demographics +WHERE c.c_current_addr_sk = ca.ca_address_sk + AND ca_county IN ( 'Lycoming County', 'Sheridan County', + 'Kandiyohi County', + 'Pike County', + 'Greene County' ) + AND cd_demo_sk = c.c_current_cdemo_sk + AND EXISTS (SELECT * + FROM store_sales, + date_dim + WHERE c.c_customer_sk = ss_customer_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 2002 + AND d_moy BETWEEN 4 AND 4 + 3) + AND ( EXISTS (SELECT * + FROM web_sales, + date_dim + WHERE c.c_customer_sk = ws_bill_customer_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 2002 + AND d_moy BETWEEN 4 AND 4 + 3) + OR EXISTS (SELECT * + FROM catalog_sales, + date_dim + WHERE c.c_customer_sk = cs_ship_customer_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 2002 + AND d_moy BETWEEN 4 AND 4 + 3) ) +GROUP BY cd_gender, + cd_marital_status, + cd_education_status, + cd_purchase_estimate, + cd_credit_rating, + cd_dep_count, + cd_dep_employed_count, + cd_dep_college_count +ORDER BY cd_gender, + cd_marital_status, + cd_education_status, + cd_purchase_estimate, + cd_credit_rating, + cd_dep_count, + cd_dep_employed_count, + cd_dep_college_count +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query11.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query11.sql new file mode 100644 index 000000000..cf24b9b74 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query11.sql @@ -0,0 +1,97 @@ +-- start query 11 in stream 0 using template query11.tpl +WITH year_total + AS (SELECT c_customer_id customer_id, + c_first_name customer_first_name + , + c_last_name + customer_last_name, + c_preferred_cust_flag + customer_preferred_cust_flag + , + c_birth_country + customer_birth_country, + c_login customer_login, + c_email_address + customer_email_address, + d_year dyear, + Sum(ss_ext_list_price - ss_ext_discount_amt) year_total, + 's' sale_type + FROM customer, + store_sales, + date_dim + WHERE c_customer_sk = ss_customer_sk + AND ss_sold_date_sk = d_date_sk + GROUP BY c_customer_id, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_country, + c_login, + c_email_address, + d_year + UNION ALL + SELECT c_customer_id customer_id, + c_first_name customer_first_name + , + c_last_name + customer_last_name, + c_preferred_cust_flag + customer_preferred_cust_flag + , + c_birth_country + customer_birth_country, + c_login customer_login, + c_email_address + customer_email_address, + d_year dyear, + Sum(ws_ext_list_price - ws_ext_discount_amt) year_total, + 'w' sale_type + FROM customer, + web_sales, + date_dim + WHERE c_customer_sk = ws_bill_customer_sk + AND ws_sold_date_sk = d_date_sk + GROUP BY c_customer_id, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_country, + c_login, + c_email_address, + d_year) +SELECT t_s_secyear.customer_id, + t_s_secyear.customer_first_name, + t_s_secyear.customer_last_name, + t_s_secyear.customer_birth_country +FROM year_total t_s_firstyear, + year_total t_s_secyear, + year_total t_w_firstyear, + year_total t_w_secyear +WHERE t_s_secyear.customer_id = t_s_firstyear.customer_id + AND t_s_firstyear.customer_id = t_w_secyear.customer_id + AND t_s_firstyear.customer_id = t_w_firstyear.customer_id + AND t_s_firstyear.sale_type = 's' + AND t_w_firstyear.sale_type = 'w' + AND t_s_secyear.sale_type = 's' + AND t_w_secyear.sale_type = 'w' + AND t_s_firstyear.dyear = 2001 + AND t_s_secyear.dyear = 2001 + 1 + AND t_w_firstyear.dyear = 2001 + AND t_w_secyear.dyear = 2001 + 1 + AND t_s_firstyear.year_total > 0 + AND t_w_firstyear.year_total > 0 + AND CASE + WHEN t_w_firstyear.year_total > 0 THEN t_w_secyear.year_total / + t_w_firstyear.year_total + ELSE 0.0 + END > CASE + WHEN t_s_firstyear.year_total > 0 THEN + t_s_secyear.year_total / + t_s_firstyear.year_total + ELSE 0.0 + END +ORDER BY t_s_secyear.customer_id, + t_s_secyear.customer_first_name, + t_s_secyear.customer_last_name, + t_s_secyear.customer_birth_country +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query12.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query12.sql new file mode 100644 index 000000000..5f6d721c4 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query12.sql @@ -0,0 +1,30 @@ +-- start query 12 in stream 0 using template query12.tpl +SELECT + i_item_id , + i_item_desc , + i_category , + i_class , + i_current_price , + Sum(ws_ext_sales_price) AS itemrevenue , + Sum(ws_ext_sales_price)*100/Sum(Sum(ws_ext_sales_price)) OVER (partition BY i_class) AS revenueratio +FROM web_sales , + item , + date_dim +WHERE ws_item_sk = i_item_sk +AND i_category IN ('Home', + 'Men', + 'Women') +AND ws_sold_date_sk = d_date_sk +AND d_date BETWEEN Cast('2000-05-11' AS DATE) AND ( + Cast('2000-05-11' AS DATE) + INTERVAL '30' day) +GROUP BY i_item_id , + i_item_desc , + i_category , + i_class , + i_current_price +ORDER BY i_category , + i_class , + i_item_id , + i_item_desc , + revenueratio +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query13.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query13.sql new file mode 100644 index 000000000..2bec54b95 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query13.sql @@ -0,0 +1,44 @@ +-- start query 13 in stream 0 using template query13.tpl +SELECT Avg(ss_quantity), + Avg(ss_ext_sales_price), + Avg(ss_ext_wholesale_cost), + Sum(ss_ext_wholesale_cost) +FROM store_sales, + store, + customer_demographics, + household_demographics, + customer_address, + date_dim +WHERE s_store_sk = ss_store_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 2001 + AND ( ( ss_hdemo_sk = hd_demo_sk + AND cd_demo_sk = ss_cdemo_sk + AND cd_marital_status = 'U' + AND cd_education_status = 'Advanced Degree' + AND ss_sales_price BETWEEN 100.00 AND 150.00 + AND hd_dep_count = 3 ) + OR ( ss_hdemo_sk = hd_demo_sk + AND cd_demo_sk = ss_cdemo_sk + AND cd_marital_status = 'M' + AND cd_education_status = 'Primary' + AND ss_sales_price BETWEEN 50.00 AND 100.00 + AND hd_dep_count = 1 ) + OR ( ss_hdemo_sk = hd_demo_sk + AND cd_demo_sk = ss_cdemo_sk + AND cd_marital_status = 'D' + AND cd_education_status = 'Secondary' + AND ss_sales_price BETWEEN 150.00 AND 200.00 + AND hd_dep_count = 1 ) ) + AND ( ( ss_addr_sk = ca_address_sk + AND ca_country = 'United States' + AND ca_state IN ( 'AZ', 'NE', 'IA' ) + AND ss_net_profit BETWEEN 100 AND 200 ) + OR ( ss_addr_sk = ca_address_sk + AND ca_country = 'United States' + AND ca_state IN ( 'MS', 'CA', 'NV' ) + AND ss_net_profit BETWEEN 150 AND 300 ) + OR ( ss_addr_sk = ca_address_sk + AND ca_country = 'United States' + AND ca_state IN ( 'GA', 'TX', 'NJ' ) + AND ss_net_profit BETWEEN 50 AND 250 ) ); diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query14.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query14.sql new file mode 100644 index 000000000..7a7572bba --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query14.sql @@ -0,0 +1,245 @@ +-- start query 14 in stream 0 using template query14.tpl +WITH cross_items + AS (SELECT i_item_sk ss_item_sk + FROM item, + (SELECT iss.i_brand_id brand_id, + iss.i_class_id class_id, + iss.i_category_id category_id + FROM store_sales, + item iss, + date_dim d1 + WHERE ss_item_sk = iss.i_item_sk + AND ss_sold_date_sk = d1.d_date_sk + AND d1.d_year BETWEEN 1999 AND 1999 + 2 + INTERSECT + SELECT ics.i_brand_id, + ics.i_class_id, + ics.i_category_id + FROM catalog_sales, + item ics, + date_dim d2 + WHERE cs_item_sk = ics.i_item_sk + AND cs_sold_date_sk = d2.d_date_sk + AND d2.d_year BETWEEN 1999 AND 1999 + 2 + INTERSECT + SELECT iws.i_brand_id, + iws.i_class_id, + iws.i_category_id + FROM web_sales, + item iws, + date_dim d3 + WHERE ws_item_sk = iws.i_item_sk + AND ws_sold_date_sk = d3.d_date_sk + AND d3.d_year BETWEEN 1999 AND 1999 + 2) + WHERE i_brand_id = brand_id + AND i_class_id = class_id + AND i_category_id = category_id), + avg_sales + AS (SELECT Avg(quantity * list_price) average_sales + FROM (SELECT ss_quantity quantity, + ss_list_price list_price + FROM store_sales, + date_dim + WHERE ss_sold_date_sk = d_date_sk + AND d_year BETWEEN 1999 AND 1999 + 2 + UNION ALL + SELECT cs_quantity quantity, + cs_list_price list_price + FROM catalog_sales, + date_dim + WHERE cs_sold_date_sk = d_date_sk + AND d_year BETWEEN 1999 AND 1999 + 2 + UNION ALL + SELECT ws_quantity quantity, + ws_list_price list_price + FROM web_sales, + date_dim + WHERE ws_sold_date_sk = d_date_sk + AND d_year BETWEEN 1999 AND 1999 + 2) x) +SELECT channel, + i_brand_id, + i_class_id, + i_category_id, + Sum(sales), + Sum(number_sales) +FROM (SELECT 'store' channel, + i_brand_id, + i_class_id, + i_category_id, + Sum(ss_quantity * ss_list_price) sales, + Count(*) number_sales + FROM store_sales, + item, + date_dim + WHERE ss_item_sk IN (SELECT ss_item_sk + FROM cross_items) + AND ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 1999 + 2 + AND d_moy = 11 + GROUP BY i_brand_id, + i_class_id, + i_category_id + HAVING Sum(ss_quantity * ss_list_price) > (SELECT average_sales + FROM avg_sales) + UNION ALL + SELECT 'catalog' channel, + i_brand_id, + i_class_id, + i_category_id, + Sum(cs_quantity * cs_list_price) sales, + Count(*) number_sales + FROM catalog_sales, + item, + date_dim + WHERE cs_item_sk IN (SELECT ss_item_sk + FROM cross_items) + AND cs_item_sk = i_item_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 1999 + 2 + AND d_moy = 11 + GROUP BY i_brand_id, + i_class_id, + i_category_id + HAVING Sum(cs_quantity * cs_list_price) > (SELECT average_sales + FROM avg_sales) + UNION ALL + SELECT 'web' channel, + i_brand_id, + i_class_id, + i_category_id, + Sum(ws_quantity * ws_list_price) sales, + Count(*) number_sales + FROM web_sales, + item, + date_dim + WHERE ws_item_sk IN (SELECT ss_item_sk + FROM cross_items) + AND ws_item_sk = i_item_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 1999 + 2 + AND d_moy = 11 + GROUP BY i_brand_id, + i_class_id, + i_category_id + HAVING Sum(ws_quantity * ws_list_price) > (SELECT average_sales + FROM avg_sales)) y +GROUP BY rollup ( channel, i_brand_id, i_class_id, i_category_id ) +ORDER BY channel, + i_brand_id, + i_class_id, + i_category_id +LIMIT 100; + +WITH cross_items + AS (SELECT i_item_sk ss_item_sk + FROM item, + (SELECT iss.i_brand_id brand_id, + iss.i_class_id class_id, + iss.i_category_id category_id + FROM store_sales, + item iss, + date_dim d1 + WHERE ss_item_sk = iss.i_item_sk + AND ss_sold_date_sk = d1.d_date_sk + AND d1.d_year BETWEEN 1999 AND 1999 + 2 + INTERSECT + SELECT ics.i_brand_id, + ics.i_class_id, + ics.i_category_id + FROM catalog_sales, + item ics, + date_dim d2 + WHERE cs_item_sk = ics.i_item_sk + AND cs_sold_date_sk = d2.d_date_sk + AND d2.d_year BETWEEN 1999 AND 1999 + 2 + INTERSECT + SELECT iws.i_brand_id, + iws.i_class_id, + iws.i_category_id + FROM web_sales, + item iws, + date_dim d3 + WHERE ws_item_sk = iws.i_item_sk + AND ws_sold_date_sk = d3.d_date_sk + AND d3.d_year BETWEEN 1999 AND 1999 + 2) x + WHERE i_brand_id = brand_id + AND i_class_id = class_id + AND i_category_id = category_id), + avg_sales + AS (SELECT Avg(quantity * list_price) average_sales + FROM (SELECT ss_quantity quantity, + ss_list_price list_price + FROM store_sales, + date_dim + WHERE ss_sold_date_sk = d_date_sk + AND d_year BETWEEN 1999 AND 1999 + 2 + UNION ALL + SELECT cs_quantity quantity, + cs_list_price list_price + FROM catalog_sales, + date_dim + WHERE cs_sold_date_sk = d_date_sk + AND d_year BETWEEN 1999 AND 1999 + 2 + UNION ALL + SELECT ws_quantity quantity, + ws_list_price list_price + FROM web_sales, + date_dim + WHERE ws_sold_date_sk = d_date_sk + AND d_year BETWEEN 1999 AND 1999 + 2) x) +SELECT * +FROM (SELECT 'store' channel, + i_brand_id, + i_class_id, + i_category_id, + Sum(ss_quantity * ss_list_price) sales, + Count(*) number_sales + FROM store_sales, + item, + date_dim + WHERE ss_item_sk IN (SELECT ss_item_sk + FROM cross_items) + AND ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND d_week_seq = (SELECT d_week_seq + FROM date_dim + WHERE d_year = 1999 + 1 + AND d_moy = 12 + AND d_dom = 25) + GROUP BY i_brand_id, + i_class_id, + i_category_id + HAVING Sum(ss_quantity * ss_list_price) > (SELECT average_sales + FROM avg_sales)) this_year, + (SELECT 'store' channel, + i_brand_id, + i_class_id, + i_category_id, + Sum(ss_quantity * ss_list_price) sales, + Count(*) number_sales + FROM store_sales, + item, + date_dim + WHERE ss_item_sk IN (SELECT ss_item_sk + FROM cross_items) + AND ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND d_week_seq = (SELECT d_week_seq + FROM date_dim + WHERE d_year = 1999 + AND d_moy = 12 + AND d_dom = 25) + GROUP BY i_brand_id, + i_class_id, + i_category_id + HAVING Sum(ss_quantity * ss_list_price) > (SELECT average_sales + FROM avg_sales)) last_year +WHERE this_year.i_brand_id = last_year.i_brand_id + AND this_year.i_class_id = last_year.i_class_id + AND this_year.i_category_id = last_year.i_category_id +ORDER BY this_year.channel, + this_year.i_brand_id, + this_year.i_class_id, + this_year.i_category_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query15.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query15.sql new file mode 100644 index 000000000..207ddd719 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query15.sql @@ -0,0 +1,20 @@ +-- start query 15 in stream 0 using template query15.tpl +SELECT ca_zip, + Sum(cs_sales_price) +FROM catalog_sales, + customer, + customer_address, + date_dim +WHERE cs_bill_customer_sk = c_customer_sk + AND c_current_addr_sk = ca_address_sk + AND ( Substr(ca_zip, 1, 5) IN ( '85669', '86197', '88274', '83405', + '86475', '85392', '85460', '80348', + '81792' ) + OR ca_state IN ( 'CA', 'WA', 'GA' ) + OR cs_sales_price > 500 ) + AND cs_sold_date_sk = d_date_sk + AND d_qoy = 1 + AND d_year = 1998 +GROUP BY ca_zip +ORDER BY ca_zip +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query16.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query16.sql new file mode 100644 index 000000000..b19499577 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query16.sql @@ -0,0 +1,33 @@ +-- start query 16 in stream 0 using template query16.tpl +SELECT + Count(DISTINCT cs_order_number) AS `order count` , + Sum(cs_ext_ship_cost) AS `total shipping cost` , + Sum(cs_net_profit) AS `total net profit` +FROM catalog_sales cs1 , + date_dim , + customer_address , + call_center +WHERE d_date BETWEEN '2002-3-01' AND ( + Cast('2002-3-01' AS DATE) + INTERVAL '60' day) +AND cs1.cs_ship_date_sk = d_date_sk +AND cs1.cs_ship_addr_sk = ca_address_sk +AND ca_state = 'IA' +AND cs1.cs_call_center_sk = cc_call_center_sk +AND cc_county IN ('Williamson County', + 'Williamson County', + 'Williamson County', + 'Williamson County', + 'Williamson County' ) +AND EXISTS + ( + SELECT * + FROM catalog_sales cs2 + WHERE cs1.cs_order_number = cs2.cs_order_number + AND cs1.cs_warehouse_sk <> cs2.cs_warehouse_sk) +AND NOT EXISTS + ( + SELECT * + FROM catalog_returns cr1 + WHERE cs1.cs_order_number = cr1.cr_order_number) +ORDER BY count(DISTINCT cs_order_number) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query17.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query17.sql new file mode 100644 index 000000000..da916d4a5 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query17.sql @@ -0,0 +1,56 @@ +-- start query 17 in stream 0 using template query17.tpl +SELECT i_item_id, + i_item_desc, + s_state, + Count(ss_quantity) AS + store_sales_quantitycount, + Avg(ss_quantity) AS + store_sales_quantityave, + Stddev_samp(ss_quantity) AS + store_sales_quantitystdev, + Stddev_samp(ss_quantity) / Avg(ss_quantity) AS + store_sales_quantitycov, + Count(sr_return_quantity) AS + store_returns_quantitycount, + Avg(sr_return_quantity) AS + store_returns_quantityave, + Stddev_samp(sr_return_quantity) AS + store_returns_quantitystdev, + Stddev_samp(sr_return_quantity) / Avg(sr_return_quantity) AS + store_returns_quantitycov, + Count(cs_quantity) AS + catalog_sales_quantitycount, + Avg(cs_quantity) AS + catalog_sales_quantityave, + Stddev_samp(cs_quantity) / Avg(cs_quantity) AS + catalog_sales_quantitystdev, + Stddev_samp(cs_quantity) / Avg(cs_quantity) AS + catalog_sales_quantitycov +FROM store_sales, + store_returns, + catalog_sales, + date_dim d1, + date_dim d2, + date_dim d3, + store, + item +WHERE d1.d_quarter_name = '1999Q1' + AND d1.d_date_sk = ss_sold_date_sk + AND i_item_sk = ss_item_sk + AND s_store_sk = ss_store_sk + AND ss_customer_sk = sr_customer_sk + AND ss_item_sk = sr_item_sk + AND ss_ticket_number = sr_ticket_number + AND sr_returned_date_sk = d2.d_date_sk + AND d2.d_quarter_name IN ( '1999Q1', '1999Q2', '1999Q3' ) + AND sr_customer_sk = cs_bill_customer_sk + AND sr_item_sk = cs_item_sk + AND cs_sold_date_sk = d3.d_date_sk + AND d3.d_quarter_name IN ( '1999Q1', '1999Q2', '1999Q3' ) +GROUP BY i_item_id, + i_item_desc, + s_state +ORDER BY i_item_id, + i_item_desc, + s_state +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query18.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query18.sql new file mode 100644 index 000000000..b59ce7ab0 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query18.sql @@ -0,0 +1,38 @@ +-- start query 18 in stream 0 using template query18.tpl +SELECT i_item_id, + ca_country, + ca_state, + ca_county, + Avg(Cast(cs_quantity AS DECIMAL(12, 2))) agg1, + Avg(Cast(cs_list_price AS DECIMAL(12, 2))) agg2, + Avg(Cast(cs_coupon_amt AS DECIMAL(12, 2))) agg3, + Avg(Cast(cs_sales_price AS DECIMAL(12, 2))) agg4, + Avg(Cast(cs_net_profit AS DECIMAL(12, 2))) agg5, + Avg(Cast(c_birth_year AS DECIMAL(12, 2))) agg6, + Avg(Cast(cd1.cd_dep_count AS DECIMAL(12, 2))) agg7 +FROM catalog_sales, + customer_demographics cd1, + customer_demographics cd2, + customer, + customer_address, + date_dim, + item +WHERE cs_sold_date_sk = d_date_sk + AND cs_item_sk = i_item_sk + AND cs_bill_cdemo_sk = cd1.cd_demo_sk + AND cs_bill_customer_sk = c_customer_sk + AND cd1.cd_gender = 'F' + AND cd1.cd_education_status = 'Secondary' + AND c_current_cdemo_sk = cd2.cd_demo_sk + AND c_current_addr_sk = ca_address_sk + AND c_birth_month IN ( 8, 4, 2, 5, + 11, 9 ) + AND d_year = 2001 + AND ca_state IN ( 'KS', 'IA', 'AL', 'UT', + 'VA', 'NC', 'TX' ) +GROUP BY rollup ( i_item_id, ca_country, ca_state, ca_county ) +ORDER BY ca_country, + ca_state, + ca_county, + i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query19.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query19.sql new file mode 100644 index 000000000..c3039b2fd --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query19.sql @@ -0,0 +1,31 @@ +-- start query 19 in stream 0 using template query19.tpl +SELECT i_brand_id brand_id, + i_brand brand, + i_manufact_id, + i_manufact, + Sum(ss_ext_sales_price) ext_price +FROM date_dim, + store_sales, + item, + customer, + customer_address, + store +WHERE d_date_sk = ss_sold_date_sk + AND ss_item_sk = i_item_sk + AND i_manager_id = 38 + AND d_moy = 12 + AND d_year = 1998 + AND ss_customer_sk = c_customer_sk + AND c_current_addr_sk = ca_address_sk + AND Substr(ca_zip, 1, 5) <> Substr(s_zip, 1, 5) + AND ss_store_sk = s_store_sk +GROUP BY i_brand, + i_brand_id, + i_manufact_id, + i_manufact +ORDER BY ext_price DESC, + i_brand, + i_brand_id, + i_manufact_id, + i_manufact +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query2.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query2.sql new file mode 100644 index 000000000..c85af4246 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query2.sql @@ -0,0 +1,79 @@ +-- start query 2 in stream 0 using template query2.tpl +WITH wscs + AS (SELECT sold_date_sk, + sales_price + FROM (SELECT ws_sold_date_sk sold_date_sk, + ws_ext_sales_price sales_price + FROM web_sales) + UNION ALL + (SELECT cs_sold_date_sk sold_date_sk, + cs_ext_sales_price sales_price + FROM catalog_sales)), + wswscs + AS (SELECT d_week_seq, + Sum(CASE + WHEN ( d_day_name = 'Sunday' ) THEN sales_price + ELSE NULL + END) sun_sales, + Sum(CASE + WHEN ( d_day_name = 'Monday' ) THEN sales_price + ELSE NULL + END) mon_sales, + Sum(CASE + WHEN ( d_day_name = 'Tuesday' ) THEN sales_price + ELSE NULL + END) tue_sales, + Sum(CASE + WHEN ( d_day_name = 'Wednesday' ) THEN sales_price + ELSE NULL + END) wed_sales, + Sum(CASE + WHEN ( d_day_name = 'Thursday' ) THEN sales_price + ELSE NULL + END) thu_sales, + Sum(CASE + WHEN ( d_day_name = 'Friday' ) THEN sales_price + ELSE NULL + END) fri_sales, + Sum(CASE + WHEN ( d_day_name = 'Saturday' ) THEN sales_price + ELSE NULL + END) sat_sales + FROM wscs, + date_dim + WHERE d_date_sk = sold_date_sk + GROUP BY d_week_seq) +SELECT d_week_seq1, + Round(sun_sales1 / sun_sales2, 2), + Round(mon_sales1 / mon_sales2, 2), + Round(tue_sales1 / tue_sales2, 2), + Round(wed_sales1 / wed_sales2, 2), + Round(thu_sales1 / thu_sales2, 2), + Round(fri_sales1 / fri_sales2, 2), + Round(sat_sales1 / sat_sales2, 2) +FROM (SELECT wswscs.d_week_seq d_week_seq1, + sun_sales sun_sales1, + mon_sales mon_sales1, + tue_sales tue_sales1, + wed_sales wed_sales1, + thu_sales thu_sales1, + fri_sales fri_sales1, + sat_sales sat_sales1 + FROM wswscs, + date_dim + WHERE date_dim.d_week_seq = wswscs.d_week_seq + AND d_year = 1998) y, + (SELECT wswscs.d_week_seq d_week_seq2, + sun_sales sun_sales2, + mon_sales mon_sales2, + tue_sales tue_sales2, + wed_sales wed_sales2, + thu_sales thu_sales2, + fri_sales fri_sales2, + sat_sales sat_sales2 + FROM wswscs, + date_dim + WHERE date_dim.d_week_seq = wswscs.d_week_seq + AND d_year = 1998 + 1) z +WHERE d_week_seq1 = d_week_seq2 - 53 +ORDER BY d_week_seq1; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query20.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query20.sql new file mode 100644 index 000000000..3c73340ea --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query20.sql @@ -0,0 +1,30 @@ +-- start query 20 in stream 0 using template query20.tpl +SELECT + i_item_id , + i_item_desc , + i_category , + i_class , + i_current_price , + Sum(cs_ext_sales_price) AS itemrevenue , + Sum(cs_ext_sales_price)*100/Sum(Sum(cs_ext_sales_price)) OVER (partition BY i_class) AS revenueratio +FROM catalog_sales , + item , + date_dim +WHERE cs_item_sk = i_item_sk +AND i_category IN ('Children', + 'Women', + 'Electronics') +AND cs_sold_date_sk = d_date_sk +AND d_date BETWEEN Cast('2001-02-03' AS DATE) AND ( + Cast('2001-02-03' AS DATE) + INTERVAL '30' day) +GROUP BY i_item_id , + i_item_desc , + i_category , + i_class , + i_current_price +ORDER BY i_category , + i_class , + i_item_id , + i_item_desc , + revenueratio +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query21.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query21.sql new file mode 100644 index 000000000..1811226a8 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query21.sql @@ -0,0 +1,38 @@ +-- start query 21 in stream 0 using template query21.tpl +SELECT + * +FROM ( + SELECT w_warehouse_name , + i_item_id , + Sum( + CASE + WHEN ( + Cast(d_date AS DATE) < Cast ('2000-05-13' AS DATE)) THEN inv_quantity_on_hand + ELSE 0 + END) AS inv_before , + Sum( + CASE + WHEN ( + Cast(d_date AS DATE) >= Cast ('2000-05-13' AS DATE)) THEN inv_quantity_on_hand + ELSE 0 + END) AS inv_after + FROM inventory , + warehouse , + item , + date_dim + WHERE i_current_price BETWEEN 0.99 AND 1.49 + AND i_item_sk = inv_item_sk + AND inv_warehouse_sk = w_warehouse_sk + AND inv_date_sk = d_date_sk + AND d_date BETWEEN (Cast ('2000-05-13' AS DATE) - INTERVAL '30' day) AND ( + cast ('2000-05-13' AS date) + INTERVAL '30' day) + GROUP BY w_warehouse_name, + i_item_id) x +WHERE ( + CASE + WHEN inv_before > 0 THEN inv_after / inv_before + ELSE NULL + END) BETWEEN 2.0/3.0 AND 3.0/2.0 +ORDER BY w_warehouse_name , + i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query22.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query22.sql new file mode 100644 index 000000000..707fc7c85 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query22.sql @@ -0,0 +1,21 @@ +-- start query 22 in stream 0 using template query22.tpl +SELECT i_product_name, + i_brand, + i_class, + i_category, + Avg(inv_quantity_on_hand) qoh +FROM inventory, + date_dim, + item, + warehouse +WHERE inv_date_sk = d_date_sk + AND inv_item_sk = i_item_sk + AND inv_warehouse_sk = w_warehouse_sk + AND d_month_seq BETWEEN 1205 AND 1205 + 11 +GROUP BY rollup( i_product_name, i_brand, i_class, i_category ) +ORDER BY qoh, + i_product_name, + i_brand, + i_class, + i_category +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query23.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query23.sql new file mode 100644 index 000000000..66ffc4412 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query23.sql @@ -0,0 +1,136 @@ +-- start query 23 in stream 0 using template query23.tpl +WITH frequent_ss_items + AS (SELECT Substr(i_item_desc, 1, 30) itemdesc, + i_item_sk item_sk, + d_date solddate, + Count(*) cnt + FROM store_sales, + date_dim, + item + WHERE ss_sold_date_sk = d_date_sk + AND ss_item_sk = i_item_sk + AND d_year IN ( 1998, 1998 + 1, 1998 + 2, 1998 + 3 ) + GROUP BY Substr(i_item_desc, 1, 30), + i_item_sk, + d_date + HAVING Count(*) > 4), + max_store_sales + AS (SELECT Max(csales) tpcds_cmax + FROM (SELECT c_customer_sk, + Sum(ss_quantity * ss_sales_price) csales + FROM store_sales, + customer, + date_dim + WHERE ss_customer_sk = c_customer_sk + AND ss_sold_date_sk = d_date_sk + AND d_year IN ( 1998, 1998 + 1, 1998 + 2, 1998 + 3 ) + GROUP BY c_customer_sk)), + best_ss_customer + AS (SELECT c_customer_sk, + Sum(ss_quantity * ss_sales_price) ssales + FROM store_sales, + customer + WHERE ss_customer_sk = c_customer_sk + GROUP BY c_customer_sk + HAVING Sum(ss_quantity * ss_sales_price) > + ( 95 / 100.0 ) * (SELECT * + FROM max_store_sales)) +SELECT Sum(sales) +FROM (SELECT cs_quantity * cs_list_price sales + FROM catalog_sales, + date_dim + WHERE d_year = 1998 + AND d_moy = 6 + AND cs_sold_date_sk = d_date_sk + AND cs_item_sk IN (SELECT item_sk + FROM frequent_ss_items) + AND cs_bill_customer_sk IN (SELECT c_customer_sk + FROM best_ss_customer) + UNION ALL + SELECT ws_quantity * ws_list_price sales + FROM web_sales, + date_dim + WHERE d_year = 1998 + AND d_moy = 6 + AND ws_sold_date_sk = d_date_sk + AND ws_item_sk IN (SELECT item_sk + FROM frequent_ss_items) + AND ws_bill_customer_sk IN (SELECT c_customer_sk + FROM best_ss_customer)) LIMIT 100; + +WITH frequent_ss_items + AS (SELECT Substr(i_item_desc, 1, 30) itemdesc, + i_item_sk item_sk, + d_date solddate, + Count(*) cnt + FROM store_sales, + date_dim, + item + WHERE ss_sold_date_sk = d_date_sk + AND ss_item_sk = i_item_sk + AND d_year IN ( 1998, 1998 + 1, 1998 + 2, 1998 + 3 ) + GROUP BY Substr(i_item_desc, 1, 30), + i_item_sk, + d_date + HAVING Count(*) > 4), + max_store_sales + AS (SELECT Max(csales) tpcds_cmax + FROM (SELECT c_customer_sk, + Sum(ss_quantity * ss_sales_price) csales + FROM store_sales, + customer, + date_dim + WHERE ss_customer_sk = c_customer_sk + AND ss_sold_date_sk = d_date_sk + AND d_year IN ( 1998, 1998 + 1, 1998 + 2, 1998 + 3 ) + GROUP BY c_customer_sk)), + best_ss_customer + AS (SELECT c_customer_sk, + Sum(ss_quantity * ss_sales_price) ssales + FROM store_sales, + customer + WHERE ss_customer_sk = c_customer_sk + GROUP BY c_customer_sk + HAVING Sum(ss_quantity * ss_sales_price) > + ( 95 / 100.0 ) * (SELECT * + FROM max_store_sales)) +SELECT c_last_name, + c_first_name, + sales +FROM (SELECT c_last_name, + c_first_name, + Sum(cs_quantity * cs_list_price) sales + FROM catalog_sales, + customer, + date_dim + WHERE d_year = 1998 + AND d_moy = 6 + AND cs_sold_date_sk = d_date_sk + AND cs_item_sk IN (SELECT item_sk + FROM frequent_ss_items) + AND cs_bill_customer_sk IN (SELECT c_customer_sk + FROM best_ss_customer) + AND cs_bill_customer_sk = c_customer_sk + GROUP BY c_last_name, + c_first_name + UNION ALL + SELECT c_last_name, + c_first_name, + Sum(ws_quantity * ws_list_price) sales + FROM web_sales, + customer, + date_dim + WHERE d_year = 1998 + AND d_moy = 6 + AND ws_sold_date_sk = d_date_sk + AND ws_item_sk IN (SELECT item_sk + FROM frequent_ss_items) + AND ws_bill_customer_sk IN (SELECT c_customer_sk + FROM best_ss_customer) + AND ws_bill_customer_sk = c_customer_sk + GROUP BY c_last_name, + c_first_name) +ORDER BY c_last_name, + c_first_name, + sales +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query24.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query24.sql new file mode 100644 index 000000000..8382ca81d --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query24.sql @@ -0,0 +1,96 @@ +-- start query 24 in stream 0 using template query24.tpl +WITH ssales + AS (SELECT c_last_name, + c_first_name, + s_store_name, + ca_state, + s_state, + i_color, + i_current_price, + i_manager_id, + i_units, + i_size, + Sum(ss_net_profit) netpaid + FROM store_sales, + store_returns, + store, + item, + customer, + customer_address + WHERE ss_ticket_number = sr_ticket_number + AND ss_item_sk = sr_item_sk + AND ss_customer_sk = c_customer_sk + AND ss_item_sk = i_item_sk + AND ss_store_sk = s_store_sk + AND c_birth_country = Upper(ca_country) + AND s_zip = ca_zip + AND s_market_id = 6 + GROUP BY c_last_name, + c_first_name, + s_store_name, + ca_state, + s_state, + i_color, + i_current_price, + i_manager_id, + i_units, + i_size) +SELECT c_last_name, + c_first_name, + s_store_name, + Sum(netpaid) paid +FROM ssales +WHERE i_color = 'papaya' +GROUP BY c_last_name, + c_first_name, + s_store_name +HAVING Sum(netpaid) > (SELECT 0.05 * Avg(netpaid) + FROM ssales); + +WITH ssales + AS (SELECT c_last_name, + c_first_name, + s_store_name, + ca_state, + s_state, + i_color, + i_current_price, + i_manager_id, + i_units, + i_size, + Sum(ss_net_profit) netpaid + FROM store_sales, + store_returns, + store, + item, + customer, + customer_address + WHERE ss_ticket_number = sr_ticket_number + AND ss_item_sk = sr_item_sk + AND ss_customer_sk = c_customer_sk + AND ss_item_sk = i_item_sk + AND ss_store_sk = s_store_sk + AND c_birth_country = Upper(ca_country) + AND s_zip = ca_zip + AND s_market_id = 6 + GROUP BY c_last_name, + c_first_name, + s_store_name, + ca_state, + s_state, + i_color, + i_current_price, + i_manager_id, + i_units, + i_size) +SELECT c_last_name, + c_first_name, + s_store_name, + Sum(netpaid) paid +FROM ssales +WHERE i_color = 'chartreuse' +GROUP BY c_last_name, + c_first_name, + s_store_name +HAVING Sum(netpaid) > (SELECT 0.05 * Avg(netpaid) + FROM ssales); diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query25.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query25.sql new file mode 100644 index 000000000..fe58702f6 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query25.sql @@ -0,0 +1,41 @@ +-- start query 25 in stream 0 using template query25.tpl +SELECT i_item_id, + i_item_desc, + s_store_id, + s_store_name, + Max(ss_net_profit) AS store_sales_profit, + Max(sr_net_loss) AS store_returns_loss, + Max(cs_net_profit) AS catalog_sales_profit +FROM store_sales, + store_returns, + catalog_sales, + date_dim d1, + date_dim d2, + date_dim d3, + store, + item +WHERE d1.d_moy = 4 + AND d1.d_year = 2001 + AND d1.d_date_sk = ss_sold_date_sk + AND i_item_sk = ss_item_sk + AND s_store_sk = ss_store_sk + AND ss_customer_sk = sr_customer_sk + AND ss_item_sk = sr_item_sk + AND ss_ticket_number = sr_ticket_number + AND sr_returned_date_sk = d2.d_date_sk + AND d2.d_moy BETWEEN 4 AND 10 + AND d2.d_year = 2001 + AND sr_customer_sk = cs_bill_customer_sk + AND sr_item_sk = cs_item_sk + AND cs_sold_date_sk = d3.d_date_sk + AND d3.d_moy BETWEEN 4 AND 10 + AND d3.d_year = 2001 +GROUP BY i_item_id, + i_item_desc, + s_store_id, + s_store_name +ORDER BY i_item_id, + i_item_desc, + s_store_id, + s_store_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query26.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query26.sql new file mode 100644 index 000000000..d4818a37b --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query26.sql @@ -0,0 +1,24 @@ +-- start query 26 in stream 0 using template query26.tpl +SELECT i_item_id, + Avg(cs_quantity) agg1, + Avg(cs_list_price) agg2, + Avg(cs_coupon_amt) agg3, + Avg(cs_sales_price) agg4 +FROM catalog_sales, + customer_demographics, + date_dim, + item, + promotion +WHERE cs_sold_date_sk = d_date_sk + AND cs_item_sk = i_item_sk + AND cs_bill_cdemo_sk = cd_demo_sk + AND cs_promo_sk = p_promo_sk + AND cd_gender = 'F' + AND cd_marital_status = 'W' + AND cd_education_status = 'Secondary' + AND ( p_channel_email = 'N' + OR p_channel_event = 'N' ) + AND d_year = 2000 +GROUP BY i_item_id +ORDER BY i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query27.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query27.sql new file mode 100644 index 000000000..98fe056e5 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query27.sql @@ -0,0 +1,27 @@ +-- start query 27 in stream 0 using template query27.tpl +SELECT i_item_id, + s_state, + Grouping(s_state) g_state, + Avg(ss_quantity) agg1, + Avg(ss_list_price) agg2, + Avg(ss_coupon_amt) agg3, + Avg(ss_sales_price) agg4 +FROM store_sales, + customer_demographics, + date_dim, + store, + item +WHERE ss_sold_date_sk = d_date_sk + AND ss_item_sk = i_item_sk + AND ss_store_sk = s_store_sk + AND ss_cdemo_sk = cd_demo_sk + AND cd_gender = 'M' + AND cd_marital_status = 'D' + AND cd_education_status = 'College' + AND d_year = 2000 + AND s_state IN ( 'TN', 'TN', 'TN', 'TN', + 'TN', 'TN' ) +GROUP BY rollup ( i_item_id, s_state ) +ORDER BY i_item_id, + s_state +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query28.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query28.sql new file mode 100644 index 000000000..3aa74a9d9 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query28.sql @@ -0,0 +1,51 @@ +-- start query 28 in stream 0 using template query28.tpl +SELECT * +FROM (SELECT Avg(ss_list_price) B1_LP, + Count(ss_list_price) B1_CNT, + Count(DISTINCT ss_list_price) B1_CNTD + FROM store_sales + WHERE ss_quantity BETWEEN 0 AND 5 + AND ( ss_list_price BETWEEN 18 AND 18 + 10 + OR ss_coupon_amt BETWEEN 1939 AND 1939 + 1000 + OR ss_wholesale_cost BETWEEN 34 AND 34 + 20 )) B1, + (SELECT Avg(ss_list_price) B2_LP, + Count(ss_list_price) B2_CNT, + Count(DISTINCT ss_list_price) B2_CNTD + FROM store_sales + WHERE ss_quantity BETWEEN 6 AND 10 + AND ( ss_list_price BETWEEN 1 AND 1 + 10 + OR ss_coupon_amt BETWEEN 35 AND 35 + 1000 + OR ss_wholesale_cost BETWEEN 50 AND 50 + 20 )) B2, + (SELECT Avg(ss_list_price) B3_LP, + Count(ss_list_price) B3_CNT, + Count(DISTINCT ss_list_price) B3_CNTD + FROM store_sales + WHERE ss_quantity BETWEEN 11 AND 15 + AND ( ss_list_price BETWEEN 91 AND 91 + 10 + OR ss_coupon_amt BETWEEN 1412 AND 1412 + 1000 + OR ss_wholesale_cost BETWEEN 17 AND 17 + 20 )) B3, + (SELECT Avg(ss_list_price) B4_LP, + Count(ss_list_price) B4_CNT, + Count(DISTINCT ss_list_price) B4_CNTD + FROM store_sales + WHERE ss_quantity BETWEEN 16 AND 20 + AND ( ss_list_price BETWEEN 9 AND 9 + 10 + OR ss_coupon_amt BETWEEN 5270 AND 5270 + 1000 + OR ss_wholesale_cost BETWEEN 29 AND 29 + 20 )) B4, + (SELECT Avg(ss_list_price) B5_LP, + Count(ss_list_price) B5_CNT, + Count(DISTINCT ss_list_price) B5_CNTD + FROM store_sales + WHERE ss_quantity BETWEEN 21 AND 25 + AND ( ss_list_price BETWEEN 45 AND 45 + 10 + OR ss_coupon_amt BETWEEN 826 AND 826 + 1000 + OR ss_wholesale_cost BETWEEN 5 AND 5 + 20 )) B5, + (SELECT Avg(ss_list_price) B6_LP, + Count(ss_list_price) B6_CNT, + Count(DISTINCT ss_list_price) B6_CNTD + FROM store_sales + WHERE ss_quantity BETWEEN 26 AND 30 + AND ( ss_list_price BETWEEN 174 AND 174 + 10 + OR ss_coupon_amt BETWEEN 5548 AND 5548 + 1000 + OR ss_wholesale_cost BETWEEN 42 AND 42 + 20 )) B6 +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query29.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query29.sql new file mode 100644 index 000000000..b685e6179 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query29.sql @@ -0,0 +1,40 @@ +-- start query 29 in stream 0 using template query29.tpl +SELECT i_item_id, + i_item_desc, + s_store_id, + s_store_name, + Avg(ss_quantity) AS store_sales_quantity, + Avg(sr_return_quantity) AS store_returns_quantity, + Avg(cs_quantity) AS catalog_sales_quantity +FROM store_sales, + store_returns, + catalog_sales, + date_dim d1, + date_dim d2, + date_dim d3, + store, + item +WHERE d1.d_moy = 4 + AND d1.d_year = 1998 + AND d1.d_date_sk = ss_sold_date_sk + AND i_item_sk = ss_item_sk + AND s_store_sk = ss_store_sk + AND ss_customer_sk = sr_customer_sk + AND ss_item_sk = sr_item_sk + AND ss_ticket_number = sr_ticket_number + AND sr_returned_date_sk = d2.d_date_sk + AND d2.d_moy BETWEEN 4 AND 4 + 3 + AND d2.d_year = 1998 + AND sr_customer_sk = cs_bill_customer_sk + AND sr_item_sk = cs_item_sk + AND cs_sold_date_sk = d3.d_date_sk + AND d3.d_year IN ( 1998, 1998 + 1, 1998 + 2 ) +GROUP BY i_item_id, + i_item_desc, + s_store_id, + s_store_name +ORDER BY i_item_id, + i_item_desc, + s_store_id, + s_store_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query3.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query3.sql new file mode 100644 index 000000000..711b0fe7c --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query3.sql @@ -0,0 +1,19 @@ +-- start query 3 in stream 0 using template query3.tpl +SELECT dt.d_year, + item.i_brand_id brand_id, + item.i_brand brand, + Sum(ss_ext_discount_amt) sum_agg +FROM date_dim dt, + store_sales, + item +WHERE dt.d_date_sk = store_sales.ss_sold_date_sk + AND store_sales.ss_item_sk = item.i_item_sk + AND item.i_manufact_id = 427 + AND dt.d_moy = 11 +GROUP BY dt.d_year, + item.i_brand, + item.i_brand_id +ORDER BY dt.d_year, + sum_agg DESC, + brand_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query30.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query30.sql new file mode 100644 index 000000000..4b0498f72 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query30.sql @@ -0,0 +1,49 @@ +-- start query 30 in stream 0 using template query30.tpl +WITH customer_total_return + AS (SELECT wr_returning_customer_sk AS ctr_customer_sk, + ca_state AS ctr_state, + Sum(wr_return_amt) AS ctr_total_return + FROM web_returns, + date_dim, + customer_address + WHERE wr_returned_date_sk = d_date_sk + AND d_year = 2000 + AND wr_returning_addr_sk = ca_address_sk + GROUP BY wr_returning_customer_sk, + ca_state) +SELECT c_customer_id, + c_salutation, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_day, + c_birth_month, + c_birth_year, + c_birth_country, + c_login, + c_email_address, + c_last_review_date, + ctr_total_return +FROM customer_total_return ctr1, + customer_address, + customer +WHERE ctr1.ctr_total_return > (SELECT Avg(ctr_total_return) * 1.2 + FROM customer_total_return ctr2 + WHERE ctr1.ctr_state = ctr2.ctr_state) + AND ca_address_sk = c_current_addr_sk + AND ca_state = 'IN' + AND ctr1.ctr_customer_sk = c_customer_sk +ORDER BY c_customer_id, + c_salutation, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_day, + c_birth_month, + c_birth_year, + c_birth_country, + c_login, + c_email_address, + c_last_review_date, + ctr_total_return +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query31.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query31.sql new file mode 100644 index 000000000..8ab3ffb41 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query31.sql @@ -0,0 +1,73 @@ +-- start query 31 in stream 0 using template query31.tpl +WITH ss + AS (SELECT ca_county, + d_qoy, + d_year, + Sum(ss_ext_sales_price) AS store_sales + FROM store_sales, + date_dim, + customer_address + WHERE ss_sold_date_sk = d_date_sk + AND ss_addr_sk = ca_address_sk + GROUP BY ca_county, + d_qoy, + d_year), + ws + AS (SELECT ca_county, + d_qoy, + d_year, + Sum(ws_ext_sales_price) AS web_sales + FROM web_sales, + date_dim, + customer_address + WHERE ws_sold_date_sk = d_date_sk + AND ws_bill_addr_sk = ca_address_sk + GROUP BY ca_county, + d_qoy, + d_year) +SELECT ss1.ca_county, + ss1.d_year, + ws2.web_sales / ws1.web_sales web_q1_q2_increase, + ss2.store_sales / ss1.store_sales store_q1_q2_increase, + ws3.web_sales / ws2.web_sales web_q2_q3_increase, + ss3.store_sales / ss2.store_sales store_q2_q3_increase +FROM ss ss1, + ss ss2, + ss ss3, + ws ws1, + ws ws2, + ws ws3 +WHERE ss1.d_qoy = 1 + AND ss1.d_year = 2001 + AND ss1.ca_county = ss2.ca_county + AND ss2.d_qoy = 2 + AND ss2.d_year = 2001 + AND ss2.ca_county = ss3.ca_county + AND ss3.d_qoy = 3 + AND ss3.d_year = 2001 + AND ss1.ca_county = ws1.ca_county + AND ws1.d_qoy = 1 + AND ws1.d_year = 2001 + AND ws1.ca_county = ws2.ca_county + AND ws2.d_qoy = 2 + AND ws2.d_year = 2001 + AND ws1.ca_county = ws3.ca_county + AND ws3.d_qoy = 3 + AND ws3.d_year = 2001 + AND CASE + WHEN ws1.web_sales > 0 THEN ws2.web_sales / ws1.web_sales + ELSE NULL + END > CASE + WHEN ss1.store_sales > 0 THEN + ss2.store_sales / ss1.store_sales + ELSE NULL + END + AND CASE + WHEN ws2.web_sales > 0 THEN ws3.web_sales / ws2.web_sales + ELSE NULL + END > CASE + WHEN ss2.store_sales > 0 THEN + ss3.store_sales / ss2.store_sales + ELSE NULL + END +ORDER BY ss1.d_year; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query32.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query32.sql new file mode 100644 index 000000000..560c1e7cd --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query32.sql @@ -0,0 +1,21 @@ +-- start query 32 in stream 0 using template query32.tpl +SELECT + Sum(cs_ext_discount_amt) AS `excess discount amount` +FROM catalog_sales , + item , + date_dim +WHERE i_manufact_id = 610 +AND i_item_sk = cs_item_sk +AND d_date BETWEEN '2001-03-04' AND ( + Cast('2001-03-04' AS DATE) + INTERVAL '90' day) +AND d_date_sk = cs_sold_date_sk +AND cs_ext_discount_amt > + ( + SELECT 1.3 * avg(cs_ext_discount_amt) + FROM catalog_sales , + date_dim + WHERE cs_item_sk = i_item_sk + AND d_date BETWEEN '2001-03-04' AND ( + cast('2001-03-04' AS date) + INTERVAL '90' day) + AND d_date_sk = cs_sold_date_sk ) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query33.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query33.sql new file mode 100644 index 000000000..c4161054a --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query33.sql @@ -0,0 +1,65 @@ +-- start query 33 in stream 0 using template query33.tpl +WITH ss + AS (SELECT i_manufact_id, + Sum(ss_ext_sales_price) total_sales + FROM store_sales, + date_dim, + customer_address, + item + WHERE i_manufact_id IN (SELECT i_manufact_id + FROM item + WHERE i_category IN ( 'Books' )) + AND ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 3 + AND ss_addr_sk = ca_address_sk + AND ca_gmt_offset = -5 + GROUP BY i_manufact_id), + cs + AS (SELECT i_manufact_id, + Sum(cs_ext_sales_price) total_sales + FROM catalog_sales, + date_dim, + customer_address, + item + WHERE i_manufact_id IN (SELECT i_manufact_id + FROM item + WHERE i_category IN ( 'Books' )) + AND cs_item_sk = i_item_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 3 + AND cs_bill_addr_sk = ca_address_sk + AND ca_gmt_offset = -5 + GROUP BY i_manufact_id), + ws + AS (SELECT i_manufact_id, + Sum(ws_ext_sales_price) total_sales + FROM web_sales, + date_dim, + customer_address, + item + WHERE i_manufact_id IN (SELECT i_manufact_id + FROM item + WHERE i_category IN ( 'Books' )) + AND ws_item_sk = i_item_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 3 + AND ws_bill_addr_sk = ca_address_sk + AND ca_gmt_offset = -5 + GROUP BY i_manufact_id) +SELECT i_manufact_id, + Sum(total_sales) total_sales +FROM (SELECT * + FROM ss + UNION ALL + SELECT * + FROM cs + UNION ALL + SELECT * + FROM ws) tmp1 +GROUP BY i_manufact_id +ORDER BY total_sales +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query34.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query34.sql new file mode 100644 index 000000000..613734ffa --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query34.sql @@ -0,0 +1,46 @@ +-- start query 34 in stream 0 using template query34.tpl +SELECT c_last_name, + c_first_name, + c_salutation, + c_preferred_cust_flag, + ss_ticket_number, + cnt +FROM (SELECT ss_ticket_number, + ss_customer_sk, + Count(*) cnt + FROM store_sales, + date_dim, + store, + household_demographics + WHERE store_sales.ss_sold_date_sk = date_dim.d_date_sk + AND store_sales.ss_store_sk = store.s_store_sk + AND store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk + AND ( date_dim.d_dom BETWEEN 1 AND 3 + OR date_dim.d_dom BETWEEN 25 AND 28 ) + AND ( household_demographics.hd_buy_potential = '>10000' + OR household_demographics.hd_buy_potential = 'unknown' ) + AND household_demographics.hd_vehicle_count > 0 + AND ( CASE + WHEN household_demographics.hd_vehicle_count > 0 THEN + household_demographics.hd_dep_count / + household_demographics.hd_vehicle_count + ELSE NULL + END ) > 1.2 + AND date_dim.d_year IN ( 1999, 1999 + 1, 1999 + 2 ) + AND store.s_county IN ( 'Williamson County', 'Williamson County', + 'Williamson County', + 'Williamson County' + , + 'Williamson County', 'Williamson County', + 'Williamson County', + 'Williamson County' + ) + GROUP BY ss_ticket_number, + ss_customer_sk) dn, + customer +WHERE ss_customer_sk = c_customer_sk + AND cnt BETWEEN 15 AND 20 +ORDER BY c_last_name, + c_first_name, + c_salutation, + c_preferred_cust_flag DESC; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query35.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query35.sql new file mode 100644 index 000000000..d5912a411 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query35.sql @@ -0,0 +1,58 @@ +-- start query 35 in stream 0 using template query35.tpl +SELECT ca_state, + cd_gender, + cd_marital_status, + cd_dep_count, + Count(*) cnt1, + Stddev_samp(cd_dep_count), + Avg(cd_dep_count), + Max(cd_dep_count), + cd_dep_employed_count, + Count(*) cnt2, + Stddev_samp(cd_dep_employed_count), + Avg(cd_dep_employed_count), + Max(cd_dep_employed_count), + cd_dep_college_count, + Count(*) cnt3, + Stddev_samp(cd_dep_college_count), + Avg(cd_dep_college_count), + Max(cd_dep_college_count) +FROM customer c, + customer_address ca, + customer_demographics +WHERE c.c_current_addr_sk = ca.ca_address_sk + AND cd_demo_sk = c.c_current_cdemo_sk + AND EXISTS (SELECT * + FROM store_sales, + date_dim + WHERE c.c_customer_sk = ss_customer_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 2001 + AND d_qoy < 4) + AND ( EXISTS (SELECT * + FROM web_sales, + date_dim + WHERE c.c_customer_sk = ws_bill_customer_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 2001 + AND d_qoy < 4) + OR EXISTS (SELECT * + FROM catalog_sales, + date_dim + WHERE c.c_customer_sk = cs_ship_customer_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 2001 + AND d_qoy < 4) ) +GROUP BY ca_state, + cd_gender, + cd_marital_status, + cd_dep_count, + cd_dep_employed_count, + cd_dep_college_count +ORDER BY ca_state, + cd_gender, + cd_marital_status, + cd_dep_count, + cd_dep_employed_count, + cd_dep_college_count +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query36.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query36.sql new file mode 100644 index 000000000..21d69fbbb --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query36.sql @@ -0,0 +1,31 @@ +-- start query 36 in stream 0 using template query36.tpl +SELECT Sum(ss_net_profit) / Sum(ss_ext_sales_price) AS + gross_margin, + i_category, + i_class, + Grouping(i_category) + Grouping(i_class) AS + lochierarchy, + Rank() + OVER ( + partition BY Grouping(i_category)+Grouping(i_class), CASE + WHEN Grouping( + i_class) = 0 THEN i_category END + ORDER BY Sum(ss_net_profit)/Sum(ss_ext_sales_price) ASC) AS + rank_within_parent +FROM store_sales, + date_dim d1, + item, + store +WHERE d1.d_year = 2000 + AND d1.d_date_sk = ss_sold_date_sk + AND i_item_sk = ss_item_sk + AND s_store_sk = ss_store_sk + AND s_state IN ( 'TN', 'TN', 'TN', 'TN', + 'TN', 'TN', 'TN', 'TN' ) +GROUP BY rollup( i_category, i_class ) +ORDER BY lochierarchy DESC, + CASE + WHEN lochierarchy = 0 THEN i_category + END, + rank_within_parent +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query37.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query37.sql new file mode 100644 index 000000000..52cd153a9 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query37.sql @@ -0,0 +1,22 @@ +-- start query 37 in stream 0 using template query37.tpl +SELECT + i_item_id , + i_item_desc , + i_current_price +FROM item, + inventory, + date_dim, + catalog_sales +WHERE i_current_price BETWEEN 20 AND 20 + 30 +AND inv_item_sk = i_item_sk +AND d_date_sk=inv_date_sk +AND d_date BETWEEN Cast('1999-03-06' AS DATE) AND ( + Cast('1999-03-06' AS DATE) + INTERVAL '60' day) +AND i_manufact_id IN (843,815,850,840) +AND inv_quantity_on_hand BETWEEN 100 AND 500 +AND cs_item_sk = i_item_sk +GROUP BY i_item_id, + i_item_desc, + i_current_price +ORDER BY i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query38.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query38.sql new file mode 100644 index 000000000..546068717 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query38.sql @@ -0,0 +1,32 @@ +-- start query 38 in stream 0 using template query38.tpl +SELECT Count(*) +FROM (SELECT DISTINCT c_last_name, + c_first_name, + d_date + FROM store_sales, + date_dim, + customer + WHERE store_sales.ss_sold_date_sk = date_dim.d_date_sk + AND store_sales.ss_customer_sk = customer.c_customer_sk + AND d_month_seq BETWEEN 1188 AND 1188 + 11 + INTERSECT + SELECT DISTINCT c_last_name, + c_first_name, + d_date + FROM catalog_sales, + date_dim, + customer + WHERE catalog_sales.cs_sold_date_sk = date_dim.d_date_sk + AND catalog_sales.cs_bill_customer_sk = customer.c_customer_sk + AND d_month_seq BETWEEN 1188 AND 1188 + 11 + INTERSECT + SELECT DISTINCT c_last_name, + c_first_name, + d_date + FROM web_sales, + date_dim, + customer + WHERE web_sales.ws_sold_date_sk = date_dim.d_date_sk + AND web_sales.ws_bill_customer_sk = customer.c_customer_sk + AND d_month_seq BETWEEN 1188 AND 1188 + 11) hot_cust +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query39.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query39.sql new file mode 100644 index 000000000..4abb67d30 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query39.sql @@ -0,0 +1,117 @@ +-- start query 39 in stream 0 using template query39.tpl +WITH inv + AS (SELECT w_warehouse_name, + w_warehouse_sk, + i_item_sk, + d_moy, + stdev, + mean, + CASE mean + WHEN 0 THEN NULL + ELSE stdev / mean + END cov + FROM (SELECT w_warehouse_name, + w_warehouse_sk, + i_item_sk, + d_moy, + Stddev_samp(inv_quantity_on_hand) stdev, + Avg(inv_quantity_on_hand) mean + FROM inventory, + item, + warehouse, + date_dim + WHERE inv_item_sk = i_item_sk + AND inv_warehouse_sk = w_warehouse_sk + AND inv_date_sk = d_date_sk + AND d_year = 2002 + GROUP BY w_warehouse_name, + w_warehouse_sk, + i_item_sk, + d_moy) foo + WHERE CASE mean + WHEN 0 THEN 0 + ELSE stdev / mean + END > 1) +SELECT inv1.w_warehouse_sk, + inv1.i_item_sk, + inv1.d_moy, + inv1.mean, + inv1.cov, + inv2.w_warehouse_sk, + inv2.i_item_sk, + inv2.d_moy, + inv2.mean, + inv2.cov +FROM inv inv1, + inv inv2 +WHERE inv1.i_item_sk = inv2.i_item_sk + AND inv1.w_warehouse_sk = inv2.w_warehouse_sk + AND inv1.d_moy = 1 + AND inv2.d_moy = 1 + 1 +ORDER BY inv1.w_warehouse_sk, + inv1.i_item_sk, + inv1.d_moy, + inv1.mean, + inv1.cov, + inv2.d_moy, + inv2.mean, + inv2.cov; + +WITH inv + AS (SELECT w_warehouse_name, + w_warehouse_sk, + i_item_sk, + d_moy, + stdev, + mean, + CASE mean + WHEN 0 THEN NULL + ELSE stdev / mean + END cov + FROM (SELECT w_warehouse_name, + w_warehouse_sk, + i_item_sk, + d_moy, + Stddev_samp(inv_quantity_on_hand) stdev, + Avg(inv_quantity_on_hand) mean + FROM inventory, + item, + warehouse, + date_dim + WHERE inv_item_sk = i_item_sk + AND inv_warehouse_sk = w_warehouse_sk + AND inv_date_sk = d_date_sk + AND d_year = 2002 + GROUP BY w_warehouse_name, + w_warehouse_sk, + i_item_sk, + d_moy) foo + WHERE CASE mean + WHEN 0 THEN 0 + ELSE stdev / mean + END > 1) +SELECT inv1.w_warehouse_sk, + inv1.i_item_sk, + inv1.d_moy, + inv1.mean, + inv1.cov, + inv2.w_warehouse_sk, + inv2.i_item_sk, + inv2.d_moy, + inv2.mean, + inv2.cov +FROM inv inv1, + inv inv2 +WHERE inv1.i_item_sk = inv2.i_item_sk + AND inv1.w_warehouse_sk = inv2.w_warehouse_sk + AND inv1.d_moy = 1 + AND inv2.d_moy = 1 + 1 + AND inv1.cov > 1.5 +ORDER BY inv1.w_warehouse_sk, + inv1.i_item_sk, + inv1.d_moy, + inv1.mean, + inv1.cov, + inv2.d_moy, + inv2.mean, + inv2.cov; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query4.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query4.sql new file mode 100644 index 000000000..281b2d5d2 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query4.sql @@ -0,0 +1,152 @@ +-- start query 4 in stream 0 using template query4.tpl +WITH year_total + AS (SELECT c_customer_id customer_id, + c_first_name customer_first_name, + c_last_name customer_last_name, + c_preferred_cust_flag customer_preferred_cust_flag + , + c_birth_country + customer_birth_country, + c_login customer_login, + c_email_address customer_email_address, + d_year dyear, + Sum(( ( ss_ext_list_price - ss_ext_wholesale_cost + - ss_ext_discount_amt + ) + + + ss_ext_sales_price ) / 2) year_total, + 's' sale_type + FROM customer, + store_sales, + date_dim + WHERE c_customer_sk = ss_customer_sk + AND ss_sold_date_sk = d_date_sk + GROUP BY c_customer_id, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_country, + c_login, + c_email_address, + d_year + UNION ALL + SELECT c_customer_id customer_id, + c_first_name customer_first_name, + c_last_name customer_last_name, + c_preferred_cust_flag + customer_preferred_cust_flag, + c_birth_country customer_birth_country + , + c_login + customer_login, + c_email_address customer_email_address + , + d_year dyear + , + Sum(( ( ( cs_ext_list_price + - cs_ext_wholesale_cost + - cs_ext_discount_amt + ) + + cs_ext_sales_price ) / 2 )) year_total, + 'c' sale_type + FROM customer, + catalog_sales, + date_dim + WHERE c_customer_sk = cs_bill_customer_sk + AND cs_sold_date_sk = d_date_sk + GROUP BY c_customer_id, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_country, + c_login, + c_email_address, + d_year + UNION ALL + SELECT c_customer_id customer_id, + c_first_name customer_first_name, + c_last_name customer_last_name, + c_preferred_cust_flag + customer_preferred_cust_flag, + c_birth_country customer_birth_country + , + c_login + customer_login, + c_email_address customer_email_address + , + d_year dyear + , + Sum(( ( ( ws_ext_list_price + - ws_ext_wholesale_cost + - ws_ext_discount_amt + ) + + ws_ext_sales_price ) / 2 )) year_total, + 'w' sale_type + FROM customer, + web_sales, + date_dim + WHERE c_customer_sk = ws_bill_customer_sk + AND ws_sold_date_sk = d_date_sk + GROUP BY c_customer_id, + c_first_name, + c_last_name, + c_preferred_cust_flag, + c_birth_country, + c_login, + c_email_address, + d_year) +SELECT t_s_secyear.customer_id, + t_s_secyear.customer_first_name, + t_s_secyear.customer_last_name, + t_s_secyear.customer_preferred_cust_flag +FROM year_total t_s_firstyear, + year_total t_s_secyear, + year_total t_c_firstyear, + year_total t_c_secyear, + year_total t_w_firstyear, + year_total t_w_secyear +WHERE t_s_secyear.customer_id = t_s_firstyear.customer_id + AND t_s_firstyear.customer_id = t_c_secyear.customer_id + AND t_s_firstyear.customer_id = t_c_firstyear.customer_id + AND t_s_firstyear.customer_id = t_w_firstyear.customer_id + AND t_s_firstyear.customer_id = t_w_secyear.customer_id + AND t_s_firstyear.sale_type = 's' + AND t_c_firstyear.sale_type = 'c' + AND t_w_firstyear.sale_type = 'w' + AND t_s_secyear.sale_type = 's' + AND t_c_secyear.sale_type = 'c' + AND t_w_secyear.sale_type = 'w' + AND t_s_firstyear.dyear = 2001 + AND t_s_secyear.dyear = 2001 + 1 + AND t_c_firstyear.dyear = 2001 + AND t_c_secyear.dyear = 2001 + 1 + AND t_w_firstyear.dyear = 2001 + AND t_w_secyear.dyear = 2001 + 1 + AND t_s_firstyear.year_total > 0 + AND t_c_firstyear.year_total > 0 + AND t_w_firstyear.year_total > 0 + AND CASE + WHEN t_c_firstyear.year_total > 0 THEN t_c_secyear.year_total / + t_c_firstyear.year_total + ELSE NULL + END > CASE + WHEN t_s_firstyear.year_total > 0 THEN + t_s_secyear.year_total / + t_s_firstyear.year_total + ELSE NULL + END + AND CASE + WHEN t_c_firstyear.year_total > 0 THEN t_c_secyear.year_total / + t_c_firstyear.year_total + ELSE NULL + END > CASE + WHEN t_w_firstyear.year_total > 0 THEN + t_w_secyear.year_total / + t_w_firstyear.year_total + ELSE NULL + END +ORDER BY t_s_secyear.customer_id, + t_s_secyear.customer_first_name, + t_s_secyear.customer_last_name, + t_s_secyear.customer_preferred_cust_flag +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query40.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query40.sql new file mode 100644 index 000000000..f7f84b873 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query40.sql @@ -0,0 +1,35 @@ +-- start query 40 in stream 0 using template query40.tpl +SELECT + w_state , + i_item_id , + Sum( + CASE + WHEN ( + Cast(d_date AS DATE) < Cast ('2002-06-01' AS DATE)) THEN cs_sales_price - COALESCE(cr_refunded_cash,0) + ELSE 0 + END) AS sales_before , + Sum( + CASE + WHEN ( + Cast(d_date AS DATE) >= Cast ('2002-06-01' AS DATE)) THEN cs_sales_price - COALESCE(cr_refunded_cash,0) + ELSE 0 + END) AS sales_after +FROM catalog_sales +LEFT OUTER JOIN catalog_returns +ON ( + cs_order_number = cr_order_number + AND cs_item_sk = cr_item_sk) , + warehouse , + item , + date_dim +WHERE i_current_price BETWEEN 0.99 AND 1.49 +AND i_item_sk = cs_item_sk +AND cs_warehouse_sk = w_warehouse_sk +AND cs_sold_date_sk = d_date_sk +AND d_date BETWEEN (Cast ('2002-06-01' AS DATE) - INTERVAL '30' day) AND ( + cast ('2002-06-01' AS date) + INTERVAL '30' day) +GROUP BY w_state, + i_item_id +ORDER BY w_state, + i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query41.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query41.sql new file mode 100644 index 000000000..467ed35ae --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query41.sql @@ -0,0 +1,66 @@ +-- start query 41 in stream 0 using template query41.tpl +SELECT Distinct(i_product_name) +FROM item i1 +WHERE i_manufact_id BETWEEN 765 AND 765 + 40 + AND (SELECT Count(*) AS item_cnt + FROM item + WHERE ( i_manufact = i1.i_manufact + AND ( ( i_category = 'Women' + AND ( i_color = 'dim' + OR i_color = 'green' ) + AND ( i_units = 'Gross' + OR i_units = 'Dozen' ) + AND ( i_size = 'economy' + OR i_size = 'petite' ) ) + OR ( i_category = 'Women' + AND ( i_color = 'navajo' + OR i_color = 'aquamarine' ) + AND ( i_units = 'Case' + OR i_units = 'Unknown' ) + AND ( i_size = 'large' + OR i_size = 'N/A' ) ) + OR ( i_category = 'Men' + AND ( i_color = 'indian' + OR i_color = 'dark' ) + AND ( i_units = 'Oz' + OR i_units = 'Lb' ) + AND ( i_size = 'extra large' + OR i_size = 'small' ) ) + OR ( i_category = 'Men' + AND ( i_color = 'peach' + OR i_color = 'purple' ) + AND ( i_units = 'Tbl' + OR i_units = 'Bunch' ) + AND ( i_size = 'economy' + OR i_size = 'petite' ) ) ) ) + OR ( i_manufact = i1.i_manufact + AND ( ( i_category = 'Women' + AND ( i_color = 'orchid' + OR i_color = 'peru' ) + AND ( i_units = 'Carton' + OR i_units = 'Cup' ) + AND ( i_size = 'economy' + OR i_size = 'petite' ) ) + OR ( i_category = 'Women' + AND ( i_color = 'violet' + OR i_color = 'papaya' ) + AND ( i_units = 'Ounce' + OR i_units = 'Box' ) + AND ( i_size = 'large' + OR i_size = 'N/A' ) ) + OR ( i_category = 'Men' + AND ( i_color = 'drab' + OR i_color = 'grey' ) + AND ( i_units = 'Each' + OR i_units = 'N/A' ) + AND ( i_size = 'extra large' + OR i_size = 'small' ) ) + OR ( i_category = 'Men' + AND ( i_color = 'chocolate' + OR i_color = 'antique' ) + AND ( i_units = 'Dram' + OR i_units = 'Gram' ) + AND ( i_size = 'economy' + OR i_size = 'petite' ) ) ) )) > 0 +ORDER BY i_product_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query42.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query42.sql new file mode 100644 index 000000000..706d3c6f1 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query42.sql @@ -0,0 +1,21 @@ +-- start query 42 in stream 0 using template query42.tpl +SELECT dt.d_year, + item.i_category_id, + item.i_category, + Sum(ss_ext_sales_price) +FROM date_dim dt, + store_sales, + item +WHERE dt.d_date_sk = store_sales.ss_sold_date_sk + AND store_sales.ss_item_sk = item.i_item_sk + AND item.i_manager_id = 1 + AND dt.d_moy = 12 + AND dt.d_year = 2000 +GROUP BY dt.d_year, + item.i_category_id, + item.i_category +ORDER BY Sum(ss_ext_sales_price) DESC, + dt.d_year, + item.i_category_id, + item.i_category +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query43.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query43.sql new file mode 100644 index 000000000..e7624a19c --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query43.sql @@ -0,0 +1,50 @@ +-- start query 43 in stream 0 using template query43.tpl +SELECT s_store_name, + s_store_id, + Sum(CASE + WHEN ( d_day_name = 'Sunday' ) THEN ss_sales_price + ELSE NULL + END) sun_sales, + Sum(CASE + WHEN ( d_day_name = 'Monday' ) THEN ss_sales_price + ELSE NULL + END) mon_sales, + Sum(CASE + WHEN ( d_day_name = 'Tuesday' ) THEN ss_sales_price + ELSE NULL + END) tue_sales, + Sum(CASE + WHEN ( d_day_name = 'Wednesday' ) THEN ss_sales_price + ELSE NULL + END) wed_sales, + Sum(CASE + WHEN ( d_day_name = 'Thursday' ) THEN ss_sales_price + ELSE NULL + END) thu_sales, + Sum(CASE + WHEN ( d_day_name = 'Friday' ) THEN ss_sales_price + ELSE NULL + END) fri_sales, + Sum(CASE + WHEN ( d_day_name = 'Saturday' ) THEN ss_sales_price + ELSE NULL + END) sat_sales +FROM date_dim, + store_sales, + store +WHERE d_date_sk = ss_sold_date_sk + AND s_store_sk = ss_store_sk + AND s_gmt_offset = -5 + AND d_year = 2002 +GROUP BY s_store_name, + s_store_id +ORDER BY s_store_name, + s_store_id, + sun_sales, + mon_sales, + tue_sales, + wed_sales, + thu_sales, + fri_sales, + sat_sales +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query44.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query44.sql new file mode 100644 index 000000000..ba869d3ce --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query44.sql @@ -0,0 +1,51 @@ +-- start query 44 in stream 0 using template query44.tpl +SELECT asceding.rnk, + i1.i_product_name best_performing, + i2.i_product_name worst_performing +FROM (SELECT * + FROM (SELECT item_sk, + Rank() + OVER ( + ORDER BY rank_col ASC) rnk + FROM (SELECT ss_item_sk item_sk, + Avg(ss_net_profit) rank_col + FROM store_sales ss1 + WHERE ss_store_sk = 4 + GROUP BY ss_item_sk + HAVING Avg(ss_net_profit) > 0.9 * + (SELECT Avg(ss_net_profit) + rank_col + FROM store_sales + WHERE ss_store_sk = 4 + AND ss_cdemo_sk IS + NULL + GROUP BY ss_store_sk))V1) + V11 + WHERE rnk < 11) asceding, + (SELECT * + FROM (SELECT item_sk, + Rank() + OVER ( + ORDER BY rank_col DESC) rnk + FROM (SELECT ss_item_sk item_sk, + Avg(ss_net_profit) rank_col + FROM store_sales ss1 + WHERE ss_store_sk = 4 + GROUP BY ss_item_sk + HAVING Avg(ss_net_profit) > 0.9 * + (SELECT Avg(ss_net_profit) + rank_col + FROM store_sales + WHERE ss_store_sk = 4 + AND ss_cdemo_sk IS + NULL + GROUP BY ss_store_sk))V2) + V21 + WHERE rnk < 11) descending, + item i1, + item i2 +WHERE asceding.rnk = descending.rnk + AND i1.i_item_sk = asceding.item_sk + AND i2.i_item_sk = descending.item_sk +ORDER BY asceding.rnk +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query45.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query45.sql new file mode 100644 index 000000000..e8d35c1c0 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query45.sql @@ -0,0 +1,28 @@ +-- start query 45 in stream 0 using template query45.tpl +SELECT ca_zip, + ca_state, + Sum(ws_sales_price) +FROM web_sales, + customer, + customer_address, + date_dim, + item +WHERE ws_bill_customer_sk = c_customer_sk + AND c_current_addr_sk = ca_address_sk + AND ws_item_sk = i_item_sk + AND ( Substr(ca_zip, 1, 5) IN ( '85669', '86197', '88274', '83405', + '86475', '85392', '85460', '80348', + '81792' ) + OR i_item_id IN (SELECT i_item_id + FROM item + WHERE i_item_sk IN ( 2, 3, 5, 7, + 11, 13, 17, 19, + 23, 29 )) ) + AND ws_sold_date_sk = d_date_sk + AND d_qoy = 1 + AND d_year = 2000 +GROUP BY ca_zip, + ca_state +ORDER BY ca_zip, + ca_state +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query46.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query46.sql new file mode 100644 index 000000000..b88b36400 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query46.sql @@ -0,0 +1,44 @@ +-- start query 46 in stream 0 using template query46.tpl +SELECT c_last_name, + c_first_name, + ca_city, + bought_city, + ss_ticket_number, + amt, + profit +FROM (SELECT ss_ticket_number, + ss_customer_sk, + ca_city bought_city, + Sum(ss_coupon_amt) amt, + Sum(ss_net_profit) profit + FROM store_sales, + date_dim, + store, + household_demographics, + customer_address + WHERE store_sales.ss_sold_date_sk = date_dim.d_date_sk + AND store_sales.ss_store_sk = store.s_store_sk + AND store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk + AND store_sales.ss_addr_sk = customer_address.ca_address_sk + AND ( household_demographics.hd_dep_count = 6 + OR household_demographics.hd_vehicle_count = 0 ) + AND date_dim.d_dow IN ( 6, 0 ) + AND date_dim.d_year IN ( 2000, 2000 + 1, 2000 + 2 ) + AND store.s_city IN ( 'Midway', 'Fairview', 'Fairview', + 'Fairview', + 'Fairview' ) + GROUP BY ss_ticket_number, + ss_customer_sk, + ss_addr_sk, + ca_city) dn, + customer, + customer_address current_addr +WHERE ss_customer_sk = c_customer_sk + AND customer.c_current_addr_sk = current_addr.ca_address_sk + AND current_addr.ca_city <> bought_city +ORDER BY c_last_name, + c_first_name, + ca_city, + bought_city, + ss_ticket_number +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query47.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query47.sql new file mode 100644 index 000000000..36ddcbbcb --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query47.sql @@ -0,0 +1,72 @@ +-- start query 47 in stream 0 using template query47.tpl +WITH v1 + AS (SELECT i_category, + i_brand, + s_store_name, + s_company_name, + d_year, + d_moy, + Sum(ss_sales_price) sum_sales, + Avg(Sum(ss_sales_price)) + OVER ( + partition BY i_category, i_brand, s_store_name, + s_company_name, + d_year) + avg_monthly_sales, + Rank() + OVER ( + partition BY i_category, i_brand, s_store_name, + s_company_name + ORDER BY d_year, d_moy) rn + FROM item, + store_sales, + date_dim, + store + WHERE ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND ss_store_sk = s_store_sk + AND ( d_year = 1999 + OR ( d_year = 1999 - 1 + AND d_moy = 12 ) + OR ( d_year = 1999 + 1 + AND d_moy = 1 ) ) + GROUP BY i_category, + i_brand, + s_store_name, + s_company_name, + d_year, + d_moy), + v2 + AS (SELECT v1.i_category, + v1.d_year, + v1.d_moy, + v1.avg_monthly_sales, + v1.sum_sales, + v1_lag.sum_sales psum, + v1_lead.sum_sales nsum + FROM v1, + v1 v1_lag, + v1 v1_lead + WHERE v1.i_category = v1_lag.i_category + AND v1.i_category = v1_lead.i_category + AND v1.i_brand = v1_lag.i_brand + AND v1.i_brand = v1_lead.i_brand + AND v1.s_store_name = v1_lag.s_store_name + AND v1.s_store_name = v1_lead.s_store_name + AND v1.s_company_name = v1_lag.s_company_name + AND v1.s_company_name = v1_lead.s_company_name + AND v1.rn = v1_lag.rn + 1 + AND v1.rn = v1_lead.rn - 1) +SELECT * +FROM v2 +WHERE d_year = 1999 + AND avg_monthly_sales > 0 + AND CASE + WHEN avg_monthly_sales > 0 THEN Abs(sum_sales - avg_monthly_sales) + / + avg_monthly_sales + ELSE NULL + END > 0.1 +ORDER BY sum_sales - avg_monthly_sales, + 3 +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query48.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query48.sql new file mode 100644 index 000000000..aa8375382 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query48.sql @@ -0,0 +1,34 @@ +-- start query 48 in stream 0 using template query48.tpl +SELECT Sum (ss_quantity) +FROM store_sales, + store, + customer_demographics, + customer_address, + date_dim +WHERE s_store_sk = ss_store_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 1999 + AND ( ( cd_demo_sk = ss_cdemo_sk + AND cd_marital_status = 'W' + AND cd_education_status = 'Secondary' + AND ss_sales_price BETWEEN 100.00 AND 150.00 ) + OR ( cd_demo_sk = ss_cdemo_sk + AND cd_marital_status = 'M' + AND cd_education_status = 'Advanced Degree' + AND ss_sales_price BETWEEN 50.00 AND 100.00 ) + OR ( cd_demo_sk = ss_cdemo_sk + AND cd_marital_status = 'D' + AND cd_education_status = '2 yr Degree' + AND ss_sales_price BETWEEN 150.00 AND 200.00 ) ) + AND ( ( ss_addr_sk = ca_address_sk + AND ca_country = 'United States' + AND ca_state IN ( 'TX', 'NE', 'MO' ) + AND ss_net_profit BETWEEN 0 AND 2000 ) + OR ( ss_addr_sk = ca_address_sk + AND ca_country = 'United States' + AND ca_state IN ( 'CO', 'TN', 'ND' ) + AND ss_net_profit BETWEEN 150 AND 3000 ) + OR ( ss_addr_sk = ca_address_sk + AND ca_country = 'United States' + AND ca_state IN ( 'OK', 'PA', 'CA' ) + AND ss_net_profit BETWEEN 50 AND 25000 ) ); diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query49.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query49.sql new file mode 100644 index 000000000..9c9eb073c --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query49.sql @@ -0,0 +1,133 @@ +-- start query 49 in stream 0 using template query49.tpl +SELECT 'web' AS channel, + web.item, + web.return_ratio, + web.return_rank, + web.currency_rank +FROM (SELECT item, + return_ratio, + currency_ratio, + Rank() + OVER ( + ORDER BY return_ratio) AS return_rank, + Rank() + OVER ( + ORDER BY currency_ratio) AS currency_rank + FROM (SELECT ws.ws_item_sk AS + item, + ( Cast(Sum(COALESCE(wr.wr_return_quantity, 0)) AS DECIMAL(15, + 4)) / + Cast( + Sum(COALESCE(ws.ws_quantity, 0)) AS DECIMAL(15, 4)) ) AS + return_ratio, + ( Cast(Sum(COALESCE(wr.wr_return_amt, 0)) AS DECIMAL(15, 4)) + / Cast( + Sum( + COALESCE(ws.ws_net_paid, 0)) AS DECIMAL(15, + 4)) ) AS + currency_ratio + FROM web_sales ws + LEFT OUTER JOIN web_returns wr + ON ( ws.ws_order_number = wr.wr_order_number + AND ws.ws_item_sk = wr.wr_item_sk ), + date_dim + WHERE wr.wr_return_amt > 10000 + AND ws.ws_net_profit > 1 + AND ws.ws_net_paid > 0 + AND ws.ws_quantity > 0 + AND ws_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 12 + GROUP BY ws.ws_item_sk) in_web) web +WHERE ( web.return_rank <= 10 + OR web.currency_rank <= 10 ) +UNION +SELECT 'catalog' AS channel, + catalog.item, + catalog.return_ratio, + catalog.return_rank, + catalog.currency_rank +FROM (SELECT item, + return_ratio, + currency_ratio, + Rank() + OVER ( + ORDER BY return_ratio) AS return_rank, + Rank() + OVER ( + ORDER BY currency_ratio) AS currency_rank + FROM (SELECT cs.cs_item_sk AS + item, + ( Cast(Sum(COALESCE(cr.cr_return_quantity, 0)) AS DECIMAL(15, + 4)) / + Cast( + Sum(COALESCE(cs.cs_quantity, 0)) AS DECIMAL(15, 4)) ) AS + return_ratio, + ( Cast(Sum(COALESCE(cr.cr_return_amount, 0)) AS DECIMAL(15, 4 + )) / + Cast(Sum( + COALESCE(cs.cs_net_paid, 0)) AS DECIMAL( + 15, 4)) ) AS + currency_ratio + FROM catalog_sales cs + LEFT OUTER JOIN catalog_returns cr + ON ( cs.cs_order_number = cr.cr_order_number + AND cs.cs_item_sk = cr.cr_item_sk ), + date_dim + WHERE cr.cr_return_amount > 10000 + AND cs.cs_net_profit > 1 + AND cs.cs_net_paid > 0 + AND cs.cs_quantity > 0 + AND cs_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 12 + GROUP BY cs.cs_item_sk) in_cat) catalog +WHERE ( catalog.return_rank <= 10 + OR catalog.currency_rank <= 10 ) +UNION +SELECT 'store' AS channel, + store.item, + store.return_ratio, + store.return_rank, + store.currency_rank +FROM (SELECT item, + return_ratio, + currency_ratio, + Rank() + OVER ( + ORDER BY return_ratio) AS return_rank, + Rank() + OVER ( + ORDER BY currency_ratio) AS currency_rank + FROM (SELECT sts.ss_item_sk AS + item, + ( Cast(Sum(COALESCE(sr.sr_return_quantity, 0)) AS DECIMAL(15, + 4)) / + Cast( + Sum(COALESCE(sts.ss_quantity, 0)) AS DECIMAL(15, 4)) ) AS + return_ratio, + ( Cast(Sum(COALESCE(sr.sr_return_amt, 0)) AS DECIMAL(15, 4)) + / Cast( + Sum( + COALESCE(sts.ss_net_paid, 0)) AS DECIMAL(15, 4)) ) AS + currency_ratio + FROM store_sales sts + LEFT OUTER JOIN store_returns sr + ON ( sts.ss_ticket_number = + sr.sr_ticket_number + AND sts.ss_item_sk = sr.sr_item_sk ), + date_dim + WHERE sr.sr_return_amt > 10000 + AND sts.ss_net_profit > 1 + AND sts.ss_net_paid > 0 + AND sts.ss_quantity > 0 + AND ss_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 12 + GROUP BY sts.ss_item_sk) in_store) store +WHERE ( store.return_rank <= 10 + OR store.currency_rank <= 10 ) +ORDER BY 1, + 4, + 5 +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query5.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query5.sql new file mode 100644 index 000000000..0c03b0ddc --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query5.sql @@ -0,0 +1,127 @@ +-- start query 5 in stream 0 using template query5.tpl +WITH ssr AS +( + SELECT s_store_id, + Sum(sales_price) AS sales, + Sum(profit) AS profit, + Sum(return_amt) AS returns1, + Sum(net_loss) AS profit_loss + FROM ( + SELECT ss_store_sk AS store_sk, + ss_sold_date_sk AS date_sk, + ss_ext_sales_price AS sales_price, + ss_net_profit AS profit, + Cast(0 AS DECIMAL(7,2)) AS return_amt, + Cast(0 AS DECIMAL(7,2)) AS net_loss + FROM store_sales + UNION ALL + SELECT sr_store_sk AS store_sk, + sr_returned_date_sk AS date_sk, + Cast(0 AS DECIMAL(7,2)) AS sales_price, + Cast(0 AS DECIMAL(7,2)) AS profit, + sr_return_amt AS return_amt, + sr_net_loss AS net_loss + FROM store_returns ) salesreturns, + date_dim, + store + WHERE date_sk = d_date_sk + AND d_date BETWEEN Cast('2002-08-22' AS DATE) AND ( + Cast('2002-08-22' AS DATE) + INTERVAL '14' day) + AND store_sk = s_store_sk + GROUP BY s_store_id) , csr AS +( + SELECT cp_catalog_page_id, + sum(sales_price) AS sales, + sum(profit) AS profit, + sum(return_amt) AS returns1, + sum(net_loss) AS profit_loss + FROM ( + SELECT cs_catalog_page_sk AS page_sk, + cs_sold_date_sk AS date_sk, + cs_ext_sales_price AS sales_price, + cs_net_profit AS profit, + cast(0 AS decimal(7,2)) AS return_amt, + cast(0 AS decimal(7,2)) AS net_loss + FROM catalog_sales + UNION ALL + SELECT cr_catalog_page_sk AS page_sk, + cr_returned_date_sk AS date_sk, + cast(0 AS decimal(7,2)) AS sales_price, + cast(0 AS decimal(7,2)) AS profit, + cr_return_amount AS return_amt, + cr_net_loss AS net_loss + FROM catalog_returns ) salesreturns, + date_dim, + catalog_page + WHERE date_sk = d_date_sk + AND d_date BETWEEN cast('2002-08-22' AS date) AND ( + cast('2002-08-22' AS date) + INTERVAL '14' day) + AND page_sk = cp_catalog_page_sk + GROUP BY cp_catalog_page_id) , wsr AS +( + SELECT web_site_id, + sum(sales_price) AS sales, + sum(profit) AS profit, + sum(return_amt) AS returns1, + sum(net_loss) AS profit_loss + FROM ( + SELECT ws_web_site_sk AS wsr_web_site_sk, + ws_sold_date_sk AS date_sk, + ws_ext_sales_price AS sales_price, + ws_net_profit AS profit, + cast(0 AS decimal(7,2)) AS return_amt, + cast(0 AS decimal(7,2)) AS net_loss + FROM web_sales + UNION ALL + SELECT ws_web_site_sk AS wsr_web_site_sk, + wr_returned_date_sk AS date_sk, + cast(0 AS decimal(7,2)) AS sales_price, + cast(0 AS decimal(7,2)) AS profit, + wr_return_amt AS return_amt, + wr_net_loss AS net_loss + FROM web_returns + LEFT OUTER JOIN web_sales + ON ( + wr_item_sk = ws_item_sk + AND wr_order_number = ws_order_number) ) salesreturns, + date_dim, + web_site + WHERE date_sk = d_date_sk + AND d_date BETWEEN cast('2002-08-22' AS date) AND ( + cast('2002-08-22' AS date) + INTERVAL '14' day) + AND wsr_web_site_sk = web_site_sk + GROUP BY web_site_id) +SELECT + channel , + id , + sum(sales) AS sales , + sum(returns1) AS returns1 , + sum(profit) AS profit +FROM ( + SELECT 'store channel' AS channel , + 'store' + || s_store_id AS id , + sales , + returns1 , + (profit - profit_loss) AS profit + FROM ssr + UNION ALL + SELECT 'catalog channel' AS channel , + 'catalog_page' + || cp_catalog_page_id AS id , + sales , + returns1 , + (profit - profit_loss) AS profit + FROM csr + UNION ALL + SELECT 'web channel' AS channel , + 'web_site' + || web_site_id AS id , + sales , + returns1 , + (profit - profit_loss) AS profit + FROM wsr ) x +GROUP BY rollup (channel, id) +ORDER BY channel , + id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query50.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query50.sql new file mode 100644 index 000000000..e7ddae136 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query50.sql @@ -0,0 +1,71 @@ +-- start query 50 in stream 0 using template query50.tpl +SELECT s_store_name, + s_company_id, + s_street_number, + s_street_name, + s_street_type, + s_suite_number, + s_city, + s_county, + s_state, + s_zip, + Sum(CASE + WHEN ( sr_returned_date_sk - ss_sold_date_sk <= 30 ) THEN 1 + ELSE 0 + END) AS `30 days`, + Sum(CASE + WHEN ( sr_returned_date_sk - ss_sold_date_sk > 30 ) + AND ( sr_returned_date_sk - ss_sold_date_sk <= 60 ) + THEN 1 + ELSE 0 + END) AS `31-60 days`, + Sum(CASE + WHEN ( sr_returned_date_sk - ss_sold_date_sk > 60 ) + AND ( sr_returned_date_sk - ss_sold_date_sk <= 90 ) + THEN 1 + ELSE 0 + END) AS `61-90 days`, + Sum(CASE + WHEN ( sr_returned_date_sk - ss_sold_date_sk > 90 ) + AND ( sr_returned_date_sk - ss_sold_date_sk <= 120 ) + THEN 1 + ELSE 0 + END) AS `91-120 days`, + Sum(CASE + WHEN ( sr_returned_date_sk - ss_sold_date_sk > 120 ) THEN 1 + ELSE 0 + END) AS `>120 days` +FROM store_sales, + store_returns, + store, + date_dim d1, + date_dim d2 +WHERE d2.d_year = 2002 + AND d2.d_moy = 9 + AND ss_ticket_number = sr_ticket_number + AND ss_item_sk = sr_item_sk + AND ss_sold_date_sk = d1.d_date_sk + AND sr_returned_date_sk = d2.d_date_sk + AND ss_customer_sk = sr_customer_sk + AND ss_store_sk = s_store_sk +GROUP BY s_store_name, + s_company_id, + s_street_number, + s_street_name, + s_street_type, + s_suite_number, + s_city, + s_county, + s_state, + s_zip +ORDER BY s_store_name, + s_company_id, + s_street_number, + s_street_name, + s_street_type, + s_suite_number, + s_city, + s_county, + s_state, + s_zip +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query51.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query51.sql new file mode 100644 index 000000000..2c73e7d60 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query51.sql @@ -0,0 +1,54 @@ +-- start query 51 in stream 0 using template query51.tpl +WITH web_v1 AS +( + SELECT ws_item_sk item_sk, + d_date, + sum(Sum(ws_sales_price)) OVER (partition BY ws_item_sk ORDER BY d_date rows BETWEEN UNBOUNDED PRECEDING AND CURRENT row) cume_sales + FROM web_sales , + date_dim + WHERE ws_sold_date_sk=d_date_sk + AND d_month_seq BETWEEN 1192 AND 1192+11 + AND ws_item_sk IS NOT NULL + GROUP BY ws_item_sk, + d_date), store_v1 AS +( + SELECT ss_item_sk item_sk, + d_date, + sum(sum(ss_sales_price)) OVER (partition BY ss_item_sk ORDER BY d_date rows BETWEEN UNBOUNDED PRECEDING AND CURRENT row) cume_sales + FROM store_sales , + date_dim + WHERE ss_sold_date_sk=d_date_sk + AND d_month_seq BETWEEN 1192 AND 1192+11 + AND ss_item_sk IS NOT NULL + GROUP BY ss_item_sk, + d_date) +SELECT + * +FROM ( + SELECT item_sk , + d_date , + web_sales , + store_sales , + max(web_sales) OVER (partition BY item_sk ORDER BY d_date rows BETWEEN UNBOUNDED PRECEDING AND CURRENT row) web_cumulative , + max(store_sales) OVER (partition BY item_sk ORDER BY d_date rows BETWEEN UNBOUNDED PRECEDING AND CURRENT row) store_cumulative + FROM ( + SELECT + CASE + WHEN web.item_sk IS NOT NULL THEN web.item_sk + ELSE store.item_sk + END item_sk , + CASE + WHEN web.d_date IS NOT NULL THEN web.d_date + ELSE store.d_date + END d_date , + web.cume_sales web_sales , + store.cume_sales store_sales + FROM web_v1 web + FULL OUTER JOIN store_v1 store + ON ( + web.item_sk = store.item_sk + AND web.d_date = store.d_date) )x )y +WHERE web_cumulative > store_cumulative +ORDER BY item_sk , + d_date +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query52.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query52.sql new file mode 100644 index 000000000..065f3f169 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query52.sql @@ -0,0 +1,20 @@ +-- start query 52 in stream 0 using template query52.tpl +SELECT dt.d_year, + item.i_brand_id brand_id, + item.i_brand brand, + Sum(ss_ext_sales_price) ext_price +FROM date_dim dt, + store_sales, + item +WHERE dt.d_date_sk = store_sales.ss_sold_date_sk + AND store_sales.ss_item_sk = item.i_item_sk + AND item.i_manager_id = 1 + AND dt.d_moy = 11 + AND dt.d_year = 1999 +GROUP BY dt.d_year, + item.i_brand, + item.i_brand_id +ORDER BY dt.d_year, + ext_price DESC, + brand_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query53.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query53.sql new file mode 100644 index 000000000..4c0452a32 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query53.sql @@ -0,0 +1,46 @@ +-- start query 53 in stream 0 using template query53.tpl +SELECT * +FROM (SELECT i_manufact_id, + Sum(ss_sales_price) sum_sales, + Avg(Sum(ss_sales_price)) + OVER ( + partition BY i_manufact_id) avg_quarterly_sales + FROM item, + store_sales, + date_dim, + store + WHERE ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND ss_store_sk = s_store_sk + AND d_month_seq IN ( 1199, 1199 + 1, 1199 + 2, 1199 + 3, + 1199 + 4, 1199 + 5, 1199 + 6, 1199 + 7, + 1199 + 8, 1199 + 9, 1199 + 10, 1199 + 11 ) + AND ( ( i_category IN ( 'Books', 'Children', 'Electronics' ) + AND i_class IN ( 'personal', 'portable', 'reference', + 'self-help' ) + AND i_brand IN ( 'scholaramalgamalg #14', + 'scholaramalgamalg #7' + , + 'exportiunivamalg #9', + 'scholaramalgamalg #9' ) + ) + OR ( i_category IN ( 'Women', 'Music', 'Men' ) + AND i_class IN ( 'accessories', 'classical', + 'fragrances', + 'pants' ) + AND i_brand IN ( 'amalgimporto #1', + 'edu packscholar #1', + 'exportiimporto #1', + 'importoamalg #1' ) ) ) + GROUP BY i_manufact_id, + d_qoy) tmp1 +WHERE CASE + WHEN avg_quarterly_sales > 0 THEN Abs (sum_sales - avg_quarterly_sales) + / + avg_quarterly_sales + ELSE NULL + END > 0.1 +ORDER BY avg_quarterly_sales, + sum_sales, + i_manufact_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query54.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query54.sql new file mode 100644 index 000000000..b776c9590 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query54.sql @@ -0,0 +1,57 @@ +-- start query 54 in stream 0 using template query54.tpl +WITH my_customers + AS (SELECT DISTINCT c_customer_sk, + c_current_addr_sk + FROM (SELECT cs_sold_date_sk sold_date_sk, + cs_bill_customer_sk customer_sk, + cs_item_sk item_sk + FROM catalog_sales + UNION ALL + SELECT ws_sold_date_sk sold_date_sk, + ws_bill_customer_sk customer_sk, + ws_item_sk item_sk + FROM web_sales) cs_or_ws_sales, + item, + date_dim, + customer + WHERE sold_date_sk = d_date_sk + AND item_sk = i_item_sk + AND i_category = 'Sports' + AND i_class = 'fitness' + AND c_customer_sk = cs_or_ws_sales.customer_sk + AND d_moy = 5 + AND d_year = 2000), + my_revenue + AS (SELECT c_customer_sk, + Sum(ss_ext_sales_price) AS revenue + FROM my_customers, + store_sales, + customer_address, + store, + date_dim + WHERE c_current_addr_sk = ca_address_sk + AND ca_county = s_county + AND ca_state = s_state + AND ss_sold_date_sk = d_date_sk + AND c_customer_sk = ss_customer_sk + AND d_month_seq BETWEEN (SELECT DISTINCT d_month_seq + 1 + FROM date_dim + WHERE d_year = 2000 + AND d_moy = 5) AND + (SELECT DISTINCT + d_month_seq + 3 + FROM date_dim + WHERE d_year = 2000 + AND d_moy = 5) + GROUP BY c_customer_sk), + segments + AS (SELECT Cast(( revenue / 50 ) AS INT) AS segment + FROM my_revenue) +SELECT segment, + Count(*) AS num_customers, + segment * 50 AS segment_base +FROM segments +GROUP BY segment +ORDER BY segment, + num_customers +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query55.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query55.sql new file mode 100644 index 000000000..37ed2563f --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query55.sql @@ -0,0 +1,17 @@ +-- start query 55 in stream 0 using template query55.tpl +SELECT i_brand_id brand_id, + i_brand brand, + Sum(ss_ext_sales_price) ext_price +FROM date_dim, + store_sales, + item +WHERE d_date_sk = ss_sold_date_sk + AND ss_item_sk = i_item_sk + AND i_manager_id = 33 + AND d_moy = 12 + AND d_year = 1998 +GROUP BY i_brand, + i_brand_id +ORDER BY ext_price DESC, + i_brand_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query56.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query56.sql new file mode 100644 index 000000000..7d2ad0629 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query56.sql @@ -0,0 +1,68 @@ +-- start query 56 in stream 0 using template query56.tpl +WITH ss + AS (SELECT i_item_id, + Sum(ss_ext_sales_price) total_sales + FROM store_sales, + date_dim, + customer_address, + item + WHERE i_item_id IN (SELECT i_item_id + FROM item + WHERE i_color IN ( 'firebrick', 'rosy', 'white' ) + ) + AND ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 1998 + AND d_moy = 3 + AND ss_addr_sk = ca_address_sk + AND ca_gmt_offset = -6 + GROUP BY i_item_id), + cs + AS (SELECT i_item_id, + Sum(cs_ext_sales_price) total_sales + FROM catalog_sales, + date_dim, + customer_address, + item + WHERE i_item_id IN (SELECT i_item_id + FROM item + WHERE i_color IN ( 'firebrick', 'rosy', 'white' ) + ) + AND cs_item_sk = i_item_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 1998 + AND d_moy = 3 + AND cs_bill_addr_sk = ca_address_sk + AND ca_gmt_offset = -6 + GROUP BY i_item_id), + ws + AS (SELECT i_item_id, + Sum(ws_ext_sales_price) total_sales + FROM web_sales, + date_dim, + customer_address, + item + WHERE i_item_id IN (SELECT i_item_id + FROM item + WHERE i_color IN ( 'firebrick', 'rosy', 'white' ) + ) + AND ws_item_sk = i_item_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 1998 + AND d_moy = 3 + AND ws_bill_addr_sk = ca_address_sk + AND ca_gmt_offset = -6 + GROUP BY i_item_id) +SELECT i_item_id, + Sum(total_sales) total_sales +FROM (SELECT * + FROM ss + UNION ALL + SELECT * + FROM cs + UNION ALL + SELECT * + FROM ws) tmp1 +GROUP BY i_item_id +ORDER BY total_sales +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query57.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query57.sql new file mode 100644 index 000000000..5cb73bce4 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query57.sql @@ -0,0 +1,66 @@ +-- start query 57 in stream 0 using template query57.tpl +WITH v1 + AS (SELECT i_category, + i_brand, + cc_name, + d_year, + d_moy, + Sum(cs_sales_price) sum_sales + , + Avg(Sum(cs_sales_price)) + OVER ( + partition BY i_category, i_brand, cc_name, d_year) + avg_monthly_sales + , + Rank() + OVER ( + partition BY i_category, i_brand, cc_name + ORDER BY d_year, d_moy) rn + FROM item, + catalog_sales, + date_dim, + call_center + WHERE cs_item_sk = i_item_sk + AND cs_sold_date_sk = d_date_sk + AND cc_call_center_sk = cs_call_center_sk + AND ( d_year = 2000 + OR ( d_year = 2000 - 1 + AND d_moy = 12 ) + OR ( d_year = 2000 + 1 + AND d_moy = 1 ) ) + GROUP BY i_category, + i_brand, + cc_name, + d_year, + d_moy), + v2 + AS (SELECT v1.i_brand, + v1.d_year, + v1.avg_monthly_sales, + v1.sum_sales, + v1_lag.sum_sales psum, + v1_lead.sum_sales nsum + FROM v1, + v1 v1_lag, + v1 v1_lead + WHERE v1.i_category = v1_lag.i_category + AND v1.i_category = v1_lead.i_category + AND v1.i_brand = v1_lag.i_brand + AND v1.i_brand = v1_lead.i_brand + AND v1. cc_name = v1_lag. cc_name + AND v1. cc_name = v1_lead. cc_name + AND v1.rn = v1_lag.rn + 1 + AND v1.rn = v1_lead.rn - 1) +SELECT * +FROM v2 +WHERE d_year = 2000 + AND avg_monthly_sales > 0 + AND CASE + WHEN avg_monthly_sales > 0 THEN Abs(sum_sales - avg_monthly_sales) + / + avg_monthly_sales + ELSE NULL + END > 0.1 +ORDER BY sum_sales - avg_monthly_sales, + 3 +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query58.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query58.sql new file mode 100644 index 000000000..8611390de --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query58.sql @@ -0,0 +1,72 @@ +-- start query 58 in stream 0 using template query58.tpl +WITH ss_items + AS (SELECT i_item_id item_id, + Sum(ss_ext_sales_price) ss_item_rev + FROM store_sales, + item, + date_dim + WHERE ss_item_sk = i_item_sk + AND d_date IN (SELECT d_date + FROM date_dim + WHERE d_week_seq = (SELECT d_week_seq + FROM date_dim + WHERE d_date = '2002-02-25' + )) + AND ss_sold_date_sk = d_date_sk + GROUP BY i_item_id), + cs_items + AS (SELECT i_item_id item_id, + Sum(cs_ext_sales_price) cs_item_rev + FROM catalog_sales, + item, + date_dim + WHERE cs_item_sk = i_item_sk + AND d_date IN (SELECT d_date + FROM date_dim + WHERE d_week_seq = (SELECT d_week_seq + FROM date_dim + WHERE d_date = '2002-02-25' + )) + AND cs_sold_date_sk = d_date_sk + GROUP BY i_item_id), + ws_items + AS (SELECT i_item_id item_id, + Sum(ws_ext_sales_price) ws_item_rev + FROM web_sales, + item, + date_dim + WHERE ws_item_sk = i_item_sk + AND d_date IN (SELECT d_date + FROM date_dim + WHERE d_week_seq = (SELECT d_week_seq + FROM date_dim + WHERE d_date = '2002-02-25' + )) + AND ws_sold_date_sk = d_date_sk + GROUP BY i_item_id) +SELECT ss_items.item_id, + ss_item_rev, + ss_item_rev / ( ss_item_rev + cs_item_rev + ws_item_rev ) / 3 * + 100 ss_dev, + cs_item_rev, + cs_item_rev / ( ss_item_rev + cs_item_rev + ws_item_rev ) / 3 * + 100 cs_dev, + ws_item_rev, + ws_item_rev / ( ss_item_rev + cs_item_rev + ws_item_rev ) / 3 * + 100 ws_dev, + ( ss_item_rev + cs_item_rev + ws_item_rev ) / 3 + average +FROM ss_items, + cs_items, + ws_items +WHERE ss_items.item_id = cs_items.item_id + AND ss_items.item_id = ws_items.item_id + AND ss_item_rev BETWEEN 0.9 * cs_item_rev AND 1.1 * cs_item_rev + AND ss_item_rev BETWEEN 0.9 * ws_item_rev AND 1.1 * ws_item_rev + AND cs_item_rev BETWEEN 0.9 * ss_item_rev AND 1.1 * ss_item_rev + AND cs_item_rev BETWEEN 0.9 * ws_item_rev AND 1.1 * ws_item_rev + AND ws_item_rev BETWEEN 0.9 * ss_item_rev AND 1.1 * ss_item_rev + AND ws_item_rev BETWEEN 0.9 * cs_item_rev AND 1.1 * cs_item_rev +ORDER BY item_id, + ss_item_rev +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query59.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query59.sql new file mode 100644 index 000000000..3caa54048 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query59.sql @@ -0,0 +1,85 @@ +-- start query 59 in stream 0 using template query59.tpl +WITH wss + AS (SELECT d_week_seq, + ss_store_sk, + Sum(CASE + WHEN ( d_day_name = 'Sunday' ) THEN ss_sales_price + ELSE NULL + END) sun_sales, + Sum(CASE + WHEN ( d_day_name = 'Monday' ) THEN ss_sales_price + ELSE NULL + END) mon_sales, + Sum(CASE + WHEN ( d_day_name = 'Tuesday' ) THEN ss_sales_price + ELSE NULL + END) tue_sales, + Sum(CASE + WHEN ( d_day_name = 'Wednesday' ) THEN ss_sales_price + ELSE NULL + END) wed_sales, + Sum(CASE + WHEN ( d_day_name = 'Thursday' ) THEN ss_sales_price + ELSE NULL + END) thu_sales, + Sum(CASE + WHEN ( d_day_name = 'Friday' ) THEN ss_sales_price + ELSE NULL + END) fri_sales, + Sum(CASE + WHEN ( d_day_name = 'Saturday' ) THEN ss_sales_price + ELSE NULL + END) sat_sales + FROM store_sales, + date_dim + WHERE d_date_sk = ss_sold_date_sk + GROUP BY d_week_seq, + ss_store_sk) +SELECT s_store_name1, + s_store_id1, + d_week_seq1, + sun_sales1 / sun_sales2, + mon_sales1 / mon_sales2, + tue_sales1 / tue_sales2, + wed_sales1 / wed_sales2, + thu_sales1 / thu_sales2, + fri_sales1 / fri_sales2, + sat_sales1 / sat_sales2 +FROM (SELECT s_store_name s_store_name1, + wss.d_week_seq d_week_seq1, + s_store_id s_store_id1, + sun_sales sun_sales1, + mon_sales mon_sales1, + tue_sales tue_sales1, + wed_sales wed_sales1, + thu_sales thu_sales1, + fri_sales fri_sales1, + sat_sales sat_sales1 + FROM wss, + store, + date_dim d + WHERE d.d_week_seq = wss.d_week_seq + AND ss_store_sk = s_store_sk + AND d_month_seq BETWEEN 1196 AND 1196 + 11) y, + (SELECT s_store_name s_store_name2, + wss.d_week_seq d_week_seq2, + s_store_id s_store_id2, + sun_sales sun_sales2, + mon_sales mon_sales2, + tue_sales tue_sales2, + wed_sales wed_sales2, + thu_sales thu_sales2, + fri_sales fri_sales2, + sat_sales sat_sales2 + FROM wss, + store, + date_dim d + WHERE d.d_week_seq = wss.d_week_seq + AND ss_store_sk = s_store_sk + AND d_month_seq BETWEEN 1196 + 12 AND 1196 + 23) x +WHERE s_store_id1 = s_store_id2 + AND d_week_seq1 = d_week_seq2 - 52 +ORDER BY s_store_name1, + s_store_id1, + d_week_seq1 +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query6.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query6.sql new file mode 100644 index 000000000..1739ea575 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query6.sql @@ -0,0 +1,23 @@ +-- start query 6 in stream 0 using template query6.tpl +SELECT a.ca_state state, + Count(*) cnt +FROM customer_address a, + customer c, + store_sales s, + date_dim d, + item i +WHERE a.ca_address_sk = c.c_current_addr_sk + AND c.c_customer_sk = s.ss_customer_sk + AND s.ss_sold_date_sk = d.d_date_sk + AND s.ss_item_sk = i.i_item_sk + AND d.d_month_seq = (SELECT DISTINCT ( d_month_seq ) + FROM date_dim + WHERE d_year = 1998 + AND d_moy = 7) + AND i.i_current_price > 1.2 * (SELECT Avg(j.i_current_price) + FROM item j + WHERE j.i_category = i.i_category) +GROUP BY a.ca_state +HAVING Count(*) >= 10 +ORDER BY cnt +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query60.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query60.sql new file mode 100644 index 000000000..541108dea --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query60.sql @@ -0,0 +1,66 @@ +-- start query 60 in stream 0 using template query60.tpl +WITH ss + AS (SELECT i_item_id, + Sum(ss_ext_sales_price) total_sales + FROM store_sales, + date_dim, + customer_address, + item + WHERE i_item_id IN (SELECT i_item_id + FROM item + WHERE i_category IN ( 'Jewelry' )) + AND ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 8 + AND ss_addr_sk = ca_address_sk + AND ca_gmt_offset = -6 + GROUP BY i_item_id), + cs + AS (SELECT i_item_id, + Sum(cs_ext_sales_price) total_sales + FROM catalog_sales, + date_dim, + customer_address, + item + WHERE i_item_id IN (SELECT i_item_id + FROM item + WHERE i_category IN ( 'Jewelry' )) + AND cs_item_sk = i_item_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 8 + AND cs_bill_addr_sk = ca_address_sk + AND ca_gmt_offset = -6 + GROUP BY i_item_id), + ws + AS (SELECT i_item_id, + Sum(ws_ext_sales_price) total_sales + FROM web_sales, + date_dim, + customer_address, + item + WHERE i_item_id IN (SELECT i_item_id + FROM item + WHERE i_category IN ( 'Jewelry' )) + AND ws_item_sk = i_item_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 1999 + AND d_moy = 8 + AND ws_bill_addr_sk = ca_address_sk + AND ca_gmt_offset = -6 + GROUP BY i_item_id) +SELECT i_item_id, + Sum(total_sales) total_sales +FROM (SELECT * + FROM ss + UNION ALL + SELECT * + FROM cs + UNION ALL + SELECT * + FROM ws) tmp1 +GROUP BY i_item_id +ORDER BY i_item_id, + total_sales +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query61.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query61.sql new file mode 100644 index 000000000..d6d50753c --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query61.sql @@ -0,0 +1,47 @@ +-- start query 61 in stream 0 using template query61.tpl +SELECT promotions, + total, + Cast(promotions AS DECIMAL(15, 4)) / + Cast(total AS DECIMAL(15, 4)) * 100 +FROM (SELECT Sum(ss_ext_sales_price) promotions + FROM store_sales, + store, + promotion, + date_dim, + customer, + customer_address, + item + WHERE ss_sold_date_sk = d_date_sk + AND ss_store_sk = s_store_sk + AND ss_promo_sk = p_promo_sk + AND ss_customer_sk = c_customer_sk + AND ca_address_sk = c_current_addr_sk + AND ss_item_sk = i_item_sk + AND ca_gmt_offset = -7 + AND i_category = 'Books' + AND ( p_channel_dmail = 'Y' + OR p_channel_email = 'Y' + OR p_channel_tv = 'Y' ) + AND s_gmt_offset = -7 + AND d_year = 2001 + AND d_moy = 12) promotional_sales, + (SELECT Sum(ss_ext_sales_price) total + FROM store_sales, + store, + date_dim, + customer, + customer_address, + item + WHERE ss_sold_date_sk = d_date_sk + AND ss_store_sk = s_store_sk + AND ss_customer_sk = c_customer_sk + AND ca_address_sk = c_current_addr_sk + AND ss_item_sk = i_item_sk + AND ca_gmt_offset = -7 + AND i_category = 'Books' + AND s_gmt_offset = -7 + AND d_year = 2001 + AND d_moy = 12) all_sales +ORDER BY promotions, + total +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query62.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query62.sql new file mode 100644 index 000000000..f83715e73 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query62.sql @@ -0,0 +1,45 @@ +-- start query 62 in stream 0 using template query62.tpl +SELECT Substr(w_warehouse_name, 1, 20), + sm_type, + web_name, + Sum(CASE + WHEN ( ws_ship_date_sk - ws_sold_date_sk <= 30 ) THEN 1 + ELSE 0 + END) AS `30 days`, + Sum(CASE + WHEN ( ws_ship_date_sk - ws_sold_date_sk > 30 ) + AND ( ws_ship_date_sk - ws_sold_date_sk <= 60 ) THEN 1 + ELSE 0 + END) AS `31-60 days`, + Sum(CASE + WHEN ( ws_ship_date_sk - ws_sold_date_sk > 60 ) + AND ( ws_ship_date_sk - ws_sold_date_sk <= 90 ) THEN 1 + ELSE 0 + END) AS `61-90 days`, + Sum(CASE + WHEN ( ws_ship_date_sk - ws_sold_date_sk > 90 ) + AND ( ws_ship_date_sk - ws_sold_date_sk <= 120 ) THEN + 1 + ELSE 0 + END) AS `91-120 days`, + Sum(CASE + WHEN ( ws_ship_date_sk - ws_sold_date_sk > 120 ) THEN 1 + ELSE 0 + END) AS `>120 days` +FROM web_sales, + warehouse, + ship_mode, + web_site, + date_dim +WHERE d_month_seq BETWEEN 1222 AND 1222 + 11 + AND ws_ship_date_sk = d_date_sk + AND ws_warehouse_sk = w_warehouse_sk + AND ws_ship_mode_sk = sm_ship_mode_sk + AND ws_web_site_sk = web_site_sk +GROUP BY Substr(w_warehouse_name, 1, 20), + sm_type, + web_name +ORDER BY Substr(w_warehouse_name, 1, 20), + sm_type, + web_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query63.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query63.sql new file mode 100644 index 000000000..8706bb492 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query63.sql @@ -0,0 +1,45 @@ +-- start query 63 in stream 0 using template query63.tpl +SELECT * +FROM (SELECT i_manager_id, + Sum(ss_sales_price) sum_sales, + Avg(Sum(ss_sales_price)) + OVER ( + partition BY i_manager_id) avg_monthly_sales + FROM item, + store_sales, + date_dim, + store + WHERE ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND ss_store_sk = s_store_sk + AND d_month_seq IN ( 1200, 1200 + 1, 1200 + 2, 1200 + 3, + 1200 + 4, 1200 + 5, 1200 + 6, 1200 + 7, + 1200 + 8, 1200 + 9, 1200 + 10, 1200 + 11 ) + AND ( ( i_category IN ( 'Books', 'Children', 'Electronics' ) + AND i_class IN ( 'personal', 'portable', 'reference', + 'self-help' ) + AND i_brand IN ( 'scholaramalgamalg #14', + 'scholaramalgamalg #7' + , + 'exportiunivamalg #9', + 'scholaramalgamalg #9' ) + ) + OR ( i_category IN ( 'Women', 'Music', 'Men' ) + AND i_class IN ( 'accessories', 'classical', + 'fragrances', + 'pants' ) + AND i_brand IN ( 'amalgimporto #1', + 'edu packscholar #1', + 'exportiimporto #1', + 'importoamalg #1' ) ) ) + GROUP BY i_manager_id, + d_moy) tmp1 +WHERE CASE + WHEN avg_monthly_sales > 0 THEN Abs (sum_sales - avg_monthly_sales) / + avg_monthly_sales + ELSE NULL + END > 0.1 +ORDER BY i_manager_id, + avg_monthly_sales, + sum_sales +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query64.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query64.sql new file mode 100644 index 000000000..0c5d1d3d8 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query64.sql @@ -0,0 +1,122 @@ +-- start query 64 in stream 0 using template query64.tpl +WITH cs_ui + AS (SELECT cs_item_sk, + Sum(cs_ext_list_price) AS sale, + Sum(cr_refunded_cash + cr_reversed_charge + + cr_store_credit) AS refund + FROM catalog_sales, + catalog_returns + WHERE cs_item_sk = cr_item_sk + AND cs_order_number = cr_order_number + GROUP BY cs_item_sk + HAVING Sum(cs_ext_list_price) > 2 * Sum( + cr_refunded_cash + cr_reversed_charge + + cr_store_credit)), + cross_sales + AS (SELECT i_product_name product_name, + i_item_sk item_sk, + s_store_name store_name, + s_zip store_zip, + ad1.ca_street_number b_street_number, + ad1.ca_street_name b_streen_name, + ad1.ca_city b_city, + ad1.ca_zip b_zip, + ad2.ca_street_number c_street_number, + ad2.ca_street_name c_street_name, + ad2.ca_city c_city, + ad2.ca_zip c_zip, + d1.d_year AS syear, + d2.d_year AS fsyear, + d3.d_year s2year, + Count(*) cnt, + Sum(ss_wholesale_cost) s1, + Sum(ss_list_price) s2, + Sum(ss_coupon_amt) s3 + FROM store_sales, + store_returns, + cs_ui, + date_dim d1, + date_dim d2, + date_dim d3, + store, + customer, + customer_demographics cd1, + customer_demographics cd2, + promotion, + household_demographics hd1, + household_demographics hd2, + customer_address ad1, + customer_address ad2, + income_band ib1, + income_band ib2, + item + WHERE ss_store_sk = s_store_sk + AND ss_sold_date_sk = d1.d_date_sk + AND ss_customer_sk = c_customer_sk + AND ss_cdemo_sk = cd1.cd_demo_sk + AND ss_hdemo_sk = hd1.hd_demo_sk + AND ss_addr_sk = ad1.ca_address_sk + AND ss_item_sk = i_item_sk + AND ss_item_sk = sr_item_sk + AND ss_ticket_number = sr_ticket_number + AND ss_item_sk = cs_ui.cs_item_sk + AND c_current_cdemo_sk = cd2.cd_demo_sk + AND c_current_hdemo_sk = hd2.hd_demo_sk + AND c_current_addr_sk = ad2.ca_address_sk + AND c_first_sales_date_sk = d2.d_date_sk + AND c_first_shipto_date_sk = d3.d_date_sk + AND ss_promo_sk = p_promo_sk + AND hd1.hd_income_band_sk = ib1.ib_income_band_sk + AND hd2.hd_income_band_sk = ib2.ib_income_band_sk + AND cd1.cd_marital_status <> cd2.cd_marital_status + AND i_color IN ( 'cyan', 'peach', 'blush', 'frosted', + 'powder', 'orange' ) + AND i_current_price BETWEEN 58 AND 58 + 10 + AND i_current_price BETWEEN 58 + 1 AND 58 + 15 + GROUP BY i_product_name, + i_item_sk, + s_store_name, + s_zip, + ad1.ca_street_number, + ad1.ca_street_name, + ad1.ca_city, + ad1.ca_zip, + ad2.ca_street_number, + ad2.ca_street_name, + ad2.ca_city, + ad2.ca_zip, + d1.d_year, + d2.d_year, + d3.d_year) +SELECT cs1.product_name, + cs1.store_name, + cs1.store_zip, + cs1.b_street_number, + cs1.b_streen_name, + cs1.b_city, + cs1.b_zip, + cs1.c_street_number, + cs1.c_street_name, + cs1.c_city, + cs1.c_zip, + cs1.syear, + cs1.cnt, + cs1.s1, + cs1.s2, + cs1.s3, + cs2.s1, + cs2.s2, + cs2.s3, + cs2.syear, + cs2.cnt +FROM cross_sales cs1, + cross_sales cs2 +WHERE cs1.item_sk = cs2.item_sk + AND cs1.syear = 2001 + AND cs2.syear = 2001 + 1 + AND cs2.cnt <= cs1.cnt + AND cs1.store_name = cs2.store_name + AND cs1.store_zip = cs2.store_zip +ORDER BY cs1.product_name, + cs1.store_name, + cs2.cnt; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query65.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query65.sql new file mode 100644 index 000000000..399f128c3 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query65.sql @@ -0,0 +1,37 @@ +-- start query 65 in stream 0 using template query65.tpl +SELECT s_store_name, + i_item_desc, + sc.revenue, + i_current_price, + i_wholesale_cost, + i_brand +FROM store, + item, + (SELECT ss_store_sk, + Avg(revenue) AS ave + FROM (SELECT ss_store_sk, + ss_item_sk, + Sum(ss_sales_price) AS revenue + FROM store_sales, + date_dim + WHERE ss_sold_date_sk = d_date_sk + AND d_month_seq BETWEEN 1199 AND 1199 + 11 + GROUP BY ss_store_sk, + ss_item_sk) sa + GROUP BY ss_store_sk) sb, + (SELECT ss_store_sk, + ss_item_sk, + Sum(ss_sales_price) AS revenue + FROM store_sales, + date_dim + WHERE ss_sold_date_sk = d_date_sk + AND d_month_seq BETWEEN 1199 AND 1199 + 11 + GROUP BY ss_store_sk, + ss_item_sk) sc +WHERE sb.ss_store_sk = sc.ss_store_sk + AND sc.revenue <= 0.1 * sb.ave + AND s_store_sk = sc.ss_store_sk + AND i_item_sk = sc.ss_item_sk +ORDER BY s_store_name, + i_item_desc +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query66.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query66.sql new file mode 100644 index 000000000..79446555f --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query66.sql @@ -0,0 +1,306 @@ +-- start query 66 in stream 0 using template query66.tpl +SELECT w_warehouse_name, + w_warehouse_sq_ft, + w_city, + w_county, + w_state, + w_country, + ship_carriers, + year1, + Sum(jan_sales) AS jan_sales, + Sum(feb_sales) AS feb_sales, + Sum(mar_sales) AS mar_sales, + Sum(apr_sales) AS apr_sales, + Sum(may_sales) AS may_sales, + Sum(jun_sales) AS jun_sales, + Sum(jul_sales) AS jul_sales, + Sum(aug_sales) AS aug_sales, + Sum(sep_sales) AS sep_sales, + Sum(oct_sales) AS oct_sales, + Sum(nov_sales) AS nov_sales, + Sum(dec_sales) AS dec_sales, + Sum(jan_sales / w_warehouse_sq_ft) AS jan_sales_per_sq_foot, + Sum(feb_sales / w_warehouse_sq_ft) AS feb_sales_per_sq_foot, + Sum(mar_sales / w_warehouse_sq_ft) AS mar_sales_per_sq_foot, + Sum(apr_sales / w_warehouse_sq_ft) AS apr_sales_per_sq_foot, + Sum(may_sales / w_warehouse_sq_ft) AS may_sales_per_sq_foot, + Sum(jun_sales / w_warehouse_sq_ft) AS jun_sales_per_sq_foot, + Sum(jul_sales / w_warehouse_sq_ft) AS jul_sales_per_sq_foot, + Sum(aug_sales / w_warehouse_sq_ft) AS aug_sales_per_sq_foot, + Sum(sep_sales / w_warehouse_sq_ft) AS sep_sales_per_sq_foot, + Sum(oct_sales / w_warehouse_sq_ft) AS oct_sales_per_sq_foot, + Sum(nov_sales / w_warehouse_sq_ft) AS nov_sales_per_sq_foot, + Sum(dec_sales / w_warehouse_sq_ft) AS dec_sales_per_sq_foot, + Sum(jan_net) AS jan_net, + Sum(feb_net) AS feb_net, + Sum(mar_net) AS mar_net, + Sum(apr_net) AS apr_net, + Sum(may_net) AS may_net, + Sum(jun_net) AS jun_net, + Sum(jul_net) AS jul_net, + Sum(aug_net) AS aug_net, + Sum(sep_net) AS sep_net, + Sum(oct_net) AS oct_net, + Sum(nov_net) AS nov_net, + Sum(dec_net) AS dec_net +FROM (SELECT w_warehouse_name, + w_warehouse_sq_ft, + w_city, + w_county, + w_state, + w_country, + 'ZOUROS' + || ',' + || 'ZHOU' AS ship_carriers, + d_year AS year1, + Sum(CASE + WHEN d_moy = 1 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS jan_sales, + Sum(CASE + WHEN d_moy = 2 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS feb_sales, + Sum(CASE + WHEN d_moy = 3 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS mar_sales, + Sum(CASE + WHEN d_moy = 4 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS apr_sales, + Sum(CASE + WHEN d_moy = 5 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS may_sales, + Sum(CASE + WHEN d_moy = 6 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS jun_sales, + Sum(CASE + WHEN d_moy = 7 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS jul_sales, + Sum(CASE + WHEN d_moy = 8 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS aug_sales, + Sum(CASE + WHEN d_moy = 9 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS sep_sales, + Sum(CASE + WHEN d_moy = 10 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS oct_sales, + Sum(CASE + WHEN d_moy = 11 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS nov_sales, + Sum(CASE + WHEN d_moy = 12 THEN ws_ext_sales_price * ws_quantity + ELSE 0 + END) AS dec_sales, + Sum(CASE + WHEN d_moy = 1 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS jan_net, + Sum(CASE + WHEN d_moy = 2 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS feb_net, + Sum(CASE + WHEN d_moy = 3 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS mar_net, + Sum(CASE + WHEN d_moy = 4 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS apr_net, + Sum(CASE + WHEN d_moy = 5 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS may_net, + Sum(CASE + WHEN d_moy = 6 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS jun_net, + Sum(CASE + WHEN d_moy = 7 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS jul_net, + Sum(CASE + WHEN d_moy = 8 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS aug_net, + Sum(CASE + WHEN d_moy = 9 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS sep_net, + Sum(CASE + WHEN d_moy = 10 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS oct_net, + Sum(CASE + WHEN d_moy = 11 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS nov_net, + Sum(CASE + WHEN d_moy = 12 THEN ws_net_paid_inc_ship * ws_quantity + ELSE 0 + END) AS dec_net + FROM web_sales, + warehouse, + date_dim, + time_dim, + ship_mode + WHERE ws_warehouse_sk = w_warehouse_sk + AND ws_sold_date_sk = d_date_sk + AND ws_sold_time_sk = t_time_sk + AND ws_ship_mode_sk = sm_ship_mode_sk + AND d_year = 1998 + AND t_time BETWEEN 7249 AND 7249 + 28800 + AND sm_carrier IN ( 'ZOUROS', 'ZHOU' ) + GROUP BY w_warehouse_name, + w_warehouse_sq_ft, + w_city, + w_county, + w_state, + w_country, + d_year + UNION ALL + SELECT w_warehouse_name, + w_warehouse_sq_ft, + w_city, + w_county, + w_state, + w_country, + 'ZOUROS' + || ',' + || 'ZHOU' AS ship_carriers, + d_year AS year1, + Sum(CASE + WHEN d_moy = 1 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS jan_sales, + Sum(CASE + WHEN d_moy = 2 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS feb_sales, + Sum(CASE + WHEN d_moy = 3 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS mar_sales, + Sum(CASE + WHEN d_moy = 4 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS apr_sales, + Sum(CASE + WHEN d_moy = 5 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS may_sales, + Sum(CASE + WHEN d_moy = 6 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS jun_sales, + Sum(CASE + WHEN d_moy = 7 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS jul_sales, + Sum(CASE + WHEN d_moy = 8 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS aug_sales, + Sum(CASE + WHEN d_moy = 9 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS sep_sales, + Sum(CASE + WHEN d_moy = 10 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS oct_sales, + Sum(CASE + WHEN d_moy = 11 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS nov_sales, + Sum(CASE + WHEN d_moy = 12 THEN cs_ext_sales_price * cs_quantity + ELSE 0 + END) AS dec_sales, + Sum(CASE + WHEN d_moy = 1 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS jan_net, + Sum(CASE + WHEN d_moy = 2 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS feb_net, + Sum(CASE + WHEN d_moy = 3 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS mar_net, + Sum(CASE + WHEN d_moy = 4 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS apr_net, + Sum(CASE + WHEN d_moy = 5 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS may_net, + Sum(CASE + WHEN d_moy = 6 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS jun_net, + Sum(CASE + WHEN d_moy = 7 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS jul_net, + Sum(CASE + WHEN d_moy = 8 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS aug_net, + Sum(CASE + WHEN d_moy = 9 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS sep_net, + Sum(CASE + WHEN d_moy = 10 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS oct_net, + Sum(CASE + WHEN d_moy = 11 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS nov_net, + Sum(CASE + WHEN d_moy = 12 THEN cs_net_paid * cs_quantity + ELSE 0 + END) AS dec_net + FROM catalog_sales, + warehouse, + date_dim, + time_dim, + ship_mode + WHERE cs_warehouse_sk = w_warehouse_sk + AND cs_sold_date_sk = d_date_sk + AND cs_sold_time_sk = t_time_sk + AND cs_ship_mode_sk = sm_ship_mode_sk + AND d_year = 1998 + AND t_time BETWEEN 7249 AND 7249 + 28800 + AND sm_carrier IN ( 'ZOUROS', 'ZHOU' ) + GROUP BY w_warehouse_name, + w_warehouse_sq_ft, + w_city, + w_county, + w_state, + w_country, + d_year) x +GROUP BY w_warehouse_name, + w_warehouse_sq_ft, + w_city, + w_county, + w_state, + w_country, + ship_carriers, + year1 +ORDER BY w_warehouse_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query67.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query67.sql new file mode 100644 index 000000000..18c39edb5 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query67.sql @@ -0,0 +1,43 @@ +-- start query 67 in stream 0 using template query67.tpl +select * +from (select i_category + ,i_class + ,i_brand + ,i_product_name + ,d_year + ,d_qoy + ,d_moy + ,s_store_id + ,sumsales + ,rank() over (partition by i_category order by sumsales desc) rk + from (select i_category + ,i_class + ,i_brand + ,i_product_name + ,d_year + ,d_qoy + ,d_moy + ,s_store_id + ,sum(coalesce(ss_sales_price*ss_quantity,0)) sumsales + from store_sales + ,date_dim + ,store + ,item + where ss_sold_date_sk=d_date_sk + and ss_item_sk=i_item_sk + and ss_store_sk = s_store_sk + and d_month_seq between 1181 and 1181+11 + group by rollup(i_category, i_class, i_brand, i_product_name, d_year, d_qoy, d_moy,s_store_id))dw1) dw2 +where rk <= 100 +order by i_category + ,i_class + ,i_brand + ,i_product_name + ,d_year + ,d_qoy + ,d_moy + ,s_store_id + ,sumsales + ,rk +limit 100 +; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query68.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query68.sql new file mode 100644 index 000000000..f943603f8 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query68.sql @@ -0,0 +1,41 @@ +-- start query 68 in stream 0 using template query68.tpl +SELECT c_last_name, + c_first_name, + ca_city, + bought_city, + ss_ticket_number, + extended_price, + extended_tax, + list_price +FROM (SELECT ss_ticket_number, + ss_customer_sk, + ca_city bought_city, + Sum(ss_ext_sales_price) extended_price, + Sum(ss_ext_list_price) list_price, + Sum(ss_ext_tax) extended_tax + FROM store_sales, + date_dim, + store, + household_demographics, + customer_address + WHERE store_sales.ss_sold_date_sk = date_dim.d_date_sk + AND store_sales.ss_store_sk = store.s_store_sk + AND store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk + AND store_sales.ss_addr_sk = customer_address.ca_address_sk + AND date_dim.d_dom BETWEEN 1 AND 2 + AND ( household_demographics.hd_dep_count = 8 + OR household_demographics.hd_vehicle_count = 3 ) + AND date_dim.d_year IN ( 1998, 1998 + 1, 1998 + 2 ) + AND store.s_city IN ( 'Fairview', 'Midway' ) + GROUP BY ss_ticket_number, + ss_customer_sk, + ss_addr_sk, + ca_city) dn, + customer, + customer_address current_addr +WHERE ss_customer_sk = c_customer_sk + AND customer.c_current_addr_sk = current_addr.ca_address_sk + AND current_addr.ca_city <> bought_city +ORDER BY c_last_name, + ss_ticket_number +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query69.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query69.sql new file mode 100644 index 000000000..e22094bd8 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query69.sql @@ -0,0 +1,46 @@ +SELECT cd_gender, + cd_marital_status, + cd_education_status, + Count(*) cnt1, + cd_purchase_estimate, + Count(*) cnt2, + cd_credit_rating, + Count(*) cnt3 +FROM customer c, + customer_address ca, + customer_demographics +WHERE c.c_current_addr_sk = ca.ca_address_sk + AND ca_state IN ( 'KS', 'AZ', 'NE' ) + AND cd_demo_sk = c.c_current_cdemo_sk + AND EXISTS (SELECT * + FROM store_sales, + date_dim + WHERE c.c_customer_sk = ss_customer_sk + AND ss_sold_date_sk = d_date_sk + AND d_year = 2004 + AND d_moy BETWEEN 3 AND 3 + 2) + AND ( NOT EXISTS (SELECT * + FROM web_sales, + date_dim + WHERE c.c_customer_sk = ws_bill_customer_sk + AND ws_sold_date_sk = d_date_sk + AND d_year = 2004 + AND d_moy BETWEEN 3 AND 3 + 2) + AND NOT EXISTS (SELECT * + FROM catalog_sales, + date_dim + WHERE c.c_customer_sk = cs_ship_customer_sk + AND cs_sold_date_sk = d_date_sk + AND d_year = 2004 + AND d_moy BETWEEN 3 AND 3 + 2) ) +GROUP BY cd_gender, + cd_marital_status, + cd_education_status, + cd_purchase_estimate, + cd_credit_rating +ORDER BY cd_gender, + cd_marital_status, + cd_education_status, + cd_purchase_estimate, + cd_credit_rating +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query7.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query7.sql new file mode 100644 index 000000000..6e28b46c8 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query7.sql @@ -0,0 +1,24 @@ +-- start query 7 in stream 0 using template query7.tpl +SELECT i_item_id, + Avg(ss_quantity) agg1, + Avg(ss_list_price) agg2, + Avg(ss_coupon_amt) agg3, + Avg(ss_sales_price) agg4 +FROM store_sales, + customer_demographics, + date_dim, + item, + promotion +WHERE ss_sold_date_sk = d_date_sk + AND ss_item_sk = i_item_sk + AND ss_cdemo_sk = cd_demo_sk + AND ss_promo_sk = p_promo_sk + AND cd_gender = 'F' + AND cd_marital_status = 'W' + AND cd_education_status = '2 yr Degree' + AND ( p_channel_email = 'N' + OR p_channel_event = 'N' ) + AND d_year = 1998 +GROUP BY i_item_id +ORDER BY i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query70.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query70.sql new file mode 100644 index 000000000..34c5e0656 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query70.sql @@ -0,0 +1,40 @@ +-- start query 70 in stream 0 using template query70.tpl +SELECT Sum(ss_net_profit) AS total_sum, + s_state, + s_county, + Grouping(s_state) + Grouping(s_county) AS lochierarchy, + Rank() + OVER ( + partition BY Grouping(s_state)+Grouping(s_county), CASE WHEN + Grouping( + s_county) = 0 THEN s_state END + ORDER BY Sum(ss_net_profit) DESC) AS rank_within_parent +FROM store_sales, + date_dim d1, + store +WHERE d1.d_month_seq BETWEEN 1200 AND 1200 + 11 + AND d1.d_date_sk = ss_sold_date_sk + AND s_store_sk = ss_store_sk + AND s_state IN (SELECT s_state + FROM (SELECT s_state AS + s_state, + Rank() + OVER ( + partition BY s_state + ORDER BY Sum(ss_net_profit) DESC) AS + ranking + FROM store_sales, + store, + date_dim + WHERE d_month_seq BETWEEN 1200 AND 1200 + 11 + AND d_date_sk = ss_sold_date_sk + AND s_store_sk = ss_store_sk + GROUP BY s_state) tmp1 + WHERE ranking <= 5) +GROUP BY rollup( s_state, s_county ) +ORDER BY lochierarchy DESC, + CASE + WHEN lochierarchy = 0 THEN s_state + END, + rank_within_parent +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query71.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query71.sql new file mode 100644 index 000000000..42075471e --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query71.sql @@ -0,0 +1,48 @@ +-- start query 71 in stream 0 using template query71.tpl +SELECT i_brand_id brand_id, + i_brand brand, + t_hour, + t_minute, + Sum(ext_price) ext_price +FROM item, + (SELECT ws_ext_sales_price AS ext_price, + ws_sold_date_sk AS sold_date_sk, + ws_item_sk AS sold_item_sk, + ws_sold_time_sk AS time_sk + FROM web_sales, + date_dim + WHERE d_date_sk = ws_sold_date_sk + AND d_moy = 11 + AND d_year = 2001 + UNION ALL + SELECT cs_ext_sales_price AS ext_price, + cs_sold_date_sk AS sold_date_sk, + cs_item_sk AS sold_item_sk, + cs_sold_time_sk AS time_sk + FROM catalog_sales, + date_dim + WHERE d_date_sk = cs_sold_date_sk + AND d_moy = 11 + AND d_year = 2001 + UNION ALL + SELECT ss_ext_sales_price AS ext_price, + ss_sold_date_sk AS sold_date_sk, + ss_item_sk AS sold_item_sk, + ss_sold_time_sk AS time_sk + FROM store_sales, + date_dim + WHERE d_date_sk = ss_sold_date_sk + AND d_moy = 11 + AND d_year = 2001) AS tmp, + time_dim +WHERE sold_item_sk = i_item_sk + AND i_manager_id = 1 + AND time_sk = t_time_sk + AND ( t_meal_time = 'breakfast' + OR t_meal_time = 'dinner' ) +GROUP BY i_brand, + i_brand_id, + t_hour, + t_minute +ORDER BY ext_price DESC, + i_brand_id; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query72.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query72.sql new file mode 100644 index 000000000..e0e082cb4 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query72.sql @@ -0,0 +1,49 @@ +-- start query 72 in stream 0 using template query72.tpl +SELECT i_item_desc, + w_warehouse_name, + d1.d_week_seq, + Sum(CASE + WHEN p_promo_sk IS NULL THEN 1 + ELSE 0 + END) no_promo, + Sum(CASE + WHEN p_promo_sk IS NOT NULL THEN 1 + ELSE 0 + END) promo, + Count(*) total_cnt +FROM catalog_sales + JOIN inventory + ON ( cs_item_sk = inv_item_sk ) + JOIN warehouse + ON ( w_warehouse_sk = inv_warehouse_sk ) + JOIN item + ON ( i_item_sk = cs_item_sk ) + JOIN customer_demographics + ON ( cs_bill_cdemo_sk = cd_demo_sk ) + JOIN household_demographics + ON ( cs_bill_hdemo_sk = hd_demo_sk ) + JOIN date_dim d1 + ON ( cs_sold_date_sk = d1.d_date_sk ) + JOIN date_dim d2 + ON ( inv_date_sk = d2.d_date_sk ) + JOIN date_dim d3 + ON ( cs_ship_date_sk = d3.d_date_sk ) + LEFT OUTER JOIN promotion + ON ( cs_promo_sk = p_promo_sk ) + LEFT OUTER JOIN catalog_returns + ON ( cr_item_sk = cs_item_sk + AND cr_order_number = cs_order_number ) +WHERE d1.d_week_seq = d2.d_week_seq + AND inv_quantity_on_hand < cs_quantity + AND d3.d_date > d1.d_date + INTERVAL '5' day + AND hd_buy_potential = '501-1000' + AND d1.d_year = 2002 + AND cd_marital_status = 'M' +GROUP BY i_item_desc, + w_warehouse_name, + d1.d_week_seq +ORDER BY total_cnt DESC, + i_item_desc, + w_warehouse_name, + d_week_seq +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query73.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query73.sql new file mode 100644 index 000000000..d56d682c3 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query73.sql @@ -0,0 +1,39 @@ +-- start query 73 in stream 0 using template query73.tpl +SELECT c_last_name, + c_first_name, + c_salutation, + c_preferred_cust_flag, + ss_ticket_number, + cnt +FROM (SELECT ss_ticket_number, + ss_customer_sk, + Count(*) cnt + FROM store_sales, + date_dim, + store, + household_demographics + WHERE store_sales.ss_sold_date_sk = date_dim.d_date_sk + AND store_sales.ss_store_sk = store.s_store_sk + AND store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk + AND date_dim.d_dom BETWEEN 1 AND 2 + AND ( household_demographics.hd_buy_potential = '>10000' + OR household_demographics.hd_buy_potential = '0-500' ) + AND household_demographics.hd_vehicle_count > 0 + AND CASE + WHEN household_demographics.hd_vehicle_count > 0 THEN + household_demographics.hd_dep_count / + household_demographics.hd_vehicle_count + ELSE NULL + END > 1 + AND date_dim.d_year IN ( 2000, 2000 + 1, 2000 + 2 ) + AND store.s_county IN ( 'Williamson County', 'Williamson County', + 'Williamson County', + 'Williamson County' + ) + GROUP BY ss_ticket_number, + ss_customer_sk) dj, + customer +WHERE ss_customer_sk = c_customer_sk + AND cnt BETWEEN 1 AND 5 +ORDER BY cnt DESC, + c_last_name ASC; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query74.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query74.sql new file mode 100644 index 000000000..f8d781f79 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query74.sql @@ -0,0 +1,69 @@ +-- start query 74 in stream 0 using template query74.tpl +WITH year_total + AS (SELECT c_customer_id customer_id, + c_first_name customer_first_name, + c_last_name customer_last_name, + d_year AS year1, + Sum(ss_net_paid) year_total, + 's' sale_type + FROM customer, + store_sales, + date_dim + WHERE c_customer_sk = ss_customer_sk + AND ss_sold_date_sk = d_date_sk + AND d_year IN ( 1999, 1999 + 1 ) + GROUP BY c_customer_id, + c_first_name, + c_last_name, + d_year + UNION ALL + SELECT c_customer_id customer_id, + c_first_name customer_first_name, + c_last_name customer_last_name, + d_year AS year1, + Sum(ws_net_paid) year_total, + 'w' sale_type + FROM customer, + web_sales, + date_dim + WHERE c_customer_sk = ws_bill_customer_sk + AND ws_sold_date_sk = d_date_sk + AND d_year IN ( 1999, 1999 + 1 ) + GROUP BY c_customer_id, + c_first_name, + c_last_name, + d_year) +SELECT t_s_secyear.customer_id, + t_s_secyear.customer_first_name, + t_s_secyear.customer_last_name +FROM year_total t_s_firstyear, + year_total t_s_secyear, + year_total t_w_firstyear, + year_total t_w_secyear +WHERE t_s_secyear.customer_id = t_s_firstyear.customer_id + AND t_s_firstyear.customer_id = t_w_secyear.customer_id + AND t_s_firstyear.customer_id = t_w_firstyear.customer_id + AND t_s_firstyear.sale_type = 's' + AND t_w_firstyear.sale_type = 'w' + AND t_s_secyear.sale_type = 's' + AND t_w_secyear.sale_type = 'w' + AND t_s_firstyear.year1 = 1999 + AND t_s_secyear.year1 = 1999 + 1 + AND t_w_firstyear.year1 = 1999 + AND t_w_secyear.year1 = 1999 + 1 + AND t_s_firstyear.year_total > 0 + AND t_w_firstyear.year_total > 0 + AND CASE + WHEN t_w_firstyear.year_total > 0 THEN t_w_secyear.year_total / + t_w_firstyear.year_total + ELSE NULL + END > CASE + WHEN t_s_firstyear.year_total > 0 THEN + t_s_secyear.year_total / + t_s_firstyear.year_total + ELSE NULL + END +ORDER BY 1, + 2, + 3 +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query75.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query75.sql new file mode 100644 index 000000000..25d1ccbd4 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query75.sql @@ -0,0 +1,93 @@ +-- start query 75 in stream 0 using template query75.tpl +WITH all_sales + AS (SELECT d_year, + i_brand_id, + i_class_id, + i_category_id, + i_manufact_id, + Sum(sales_cnt) AS sales_cnt, + Sum(sales_amt) AS sales_amt + FROM (SELECT d_year, + i_brand_id, + i_class_id, + i_category_id, + i_manufact_id, + cs_quantity - COALESCE(cr_return_quantity, 0) AS + sales_cnt, + cs_ext_sales_price - COALESCE(cr_return_amount, 0.0) AS + sales_amt + FROM catalog_sales + JOIN item + ON i_item_sk = cs_item_sk + JOIN date_dim + ON d_date_sk = cs_sold_date_sk + LEFT JOIN catalog_returns + ON ( cs_order_number = cr_order_number + AND cs_item_sk = cr_item_sk ) + WHERE i_category = 'Men' + UNION + SELECT d_year, + i_brand_id, + i_class_id, + i_category_id, + i_manufact_id, + ss_quantity - COALESCE(sr_return_quantity, 0) AS + sales_cnt, + ss_ext_sales_price - COALESCE(sr_return_amt, 0.0) AS + sales_amt + FROM store_sales + JOIN item + ON i_item_sk = ss_item_sk + JOIN date_dim + ON d_date_sk = ss_sold_date_sk + LEFT JOIN store_returns + ON ( ss_ticket_number = sr_ticket_number + AND ss_item_sk = sr_item_sk ) + WHERE i_category = 'Men' + UNION + SELECT d_year, + i_brand_id, + i_class_id, + i_category_id, + i_manufact_id, + ws_quantity - COALESCE(wr_return_quantity, 0) AS + sales_cnt, + ws_ext_sales_price - COALESCE(wr_return_amt, 0.0) AS + sales_amt + FROM web_sales + JOIN item + ON i_item_sk = ws_item_sk + JOIN date_dim + ON d_date_sk = ws_sold_date_sk + LEFT JOIN web_returns + ON ( ws_order_number = wr_order_number + AND ws_item_sk = wr_item_sk ) + WHERE i_category = 'Men') sales_detail + GROUP BY d_year, + i_brand_id, + i_class_id, + i_category_id, + i_manufact_id) +SELECT prev_yr.d_year AS prev_year, + curr_yr.d_year AS year1, + curr_yr.i_brand_id, + curr_yr.i_class_id, + curr_yr.i_category_id, + curr_yr.i_manufact_id, + prev_yr.sales_cnt AS prev_yr_cnt, + curr_yr.sales_cnt AS curr_yr_cnt, + curr_yr.sales_cnt - prev_yr.sales_cnt AS sales_cnt_diff, + curr_yr.sales_amt - prev_yr.sales_amt AS sales_amt_diff +FROM all_sales curr_yr, + all_sales prev_yr +WHERE curr_yr.i_brand_id = prev_yr.i_brand_id + AND curr_yr.i_class_id = prev_yr.i_class_id + AND curr_yr.i_category_id = prev_yr.i_category_id + AND curr_yr.i_manufact_id = prev_yr.i_manufact_id + AND curr_yr.d_year = 2002 + AND prev_yr.d_year = 2002 - 1 + AND Cast(curr_yr.sales_cnt AS DECIMAL(17, 2)) / Cast(prev_yr.sales_cnt AS + DECIMAL(17, 2)) + < 0.9 +ORDER BY sales_cnt_diff +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query76.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query76.sql new file mode 100644 index 000000000..f298aa015 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query76.sql @@ -0,0 +1,57 @@ +-- start query 76 in stream 0 using template query76.tpl +SELECT channel, + col_name, + d_year, + d_qoy, + i_category, + Count(*) sales_cnt, + Sum(ext_sales_price) sales_amt +FROM (SELECT 'store' AS channel, + 'ss_hdemo_sk' col_name, + d_year, + d_qoy, + i_category, + ss_ext_sales_price ext_sales_price + FROM store_sales, + item, + date_dim + WHERE ss_hdemo_sk IS NULL + AND ss_sold_date_sk = d_date_sk + AND ss_item_sk = i_item_sk + UNION ALL + SELECT 'web' AS channel, + 'ws_ship_hdemo_sk' col_name, + d_year, + d_qoy, + i_category, + ws_ext_sales_price ext_sales_price + FROM web_sales, + item, + date_dim + WHERE ws_ship_hdemo_sk IS NULL + AND ws_sold_date_sk = d_date_sk + AND ws_item_sk = i_item_sk + UNION ALL + SELECT 'catalog' AS channel, + 'cs_warehouse_sk' col_name, + d_year, + d_qoy, + i_category, + cs_ext_sales_price ext_sales_price + FROM catalog_sales, + item, + date_dim + WHERE cs_warehouse_sk IS NULL + AND cs_sold_date_sk = d_date_sk + AND cs_item_sk = i_item_sk) foo +GROUP BY channel, + col_name, + d_year, + d_qoy, + i_category +ORDER BY channel, + col_name, + d_year, + d_qoy, + i_category +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query77.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query77.sql new file mode 100644 index 000000000..ebcea2b12 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query77.sql @@ -0,0 +1,107 @@ + +-- start query 77 in stream 0 using template query77.tpl +WITH ss AS +( + SELECT s_store_sk, + Sum(ss_ext_sales_price) AS sales, + Sum(ss_net_profit) AS profit + FROM store_sales, + date_dim, + store + WHERE ss_sold_date_sk = d_date_sk + AND d_date BETWEEN Cast('2001-08-16' AS DATE) AND ( + Cast('2001-08-16' AS DATE) + INTERVAL '30' day) + AND ss_store_sk = s_store_sk + GROUP BY s_store_sk) , sr AS +( + SELECT s_store_sk, + sum(sr_return_amt) AS returns1, + sum(sr_net_loss) AS profit_loss + FROM store_returns, + date_dim, + store + WHERE sr_returned_date_sk = d_date_sk + AND d_date BETWEEN cast('2001-08-16' AS date) AND ( + cast('2001-08-16' AS date) + INTERVAL '30' day) + AND sr_store_sk = s_store_sk + GROUP BY s_store_sk), cs AS +( + SELECT cs_call_center_sk, + sum(cs_ext_sales_price) AS sales, + sum(cs_net_profit) AS profit + FROM catalog_sales, + date_dim + WHERE cs_sold_date_sk = d_date_sk + AND d_date BETWEEN cast('2001-08-16' AS date) AND ( + cast('2001-08-16' AS date) + INTERVAL '30' day) + GROUP BY cs_call_center_sk ), cr AS +( + SELECT cr_call_center_sk, + sum(cr_return_amount) AS returns1, + sum(cr_net_loss) AS profit_loss + FROM catalog_returns, + date_dim + WHERE cr_returned_date_sk = d_date_sk + AND d_date BETWEEN cast('2001-08-16' AS date) AND ( + cast('2001-08-16' AS date) + INTERVAL '30' day) + GROUP BY cr_call_center_sk ), ws AS +( + SELECT wp_web_page_sk, + sum(ws_ext_sales_price) AS sales, + sum(ws_net_profit) AS profit + FROM web_sales, + date_dim, + web_page + WHERE ws_sold_date_sk = d_date_sk + AND d_date BETWEEN cast('2001-08-16' AS date) AND ( + cast('2001-08-16' AS date) + INTERVAL '30' day) + AND ws_web_page_sk = wp_web_page_sk + GROUP BY wp_web_page_sk), wr AS +( + SELECT wp_web_page_sk, + sum(wr_return_amt) AS returns1, + sum(wr_net_loss) AS profit_loss + FROM web_returns, + date_dim, + web_page + WHERE wr_returned_date_sk = d_date_sk + AND d_date BETWEEN cast('2001-08-16' AS date) AND ( + cast('2001-08-16' AS date) + INTERVAL '30' day) + AND wr_web_page_sk = wp_web_page_sk + GROUP BY wp_web_page_sk) +SELECT + channel , + id , + sum(sales) AS sales , + sum(returns1) AS returns1 , + sum(profit) AS profit +FROM ( + SELECT 'store channel' AS channel , + ss.s_store_sk AS id , + sales , + COALESCE(returns1, 0) AS returns1 , + (profit - COALESCE(profit_loss,0)) AS profit + FROM ss + LEFT JOIN sr + ON ss.s_store_sk = sr.s_store_sk + UNION ALL + SELECT 'catalog channel' AS channel , + cs_call_center_sk AS id , + sales , + returns1 , + (profit - profit_loss) AS profit + FROM cs , + cr + UNION ALL + SELECT 'web channel' AS channel , + ws.wp_web_page_sk AS id , + sales , + COALESCE(returns1, 0) returns1 , + (profit - COALESCE(profit_loss,0)) AS profit + FROM ws + LEFT JOIN wr + ON ws.wp_web_page_sk = wr.wp_web_page_sk ) x +GROUP BY rollup (channel, id) +ORDER BY channel , + id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query78.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query78.sql new file mode 100644 index 000000000..b57b52389 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query78.sql @@ -0,0 +1,86 @@ +-- start query 78 in stream 0 using template query78.tpl +WITH ws + AS (SELECT d_year AS ws_sold_year, + ws_item_sk, + ws_bill_customer_sk ws_customer_sk, + Sum(ws_quantity) ws_qty, + Sum(ws_wholesale_cost) ws_wc, + Sum(ws_sales_price) ws_sp + FROM web_sales + LEFT JOIN web_returns + ON wr_order_number = ws_order_number + AND ws_item_sk = wr_item_sk + JOIN date_dim + ON ws_sold_date_sk = d_date_sk + WHERE wr_order_number IS NULL + GROUP BY d_year, + ws_item_sk, + ws_bill_customer_sk), + cs + AS (SELECT d_year AS cs_sold_year, + cs_item_sk, + cs_bill_customer_sk cs_customer_sk, + Sum(cs_quantity) cs_qty, + Sum(cs_wholesale_cost) cs_wc, + Sum(cs_sales_price) cs_sp + FROM catalog_sales + LEFT JOIN catalog_returns + ON cr_order_number = cs_order_number + AND cs_item_sk = cr_item_sk + JOIN date_dim + ON cs_sold_date_sk = d_date_sk + WHERE cr_order_number IS NULL + GROUP BY d_year, + cs_item_sk, + cs_bill_customer_sk), + ss + AS (SELECT d_year AS ss_sold_year, + ss_item_sk, + ss_customer_sk, + Sum(ss_quantity) ss_qty, + Sum(ss_wholesale_cost) ss_wc, + Sum(ss_sales_price) ss_sp + FROM store_sales + LEFT JOIN store_returns + ON sr_ticket_number = ss_ticket_number + AND ss_item_sk = sr_item_sk + JOIN date_dim + ON ss_sold_date_sk = d_date_sk + WHERE sr_ticket_number IS NULL + GROUP BY d_year, + ss_item_sk, + ss_customer_sk) +SELECT ss_item_sk, + Round(ss_qty / ( COALESCE(ws_qty + cs_qty, 1) ), 2) ratio, + ss_qty store_qty, + ss_wc + store_wholesale_cost, + ss_sp + store_sales_price, + COALESCE(ws_qty, 0) + COALESCE(cs_qty, 0) + other_chan_qty, + COALESCE(ws_wc, 0) + COALESCE(cs_wc, 0) + other_chan_wholesale_cost, + COALESCE(ws_sp, 0) + COALESCE(cs_sp, 0) + other_chan_sales_price +FROM ss + LEFT JOIN ws + ON ( ws_sold_year = ss_sold_year + AND ws_item_sk = ss_item_sk + AND ws_customer_sk = ss_customer_sk ) + LEFT JOIN cs + ON ( cs_sold_year = ss_sold_year + AND cs_item_sk = cs_item_sk + AND cs_customer_sk = ss_customer_sk ) +WHERE COALESCE(ws_qty, 0) > 0 + AND COALESCE(cs_qty, 0) > 0 + AND ss_sold_year = 1999 +ORDER BY ss_item_sk, + ss_qty DESC, + ss_wc DESC, + ss_sp DESC, + other_chan_qty, + other_chan_wholesale_cost, + other_chan_sales_price, + Round(ss_qty / ( COALESCE(ws_qty + cs_qty, 1) ), 2) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query79.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query79.sql new file mode 100644 index 000000000..7e8f52d09 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query79.sql @@ -0,0 +1,35 @@ +-- start query 79 in stream 0 using template query79.tpl +SELECT c_last_name, + c_first_name, + Substr(s_city, 1, 30), + ss_ticket_number, + amt, + profit +FROM (SELECT ss_ticket_number, + ss_customer_sk, + store.s_city, + Sum(ss_coupon_amt) amt, + Sum(ss_net_profit) profit + FROM store_sales, + date_dim, + store, + household_demographics + WHERE store_sales.ss_sold_date_sk = date_dim.d_date_sk + AND store_sales.ss_store_sk = store.s_store_sk + AND store_sales.ss_hdemo_sk = household_demographics.hd_demo_sk + AND ( household_demographics.hd_dep_count = 8 + OR household_demographics.hd_vehicle_count > 4 ) + AND date_dim.d_dow = 1 + AND date_dim.d_year IN ( 2000, 2000 + 1, 2000 + 2 ) + AND store.s_number_employees BETWEEN 200 AND 295 + GROUP BY ss_ticket_number, + ss_customer_sk, + ss_addr_sk, + store.s_city) ms, + customer +WHERE ss_customer_sk = c_customer_sk +ORDER BY c_last_name, + c_first_name, + Substr(s_city, 1, 30), + profit +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query8.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query8.sql new file mode 100644 index 000000000..8d98a76a4 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query8.sql @@ -0,0 +1,227 @@ +-- start query 8 in stream 0 using template query8.tpl +SELECT s_store_name, + Sum(ss_net_profit) +FROM store_sales, + date_dim, + store, + (SELECT ca_zip + FROM (SELECT Substr(ca_zip, 1, 5) ca_zip + FROM customer_address + WHERE Substr(ca_zip, 1, 5) IN ( '67436', '26121', '38443', + '63157', + '68856', '19485', '86425', + '26741', + '70991', '60899', '63573', + '47556', + '56193', '93314', '87827', + '62017', + '85067', '95390', '48091', + '10261', + '81845', '41790', '42853', + '24675', + '12840', '60065', '84430', + '57451', + '24021', '91735', '75335', + '71935', + '34482', '56943', '70695', + '52147', + '56251', '28411', '86653', + '23005', + '22478', '29031', '34398', + '15365', + '42460', '33337', '59433', + '73943', + '72477', '74081', '74430', + '64605', + '39006', '11226', '49057', + '97308', + '42663', '18187', '19768', + '43454', + '32147', '76637', '51975', + '11181', + '45630', '33129', '45995', + '64386', + '55522', '26697', '20963', + '35154', + '64587', '49752', '66386', + '30586', + '59286', '13177', '66646', + '84195', + '74316', '36853', '32927', + '12469', + '11904', '36269', '17724', + '55346', + '12595', '53988', '65439', + '28015', + '63268', '73590', '29216', + '82575', + '69267', '13805', '91678', + '79460', + '94152', '14961', '15419', + '48277', + '62588', '55493', '28360', + '14152', + '55225', '18007', '53705', + '56573', + '80245', '71769', '57348', + '36845', + '13039', '17270', '22363', + '83474', + '25294', '43269', '77666', + '15488', + '99146', '64441', '43338', + '38736', + '62754', '48556', '86057', + '23090', + '38114', '66061', '18910', + '84385', + '23600', '19975', '27883', + '65719', + '19933', '32085', '49731', + '40473', + '27190', '46192', '23949', + '44738', + '12436', '64794', '68741', + '15333', + '24282', '49085', '31844', + '71156', + '48441', '17100', '98207', + '44982', + '20277', '71496', '96299', + '37583', + '22206', '89174', '30589', + '61924', + '53079', '10976', '13104', + '42794', + '54772', '15809', '56434', + '39975', + '13874', '30753', '77598', + '78229', + '59478', '12345', '55547', + '57422', + '42600', '79444', '29074', + '29752', + '21676', '32096', '43044', + '39383', + '37296', '36295', '63077', + '16572', + '31275', '18701', '40197', + '48242', + '27219', '49865', '84175', + '30446', + '25165', '13807', '72142', + '70499', + '70464', '71429', '18111', + '70857', + '29545', '36425', '52706', + '36194', + '42963', '75068', '47921', + '74763', + '90990', '89456', '62073', + '88397', + '73963', '75885', '62657', + '12530', + '81146', '57434', '25099', + '41429', + '98441', '48713', '52552', + '31667', + '14072', '13903', '44709', + '85429', + '58017', '38295', '44875', + '73541', + '30091', '12707', '23762', + '62258', + '33247', '78722', '77431', + '14510', + '35656', '72428', '92082', + '35267', + '43759', '24354', '90952', + '11512', + '21242', '22579', '56114', + '32339', + '52282', '41791', '24484', + '95020', + '28408', '99710', '11899', + '43344', + '72915', '27644', '62708', + '74479', + '17177', '32619', '12351', + '91339', + '31169', '57081', '53522', + '16712', + '34419', '71779', '44187', + '46206', + '96099', '61910', '53664', + '12295', + '31837', '33096', '10813', + '63048', + '31732', '79118', '73084', + '72783', + '84952', '46965', '77956', + '39815', + '32311', '75329', '48156', + '30826', + '49661', '13736', '92076', + '74865', + '88149', '92397', '52777', + '68453', + '32012', '21222', '52721', + '24626', + '18210', '42177', '91791', + '75251', + '82075', '44372', '45542', + '20609', + '60115', '17362', '22750', + '90434', + '31852', '54071', '33762', + '14705', + '40718', '56433', '30996', + '40657', + '49056', '23585', '66455', + '41021', + '74736', '72151', '37007', + '21729', + '60177', '84558', '59027', + '93855', + '60022', '86443', '19541', + '86886', + '30532', '39062', '48532', + '34713', + '52077', '22564', '64638', + '15273', + '31677', '36138', '62367', + '60261', + '80213', '42818', '25113', + '72378', + '69802', '69096', '55443', + '28820', + '13848', '78258', '37490', + '30556', + '77380', '28447', '44550', + '26791', + '70609', '82182', '33306', + '43224', + '22322', '86959', '68519', + '14308', + '46501', '81131', '34056', + '61991', + '19896', '87804', '65774', + '92564' ) + INTERSECT + SELECT ca_zip + FROM (SELECT Substr(ca_zip, 1, 5) ca_zip, + Count(*) cnt + FROM customer_address, + customer + WHERE ca_address_sk = c_current_addr_sk + AND c_preferred_cust_flag = 'Y' + GROUP BY ca_zip + HAVING Count(*) > 10)A1)A2) V1 +WHERE ss_store_sk = s_store_sk + AND ss_sold_date_sk = d_date_sk + AND d_qoy = 2 + AND d_year = 2000 + AND ( Substr(s_zip, 1, 2) = Substr(V1.ca_zip, 1, 2) ) +GROUP BY s_store_name +ORDER BY s_store_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query80.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query80.sql new file mode 100644 index 000000000..4fcb9cb2f --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query80.sql @@ -0,0 +1,105 @@ +-- start query 80 in stream 0 using template query80.tpl +WITH ssr AS +( + SELECT s_store_id AS store_id, + Sum(ss_ext_sales_price) AS sales, + Sum(COALESCE(sr_return_amt, 0)) AS returns1, + Sum(ss_net_profit - COALESCE(sr_net_loss, 0)) AS profit + FROM store_sales + LEFT OUTER JOIN store_returns + ON ( + ss_item_sk = sr_item_sk + AND ss_ticket_number = sr_ticket_number), + date_dim, + store, + item, + promotion + WHERE ss_sold_date_sk = d_date_sk + AND d_date BETWEEN Cast('2000-08-26' AS DATE) AND ( + Cast('2000-08-26' AS DATE) + INTERVAL '30' day) + AND ss_store_sk = s_store_sk + AND ss_item_sk = i_item_sk + AND i_current_price > 50 + AND ss_promo_sk = p_promo_sk + AND p_channel_tv = 'N' + GROUP BY s_store_id) , csr AS +( + SELECT cp_catalog_page_id AS catalog_page_id, + sum(cs_ext_sales_price) AS sales, + sum(COALESCE(cr_return_amount, 0)) AS returns1, + sum(cs_net_profit - COALESCE(cr_net_loss, 0)) AS profit + FROM catalog_sales + LEFT OUTER JOIN catalog_returns + ON ( + cs_item_sk = cr_item_sk + AND cs_order_number = cr_order_number), + date_dim, + catalog_page, + item, + promotion + WHERE cs_sold_date_sk = d_date_sk + AND d_date BETWEEN cast('2000-08-26' AS date) AND ( + cast('2000-08-26' AS date) + INTERVAL '30' day) + AND cs_catalog_page_sk = cp_catalog_page_sk + AND cs_item_sk = i_item_sk + AND i_current_price > 50 + AND cs_promo_sk = p_promo_sk + AND p_channel_tv = 'N' + GROUP BY cp_catalog_page_id) , wsr AS +( + SELECT web_site_id, + sum(ws_ext_sales_price) AS sales, + sum(COALESCE(wr_return_amt, 0)) AS returns1, + sum(ws_net_profit - COALESCE(wr_net_loss, 0)) AS profit + FROM web_sales + LEFT OUTER JOIN web_returns + ON ( + ws_item_sk = wr_item_sk + AND ws_order_number = wr_order_number), + date_dim, + web_site, + item, + promotion + WHERE ws_sold_date_sk = d_date_sk + AND d_date BETWEEN cast('2000-08-26' AS date) AND ( + cast('2000-08-26' AS date) + INTERVAL '30' day) + AND ws_web_site_sk = web_site_sk + AND ws_item_sk = i_item_sk + AND i_current_price > 50 + AND ws_promo_sk = p_promo_sk + AND p_channel_tv = 'N' + GROUP BY web_site_id) +SELECT + channel , + id , + sum(sales) AS sales , + sum(returns1) AS returns1 , + sum(profit) AS profit +FROM ( + SELECT 'store channel' AS channel , + 'store' + || store_id AS id , + sales , + returns1 , + profit + FROM ssr + UNION ALL + SELECT 'catalog channel' AS channel , + 'catalog_page' + || catalog_page_id AS id , + sales , + returns1 , + profit + FROM csr + UNION ALL + SELECT 'web channel' AS channel , + 'web_site' + || web_site_id AS id , + sales , + returns1 , + profit + FROM wsr ) x +GROUP BY rollup (channel, id) +ORDER BY channel , + id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query81.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query81.sql new file mode 100644 index 000000000..9be95c5d2 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query81.sql @@ -0,0 +1,56 @@ + +-- start query 81 in stream 0 using template query81.tpl +WITH customer_total_return + AS (SELECT cr_returning_customer_sk AS ctr_customer_sk, + ca_state AS ctr_state, + Sum(cr_return_amt_inc_tax) AS ctr_total_return + FROM catalog_returns, + date_dim, + customer_address + WHERE cr_returned_date_sk = d_date_sk + AND d_year = 1999 + AND cr_returning_addr_sk = ca_address_sk + GROUP BY cr_returning_customer_sk, + ca_state) +SELECT c_customer_id, + c_salutation, + c_first_name, + c_last_name, + ca_street_number, + ca_street_name, + ca_street_type, + ca_suite_number, + ca_city, + ca_county, + ca_state, + ca_zip, + ca_country, + ca_gmt_offset, + ca_location_type, + ctr_total_return +FROM customer_total_return ctr1, + customer_address, + customer +WHERE ctr1.ctr_total_return > (SELECT Avg(ctr_total_return) * 1.2 + FROM customer_total_return ctr2 + WHERE ctr1.ctr_state = ctr2.ctr_state) + AND ca_address_sk = c_current_addr_sk + AND ca_state = 'TX' + AND ctr1.ctr_customer_sk = c_customer_sk +ORDER BY c_customer_id, + c_salutation, + c_first_name, + c_last_name, + ca_street_number, + ca_street_name, + ca_street_type, + ca_suite_number, + ca_city, + ca_county, + ca_state, + ca_zip, + ca_country, + ca_gmt_offset, + ca_location_type, + ctr_total_return +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query82.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query82.sql new file mode 100644 index 000000000..27295a5fb --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query82.sql @@ -0,0 +1,23 @@ + +-- start query 82 in stream 0 using template query82.tpl +SELECT + i_item_id , + i_item_desc , + i_current_price +FROM item, + inventory, + date_dim, + store_sales +WHERE i_current_price BETWEEN 63 AND 63+30 +AND inv_item_sk = i_item_sk +AND d_date_sk=inv_date_sk +AND d_date BETWEEN Cast('1998-04-27' AS DATE) AND ( + Cast('1998-04-27' AS DATE) + INTERVAL '60' day) +AND i_manufact_id IN (57,293,427,320) +AND inv_quantity_on_hand BETWEEN 100 AND 500 +AND ss_item_sk = i_item_sk +GROUP BY i_item_id, + i_item_desc, + i_current_price +ORDER BY i_item_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query83.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query83.sql new file mode 100644 index 000000000..b5c4378fa --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query83.sql @@ -0,0 +1,75 @@ +-- start query 83 in stream 0 using template query83.tpl +WITH sr_items + AS (SELECT i_item_id item_id, + Sum(sr_return_quantity) sr_item_qty + FROM store_returns, + item, + date_dim + WHERE sr_item_sk = i_item_sk + AND d_date IN (SELECT d_date + FROM date_dim + WHERE d_week_seq IN (SELECT d_week_seq + FROM date_dim + WHERE + d_date IN ( '1999-06-30', + '1999-08-28', + '1999-11-18' + ))) + AND sr_returned_date_sk = d_date_sk + GROUP BY i_item_id), + cr_items + AS (SELECT i_item_id item_id, + Sum(cr_return_quantity) cr_item_qty + FROM catalog_returns, + item, + date_dim + WHERE cr_item_sk = i_item_sk + AND d_date IN (SELECT d_date + FROM date_dim + WHERE d_week_seq IN (SELECT d_week_seq + FROM date_dim + WHERE + d_date IN ( '1999-06-30', + '1999-08-28', + '1999-11-18' + ))) + AND cr_returned_date_sk = d_date_sk + GROUP BY i_item_id), + wr_items + AS (SELECT i_item_id item_id, + Sum(wr_return_quantity) wr_item_qty + FROM web_returns, + item, + date_dim + WHERE wr_item_sk = i_item_sk + AND d_date IN (SELECT d_date + FROM date_dim + WHERE d_week_seq IN (SELECT d_week_seq + FROM date_dim + WHERE + d_date IN ( '1999-06-30', + '1999-08-28', + '1999-11-18' + ))) + AND wr_returned_date_sk = d_date_sk + GROUP BY i_item_id) +SELECT sr_items.item_id, + sr_item_qty, + sr_item_qty / ( sr_item_qty + cr_item_qty + wr_item_qty ) / 3.0 * + 100 sr_dev, + cr_item_qty, + cr_item_qty / ( sr_item_qty + cr_item_qty + wr_item_qty ) / 3.0 * + 100 cr_dev, + wr_item_qty, + wr_item_qty / ( sr_item_qty + cr_item_qty + wr_item_qty ) / 3.0 * + 100 wr_dev, + ( sr_item_qty + cr_item_qty + wr_item_qty ) / 3.0 + average +FROM sr_items, + cr_items, + wr_items +WHERE sr_items.item_id = cr_items.item_id + AND sr_items.item_id = wr_items.item_id +ORDER BY sr_items.item_id, + sr_item_qty +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query84.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query84.sql new file mode 100644 index 000000000..f7eae1a7e --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query84.sql @@ -0,0 +1,21 @@ +-- start query 84 in stream 0 using template query84.tpl +SELECT c_customer_id AS customer_id, + c_last_name + || ', ' + || c_first_name AS customername +FROM customer, + customer_address, + customer_demographics, + household_demographics, + income_band, + store_returns +WHERE ca_city = 'Green Acres' + AND c_current_addr_sk = ca_address_sk + AND ib_lower_bound >= 54986 + AND ib_upper_bound <= 54986 + 50000 + AND ib_income_band_sk = hd_income_band_sk + AND cd_demo_sk = c_current_cdemo_sk + AND hd_demo_sk = c_current_hdemo_sk + AND sr_cdemo_sk = cd_demo_sk +ORDER BY c_customer_id +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query85.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query85.sql new file mode 100644 index 000000000..be2f68d48 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query85.sql @@ -0,0 +1,52 @@ +-- start query 85 in stream 0 using template query85.tpl +SELECT Substr(r_reason_desc, 1, 20), + Avg(ws_quantity), + Avg(wr_refunded_cash), + Avg(wr_fee) +FROM web_sales, + web_returns, + web_page, + customer_demographics cd1, + customer_demographics cd2, + customer_address, + date_dim, + reason +WHERE ws_web_page_sk = wp_web_page_sk + AND ws_item_sk = wr_item_sk + AND ws_order_number = wr_order_number + AND ws_sold_date_sk = d_date_sk + AND d_year = 2001 + AND cd1.cd_demo_sk = wr_refunded_cdemo_sk + AND cd2.cd_demo_sk = wr_returning_cdemo_sk + AND ca_address_sk = wr_refunded_addr_sk + AND r_reason_sk = wr_reason_sk + AND ( ( cd1.cd_marital_status = 'W' + AND cd1.cd_marital_status = cd2.cd_marital_status + AND cd1.cd_education_status = 'Primary' + AND cd1.cd_education_status = cd2.cd_education_status + AND ws_sales_price BETWEEN 100.00 AND 150.00 ) + OR ( cd1.cd_marital_status = 'D' + AND cd1.cd_marital_status = cd2.cd_marital_status + AND cd1.cd_education_status = 'Secondary' + AND cd1.cd_education_status = cd2.cd_education_status + AND ws_sales_price BETWEEN 50.00 AND 100.00 ) + OR ( cd1.cd_marital_status = 'M' + AND cd1.cd_marital_status = cd2.cd_marital_status + AND cd1.cd_education_status = 'Advanced Degree' + AND cd1.cd_education_status = cd2.cd_education_status + AND ws_sales_price BETWEEN 150.00 AND 200.00 ) ) + AND ( ( ca_country = 'United States' + AND ca_state IN ( 'KY', 'ME', 'IL' ) + AND ws_net_profit BETWEEN 100 AND 200 ) + OR ( ca_country = 'United States' + AND ca_state IN ( 'OK', 'NE', 'MN' ) + AND ws_net_profit BETWEEN 150 AND 300 ) + OR ( ca_country = 'United States' + AND ca_state IN ( 'FL', 'WI', 'KS' ) + AND ws_net_profit BETWEEN 50 AND 250 ) ) +GROUP BY r_reason_desc +ORDER BY Substr(r_reason_desc, 1, 20), + Avg(ws_quantity), + Avg(wr_refunded_cash), + Avg(wr_fee) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query86.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query86.sql new file mode 100644 index 000000000..ec513d402 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query86.sql @@ -0,0 +1,24 @@ +-- start query 86 in stream 0 using template query86.tpl +SELECT Sum(ws_net_paid) AS total_sum, + i_category, + i_class, + Grouping(i_category) + Grouping(i_class) AS lochierarchy, + Rank() + OVER ( + partition BY Grouping(i_category)+Grouping(i_class), CASE + WHEN Grouping( + i_class) = 0 THEN i_category END + ORDER BY Sum(ws_net_paid) DESC) AS rank_within_parent +FROM web_sales, + date_dim d1, + item +WHERE d1.d_month_seq BETWEEN 1183 AND 1183 + 11 + AND d1.d_date_sk = ws_sold_date_sk + AND i_item_sk = ws_item_sk +GROUP BY rollup( i_category, i_class ) +ORDER BY lochierarchy DESC, + CASE + WHEN lochierarchy = 0 THEN i_category + END, + rank_within_parent +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query87.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query87.sql new file mode 100644 index 000000000..6f58f2e09 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query87.sql @@ -0,0 +1,21 @@ +-- start query 87 in stream 0 using template query87.tpl +select count(*) +from ((select distinct c_last_name, c_first_name, d_date + from store_sales, date_dim, customer + where store_sales.ss_sold_date_sk = date_dim.d_date_sk + and store_sales.ss_customer_sk = customer.c_customer_sk + and d_month_seq between 1188 and 1188+11) + except + (select distinct c_last_name, c_first_name, d_date + from catalog_sales, date_dim, customer + where catalog_sales.cs_sold_date_sk = date_dim.d_date_sk + and catalog_sales.cs_bill_customer_sk = customer.c_customer_sk + and d_month_seq between 1188 and 1188+11) + except + (select distinct c_last_name, c_first_name, d_date + from web_sales, date_dim, customer + where web_sales.ws_sold_date_sk = date_dim.d_date_sk + and web_sales.ws_bill_customer_sk = customer.c_customer_sk + and d_month_seq between 1188 and 1188+11) +) cool_cust +; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query88.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query88.sql new file mode 100644 index 000000000..d1945f341 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query88.sql @@ -0,0 +1,92 @@ +-- start query 88 in stream 0 using template query88.tpl +select * +from + (select count(*) h8_30_to_9 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 8 + and time_dim.t_minute >= 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s1, + (select count(*) h9_to_9_30 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 9 + and time_dim.t_minute < 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s2, + (select count(*) h9_30_to_10 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 9 + and time_dim.t_minute >= 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s3, + (select count(*) h10_to_10_30 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 10 + and time_dim.t_minute < 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s4, + (select count(*) h10_30_to_11 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 10 + and time_dim.t_minute >= 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s5, + (select count(*) h11_to_11_30 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 11 + and time_dim.t_minute < 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s6, + (select count(*) h11_30_to_12 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 11 + and time_dim.t_minute >= 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s7, + (select count(*) h12_to_12_30 + from store_sales, household_demographics , time_dim, store + where ss_sold_time_sk = time_dim.t_time_sk + and ss_hdemo_sk = household_demographics.hd_demo_sk + and ss_store_sk = s_store_sk + and time_dim.t_hour = 12 + and time_dim.t_minute < 30 + and ((household_demographics.hd_dep_count = -1 and household_demographics.hd_vehicle_count<=-1+2) or + (household_demographics.hd_dep_count = 2 and household_demographics.hd_vehicle_count<=2+2) or + (household_demographics.hd_dep_count = 3 and household_demographics.hd_vehicle_count<=3+2)) + and store.s_store_name = 'ese') s8 +; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query89.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query89.sql new file mode 100644 index 000000000..1459f9cf9 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query89.sql @@ -0,0 +1,40 @@ +-- start query 89 in stream 0 using template query89.tpl +SELECT * +FROM (SELECT i_category, + i_class, + i_brand, + s_store_name, + s_company_name, + d_moy, + Sum(ss_sales_price) sum_sales, + Avg(Sum(ss_sales_price)) + OVER ( + partition BY i_category, i_brand, s_store_name, s_company_name + ) + avg_monthly_sales + FROM item, + store_sales, + date_dim, + store + WHERE ss_item_sk = i_item_sk + AND ss_sold_date_sk = d_date_sk + AND ss_store_sk = s_store_sk + AND d_year IN ( 2002 ) + AND ( ( i_category IN ( 'Home', 'Men', 'Sports' ) + AND i_class IN ( 'paint', 'accessories', 'fitness' ) ) + OR ( i_category IN ( 'Shoes', 'Jewelry', 'Women' ) + AND i_class IN ( 'mens', 'pendants', 'swimwear' ) ) ) + GROUP BY i_category, + i_class, + i_brand, + s_store_name, + s_company_name, + d_moy) tmp1 +WHERE CASE + WHEN ( avg_monthly_sales <> 0 ) THEN ( + Abs(sum_sales - avg_monthly_sales) / avg_monthly_sales ) + ELSE NULL + END > 0.1 +ORDER BY sum_sales - avg_monthly_sales, + s_store_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query9.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query9.sql new file mode 100644 index 000000000..8073df2f9 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query9.sql @@ -0,0 +1,63 @@ +-- start query 9 in stream 0 using template query9.tpl +SELECT CASE + WHEN (SELECT Count(*) + FROM store_sales + WHERE ss_quantity BETWEEN 1 AND 20) > 3672 THEN + (SELECT Avg(ss_ext_list_price) + FROM store_sales + WHERE + ss_quantity BETWEEN 1 AND 20) + ELSE (SELECT Avg(ss_net_profit) + FROM store_sales + WHERE ss_quantity BETWEEN 1 AND 20) + END bucket1, + CASE + WHEN (SELECT Count(*) + FROM store_sales + WHERE ss_quantity BETWEEN 21 AND 40) > 3392 THEN + (SELECT Avg(ss_ext_list_price) + FROM store_sales + WHERE + ss_quantity BETWEEN 21 AND 40) + ELSE (SELECT Avg(ss_net_profit) + FROM store_sales + WHERE ss_quantity BETWEEN 21 AND 40) + END bucket2, + CASE + WHEN (SELECT Count(*) + FROM store_sales + WHERE ss_quantity BETWEEN 41 AND 60) > 32784 THEN + (SELECT Avg(ss_ext_list_price) + FROM store_sales + WHERE + ss_quantity BETWEEN 41 AND 60) + ELSE (SELECT Avg(ss_net_profit) + FROM store_sales + WHERE ss_quantity BETWEEN 41 AND 60) + END bucket3, + CASE + WHEN (SELECT Count(*) + FROM store_sales + WHERE ss_quantity BETWEEN 61 AND 80) > 26032 THEN + (SELECT Avg(ss_ext_list_price) + FROM store_sales + WHERE + ss_quantity BETWEEN 61 AND 80) + ELSE (SELECT Avg(ss_net_profit) + FROM store_sales + WHERE ss_quantity BETWEEN 61 AND 80) + END bucket4, + CASE + WHEN (SELECT Count(*) + FROM store_sales + WHERE ss_quantity BETWEEN 81 AND 100) > 23982 THEN + (SELECT Avg(ss_ext_list_price) + FROM store_sales + WHERE + ss_quantity BETWEEN 81 AND 100) + ELSE (SELECT Avg(ss_net_profit) + FROM store_sales + WHERE ss_quantity BETWEEN 81 AND 100) + END bucket5 +FROM reason +WHERE r_reason_sk = 1; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query90.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query90.sql new file mode 100644 index 000000000..bc117f29e --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query90.sql @@ -0,0 +1,28 @@ + +-- start query 90 in stream 0 using template query90.tpl +SELECT Cast(amc AS DECIMAL(15, 4)) / Cast(pmc AS DECIMAL(15, 4)) + am_pm_ratio +FROM (SELECT Count(*) amc + FROM web_sales, + household_demographics, + time_dim, + web_page + WHERE ws_sold_time_sk = time_dim.t_time_sk + AND ws_ship_hdemo_sk = household_demographics.hd_demo_sk + AND ws_web_page_sk = web_page.wp_web_page_sk + AND time_dim.t_hour BETWEEN 12 AND 12 + 1 + AND household_demographics.hd_dep_count = 8 + AND web_page.wp_char_count BETWEEN 5000 AND 5200) at1, + (SELECT Count(*) pmc + FROM web_sales, + household_demographics, + time_dim, + web_page + WHERE ws_sold_time_sk = time_dim.t_time_sk + AND ws_ship_hdemo_sk = household_demographics.hd_demo_sk + AND ws_web_page_sk = web_page.wp_web_page_sk + AND time_dim.t_hour BETWEEN 20 AND 20 + 1 + AND household_demographics.hd_dep_count = 8 + AND web_page.wp_char_count BETWEEN 5000 AND 5200) pt +ORDER BY am_pm_ratio +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query91.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query91.sql new file mode 100644 index 000000000..457aa8b45 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query91.sql @@ -0,0 +1,32 @@ +-- start query 91 in stream 0 using template query91.tpl +SELECT cc_call_center_id Call_Center, + cc_name Call_Center_Name, + cc_manager Manager, + Sum(cr_net_loss) Returns_Loss +FROM call_center, + catalog_returns, + date_dim, + customer, + customer_address, + customer_demographics, + household_demographics +WHERE cr_call_center_sk = cc_call_center_sk + AND cr_returned_date_sk = d_date_sk + AND cr_returning_customer_sk = c_customer_sk + AND cd_demo_sk = c_current_cdemo_sk + AND hd_demo_sk = c_current_hdemo_sk + AND ca_address_sk = c_current_addr_sk + AND d_year = 1999 + AND d_moy = 12 + AND ( ( cd_marital_status = 'M' + AND cd_education_status = 'Unknown' ) + OR ( cd_marital_status = 'W' + AND cd_education_status = 'Advanced Degree' ) ) + AND hd_buy_potential LIKE 'Unknown%' + AND ca_gmt_offset = -7 +GROUP BY cc_call_center_id, + cc_name, + cc_manager, + cd_marital_status, + cd_education_status +ORDER BY Sum(cr_net_loss) DESC; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query92.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query92.sql new file mode 100644 index 000000000..c1c3dceba --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query92.sql @@ -0,0 +1,22 @@ +-- start query 92 in stream 0 using template query92.tpl +SELECT + Sum(ws_ext_discount_amt) AS `Excess Discount Amount` +FROM web_sales , + item , + date_dim +WHERE i_manufact_id = 718 +AND i_item_sk = ws_item_sk +AND d_date BETWEEN '2002-03-29' AND ( + Cast('2002-03-29' AS DATE) + INTERVAL '90' day) +AND d_date_sk = ws_sold_date_sk +AND ws_ext_discount_amt > + ( + SELECT 1.3 * avg(ws_ext_discount_amt) + FROM web_sales , + date_dim + WHERE ws_item_sk = i_item_sk + AND d_date BETWEEN '2002-03-29' AND ( + cast('2002-03-29' AS date) + INTERVAL '90' day) + AND d_date_sk = ws_sold_date_sk ) +ORDER BY sum(ws_ext_discount_amt) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query93.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query93.sql new file mode 100644 index 000000000..f7fdc3296 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query93.sql @@ -0,0 +1,22 @@ +-- start query 93 in stream 0 using template query93.tpl +SELECT ss_customer_sk, + Sum(act_sales) sumsales +FROM (SELECT ss_item_sk, + ss_ticket_number, + ss_customer_sk, + CASE + WHEN sr_return_quantity IS NOT NULL THEN + ( ss_quantity - sr_return_quantity ) * ss_sales_price + ELSE ( ss_quantity * ss_sales_price ) + END act_sales + FROM store_sales + LEFT OUTER JOIN store_returns + ON ( sr_item_sk = ss_item_sk + AND sr_ticket_number = ss_ticket_number ), + reason + WHERE sr_reason_sk = r_reason_sk + AND r_reason_desc = 'reason 38') t +GROUP BY ss_customer_sk +ORDER BY sumsales, + ss_customer_sk +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query94.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query94.sql new file mode 100644 index 000000000..e5e8b7568 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query94.sql @@ -0,0 +1,29 @@ +-- start query 94 in stream 0 using template query94.tpl +SELECT + Count(DISTINCT ws_order_number) AS `order count` , + Sum(ws_ext_ship_cost) AS `total shipping cost` , + Sum(ws_net_profit) AS `total net profit` +FROM web_sales ws1 , + date_dim , + customer_address , + web_site +WHERE d_date BETWEEN '2000-3-01' AND ( + Cast('2000-3-01' AS DATE) + INTERVAL '60' day) +AND ws1.ws_ship_date_sk = d_date_sk +AND ws1.ws_ship_addr_sk = ca_address_sk +AND ca_state = 'MT' +AND ws1.ws_web_site_sk = web_site_sk +AND web_company_name = 'pri' +AND EXISTS + ( + SELECT * + FROM web_sales ws2 + WHERE ws1.ws_order_number = ws2.ws_order_number + AND ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk) +AND NOT EXISTS + ( + SELECT * + FROM web_returns wr1 + WHERE ws1.ws_order_number = wr1.wr_order_number) +ORDER BY count(DISTINCT ws_order_number) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query95.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query95.sql new file mode 100644 index 000000000..d0aa46a95 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query95.sql @@ -0,0 +1,37 @@ +-- start query 95 in stream 0 using template query95.tpl +WITH ws_wh AS +( + SELECT ws1.ws_order_number, + ws1.ws_warehouse_sk wh1, + ws2.ws_warehouse_sk wh2 + FROM web_sales ws1, + web_sales ws2 + WHERE ws1.ws_order_number = ws2.ws_order_number + AND ws1.ws_warehouse_sk <> ws2.ws_warehouse_sk) +SELECT + Count(DISTINCT ws_order_number) AS `order count` , + Sum(ws_ext_ship_cost) AS `total shipping cost` , + Sum(ws_net_profit) AS `total net profit` +FROM web_sales ws1 , + date_dim , + customer_address , + web_site +WHERE d_date BETWEEN '2000-4-01' AND ( + Cast('2000-4-01' AS DATE) + INTERVAL '60' day) +AND ws1.ws_ship_date_sk = d_date_sk +AND ws1.ws_ship_addr_sk = ca_address_sk +AND ca_state = 'IN' +AND ws1.ws_web_site_sk = web_site_sk +AND web_company_name = 'pri' +AND ws1.ws_order_number IN + ( + SELECT ws_order_number + FROM ws_wh) +AND ws1.ws_order_number IN + ( + SELECT wr_order_number + FROM web_returns, + ws_wh + WHERE wr_order_number = ws_wh.ws_order_number) +ORDER BY count(DISTINCT ws_order_number) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query96.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query96.sql new file mode 100644 index 000000000..1f7731524 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query96.sql @@ -0,0 +1,15 @@ +-- start query 96 in stream 0 using template query96.tpl +SELECT Count(*) +FROM store_sales, + household_demographics, + time_dim, + store +WHERE ss_sold_time_sk = time_dim.t_time_sk + AND ss_hdemo_sk = household_demographics.hd_demo_sk + AND ss_store_sk = s_store_sk + AND time_dim.t_hour = 15 + AND time_dim.t_minute >= 30 + AND household_demographics.hd_dep_count = 7 + AND store.s_store_name = 'ese' +ORDER BY Count(*) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query97.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query97.sql new file mode 100644 index 000000000..6a6be875a --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query97.sql @@ -0,0 +1,40 @@ + +-- start query 97 in stream 0 using template query97.tpl +WITH ssci + AS (SELECT ss_customer_sk customer_sk, + ss_item_sk item_sk + FROM store_sales, + date_dim + WHERE ss_sold_date_sk = d_date_sk + AND d_month_seq BETWEEN 1196 AND 1196 + 11 + GROUP BY ss_customer_sk, + ss_item_sk), + csci + AS (SELECT cs_bill_customer_sk customer_sk, + cs_item_sk item_sk + FROM catalog_sales, + date_dim + WHERE cs_sold_date_sk = d_date_sk + AND d_month_seq BETWEEN 1196 AND 1196 + 11 + GROUP BY cs_bill_customer_sk, + cs_item_sk) +SELECT Sum(CASE + WHEN ssci.customer_sk IS NOT NULL + AND csci.customer_sk IS NULL THEN 1 + ELSE 0 + END) store_only, + Sum(CASE + WHEN ssci.customer_sk IS NULL + AND csci.customer_sk IS NOT NULL THEN 1 + ELSE 0 + END) catalog_only, + Sum(CASE + WHEN ssci.customer_sk IS NOT NULL + AND csci.customer_sk IS NOT NULL THEN 1 + ELSE 0 + END) store_and_catalog +FROM ssci + FULL OUTER JOIN csci + ON ( ssci.customer_sk = csci.customer_sk + AND ssci.item_sk = csci.item_sk ) +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query98.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query98.sql new file mode 100644 index 000000000..62eaaa518 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query98.sql @@ -0,0 +1,29 @@ + +-- start query 98 in stream 0 using template query98.tpl +SELECT i_item_id, + i_item_desc, + i_category, + i_class, + i_current_price, + Sum(ss_ext_sales_price) AS itemrevenue, + Sum(ss_ext_sales_price) * 100 / Sum(Sum(ss_ext_sales_price)) + OVER ( + PARTITION BY i_class) AS revenueratio +FROM store_sales, + item, + date_dim +WHERE ss_item_sk = i_item_sk + AND i_category IN ( 'Men', 'Home', 'Electronics' ) + AND ss_sold_date_sk = d_date_sk + AND d_date BETWEEN CAST('2000-05-18' AS DATE) AND ( + CAST('2000-05-18' AS DATE) + INTERVAL '30' DAY ) +GROUP BY i_item_id, + i_item_desc, + i_category, + i_class, + i_current_price +ORDER BY i_category, + i_class, + i_item_id, + i_item_desc, + revenueratio; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query99.sql b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query99.sql new file mode 100644 index 000000000..66b433064 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/sparksql/query99.sql @@ -0,0 +1,47 @@ + + +-- start query 99 in stream 0 using template query99.tpl +SELECT Substr(w_warehouse_name, 1, 20), + sm_type, + cc_name, + Sum(CASE + WHEN ( cs_ship_date_sk - cs_sold_date_sk <= 30 ) THEN 1 + ELSE 0 + END) AS `30 days`, + Sum(CASE + WHEN ( cs_ship_date_sk - cs_sold_date_sk > 30 ) + AND ( cs_ship_date_sk - cs_sold_date_sk <= 60 ) THEN 1 + ELSE 0 + END) AS `31-60 days`, + Sum(CASE + WHEN ( cs_ship_date_sk - cs_sold_date_sk > 60 ) + AND ( cs_ship_date_sk - cs_sold_date_sk <= 90 ) THEN 1 + ELSE 0 + END) AS `61-90 days`, + Sum(CASE + WHEN ( cs_ship_date_sk - cs_sold_date_sk > 90 ) + AND ( cs_ship_date_sk - cs_sold_date_sk <= 120 ) THEN + 1 + ELSE 0 + END) AS `91-120 days`, + Sum(CASE + WHEN ( cs_ship_date_sk - cs_sold_date_sk > 120 ) THEN 1 + ELSE 0 + END) AS `>120 days` +FROM catalog_sales, + warehouse, + ship_mode, + call_center, + date_dim +WHERE d_month_seq BETWEEN 1200 AND 1200 + 11 + AND cs_ship_date_sk = d_date_sk + AND cs_warehouse_sk = w_warehouse_sk + AND cs_ship_mode_sk = sm_ship_mode_sk + AND cs_call_center_sk = cc_call_center_sk +GROUP BY Substr(w_warehouse_name, 1, 20), + sm_type, + cc_name +ORDER BY Substr(w_warehouse_name, 1, 20), + sm_type, + cc_name +LIMIT 100; diff --git a/datajunction-server/tests/sql/parsing/queries/tpcds/test_tpcds.py b/datajunction-server/tests/sql/parsing/queries/tpcds/test_tpcds.py new file mode 100644 index 000000000..55c486f18 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/queries/tpcds/test_tpcds.py @@ -0,0 +1,346 @@ +""" +test parsing tpcds queries into DJ ASTs +""" + +# mypy: ignore-errors +from difflib import SequenceMatcher + +import pytest +import sqlparse + +from datajunction_server.sql.parsing.backends.antlr4 import parse, parse_statement + +ansi_tpcds_files = [ + ("./ansi/query1.sql"), + ("./ansi/query2.sql"), + ("./ansi/query3.sql"), + ("./ansi/query4.sql"), + ("./ansi/query5.sql"), + ("./ansi/query6.sql"), + ("./ansi/query7.sql"), + ("./ansi/query8.sql"), + ("./ansi/query9.sql"), + ("./ansi/query10.sql"), + ("./ansi/query11.sql"), + ("./ansi/query12.sql"), + ("./ansi/query13.sql"), + ("./ansi/query14.sql"), + ("./ansi/query15.sql"), + ("./ansi/query16.sql"), + ("./ansi/query17.sql"), + ("./ansi/query18.sql"), + ("./ansi/query19.sql"), + ("./ansi/query20.sql"), + ("./ansi/query21.sql"), + ("./ansi/query22.sql"), + ("./ansi/query23.sql"), + ("./ansi/query24.sql"), + ("./ansi/query25.sql"), + ("./ansi/query26.sql"), + ("./ansi/query27.sql"), + ("./ansi/query28.sql"), + ("./ansi/query29.sql"), + ("./ansi/query30.sql"), + ("./ansi/query31.sql"), + ("./ansi/query32.sql"), + ("./ansi/query33.sql"), + ("./ansi/query34.sql"), + ("./ansi/query35.sql"), + ("./ansi/query36.sql"), + ("./ansi/query37.sql"), + ("./ansi/query38.sql"), + ("./ansi/query39.sql"), + ("./ansi/query40.sql"), + ("./ansi/query41.sql"), + ("./ansi/query42.sql"), + ("./ansi/query43.sql"), + ("./ansi/query44.sql"), + ("./ansi/query45.sql"), + ("./ansi/query46.sql"), + ("./ansi/query47.sql"), + ("./ansi/query48.sql"), + ("./ansi/query49.sql"), + ("./ansi/query50.sql"), + ("./ansi/query51.sql"), + ("./ansi/query52.sql"), + ("./ansi/query53.sql"), + ("./ansi/query54.sql"), + ("./ansi/query55.sql"), + ("./ansi/query56.sql"), + ("./ansi/query57.sql"), + ("./ansi/query58.sql"), + ("./ansi/query59.sql"), + ("./ansi/query60.sql"), + ("./ansi/query61.sql"), + ("./ansi/query62.sql"), + ("./ansi/query63.sql"), + ("./ansi/query64.sql"), + ("./ansi/query65.sql"), + ("./ansi/query66.sql"), + ("./ansi/query67.sql"), + ("./ansi/query68.sql"), + ("./ansi/query69.sql"), + ("./ansi/query70.sql"), + ("./ansi/query71.sql"), + ("./ansi/query72.sql"), + ("./ansi/query73.sql"), + ("./ansi/query74.sql"), + ("./ansi/query75.sql"), + ("./ansi/query76.sql"), + ("./ansi/query77.sql"), + ("./ansi/query78.sql"), + ("./ansi/query79.sql"), + ("./ansi/query80.sql"), + ("./ansi/query81.sql"), + ("./ansi/query82.sql"), + ("./ansi/query83.sql"), + ("./ansi/query84.sql"), + ("./ansi/query85.sql"), + ("./ansi/query86.sql"), + ("./ansi/query87.sql"), + ("./ansi/query88.sql"), + ("./ansi/query89.sql"), + ("./ansi/query90.sql"), + ("./ansi/query91.sql"), + ("./ansi/query92.sql"), + ("./ansi/query93.sql"), + ("./ansi/query94.sql"), + ("./ansi/query95.sql"), + ("./ansi/query96.sql"), + ("./ansi/query97.sql"), + ("./ansi/query98.sql"), + ("./ansi/query99.sql"), +] + +spark_tpcds_files = [ + ("./sparksql/query1.sql"), + ("./sparksql/query2.sql"), + ("./sparksql/query3.sql"), + ("./sparksql/query4.sql"), + ("./sparksql/query5.sql"), + ("./sparksql/query6.sql"), + ("./sparksql/query7.sql"), + ("./sparksql/query8.sql"), + ("./sparksql/query9.sql"), + ("./sparksql/query10.sql"), + ("./sparksql/query11.sql"), + ("./sparksql/query12.sql"), + ("./sparksql/query13.sql"), + ("./sparksql/query14.sql"), + ("./sparksql/query15.sql"), + ("./sparksql/query16.sql"), + ("./sparksql/query17.sql"), + ("./sparksql/query18.sql"), + ("./sparksql/query19.sql"), + ("./sparksql/query20.sql"), + ("./sparksql/query21.sql"), + ("./sparksql/query22.sql"), + ("./sparksql/query23.sql"), + ("./sparksql/query24.sql"), + ("./sparksql/query25.sql"), + ("./sparksql/query26.sql"), + ("./sparksql/query27.sql"), + ("./sparksql/query28.sql"), + ("./sparksql/query29.sql"), + ("./sparksql/query30.sql"), + ("./sparksql/query31.sql"), + ("./sparksql/query32.sql"), + ("./sparksql/query33.sql"), + ("./sparksql/query34.sql"), + ("./sparksql/query35.sql"), + ("./sparksql/query36.sql"), + ("./sparksql/query37.sql"), + ("./sparksql/query38.sql"), + ("./sparksql/query39.sql"), + ("./sparksql/query40.sql"), + ("./sparksql/query41.sql"), + ("./sparksql/query42.sql"), + ("./sparksql/query43.sql"), + ("./sparksql/query44.sql"), + ("./sparksql/query45.sql"), + ("./sparksql/query46.sql"), + ("./sparksql/query47.sql"), + ("./sparksql/query48.sql"), + ("./sparksql/query49.sql"), + ("./sparksql/query50.sql"), + ("./sparksql/query51.sql"), + ("./sparksql/query52.sql"), + ("./sparksql/query53.sql"), + ("./sparksql/query54.sql"), + ("./sparksql/query55.sql"), + ("./sparksql/query56.sql"), + ("./sparksql/query57.sql"), + ("./sparksql/query58.sql"), + ("./sparksql/query59.sql"), + ("./sparksql/query60.sql"), + ("./sparksql/query61.sql"), + ("./sparksql/query62.sql"), + ("./sparksql/query63.sql"), + ("./sparksql/query64.sql"), + ("./sparksql/query65.sql"), + ("./sparksql/query66.sql"), + ("./sparksql/query67.sql"), + ("./sparksql/query68.sql"), + ("./sparksql/query69.sql"), + ("./sparksql/query70.sql"), + ("./sparksql/query71.sql"), + ("./sparksql/query72.sql"), + ("./sparksql/query73.sql"), + ("./sparksql/query74.sql"), + ("./sparksql/query75.sql"), + ("./sparksql/query76.sql"), + ("./sparksql/query77.sql"), + ("./sparksql/query78.sql"), + ("./sparksql/query79.sql"), + ("./sparksql/query80.sql"), + ("./sparksql/query81.sql"), + ("./sparksql/query82.sql"), + ("./sparksql/query83.sql"), + ("./sparksql/query84.sql"), + ("./sparksql/query85.sql"), + ("./sparksql/query86.sql"), + ("./sparksql/query87.sql"), + ("./sparksql/query88.sql"), + ("./sparksql/query89.sql"), + ("./sparksql/query90.sql"), + ("./sparksql/query91.sql"), + ("./sparksql/query92.sql"), + ("./sparksql/query93.sql"), + ("./sparksql/query94.sql"), + ("./sparksql/query95.sql"), + ("./sparksql/query96.sql"), + ("./sparksql/query97.sql"), + ("./sparksql/query98.sql"), + ("./sparksql/query99.sql"), +] + + +def similar(a, b): + return SequenceMatcher(None, a, b).ratio() + + +@pytest.mark.skipif("not config.getoption('tpcds')") +@pytest.mark.parametrize( + "query_file", + ansi_tpcds_files + spark_tpcds_files, +) +def test_tpcds_parse(query_file, request, monkeypatch): + """ + Test that TPCDS queries parse with no errors + """ + monkeypatch.chdir(request.fspath.dirname) + with open(query_file, encoding="UTF-8") as file: + content = file.read() + for query in content.split(";"): + if not query.isspace(): + parse_statement(query) + + +@pytest.mark.skipif("not config.getoption('tpcds')") +@pytest.mark.parametrize( + "query_file", + ansi_tpcds_files + spark_tpcds_files, +) +def test_tpcds_to_ast(query_file, request, monkeypatch): + """ + Test that TPCDS queries are converted into DJ ASTs with no errors + """ + monkeypatch.chdir(request.fspath.dirname) + with open(query_file, encoding="UTF-8") as file: + content = file.read() + for query in content.split(";"): + if not query.isspace(): + parse(query) + + +@pytest.mark.skipif("not config.getoption('tpcds')") +@pytest.mark.parametrize( + "query_file", + ansi_tpcds_files + spark_tpcds_files, +) +def test_tpcds_circular_parse(query_file, request, monkeypatch): + """ + Test that the string representation of TPCDS DJ ASTs can be re-parsed + """ + monkeypatch.chdir(request.fspath.dirname) + with open(query_file, encoding="UTF-8") as file: + content = file.read() + for query in content.split(";"): + if not query.isspace(): + query_ast = parse(query) + # Below print statements show up when you include --capture=tee-sys + # These are helpful when you want to visually compare the query outputs + print( + """ + """, + ) + print( + f""" + ### ORIGINAL QUERY {query_file} ### + """, + ) + print(sqlparse.format(query, reindent=True, keyword_case="upper")) + print( + """ + ### DJ AST __str__ ### + """, + ) + print( + sqlparse.format( + str(query_ast), + reindent=True, + keyword_case="upper", + ), + ) + print( + """ + """, + ) + + +@pytest.mark.skipif("not config.getoption('tpcds')") +@pytest.mark.parametrize( + "query_file", + ansi_tpcds_files + spark_tpcds_files, +) +def test_tpcds_circular_parse_and_compare(query_file, request, monkeypatch): + """ + Compare the string representation of TPCDS DJ ASTs to the original query + """ + monkeypatch.chdir(request.fspath.dirname) + with open(query_file, encoding="UTF-8") as file: + content = file.read() + for query in content.split(";"): + if not query.isspace(): + query_ast = parse(query) + parse(str(query_ast)) + assert sqlparse.format( + query, + reindent=True, + keyword_case="upper", + ) == sqlparse.format( + str(query_ast), + reindent=True, + keyword_case="upper", + ) + + +@pytest.mark.parametrize( + "query_file", + spark_tpcds_files, +) +def test_tpcds_ast_parse_comparisons( + query_file, + request, + monkeypatch, + compare_query_strings_fixture, +): + """ + Test str -> parse(1) -> DJ AST -> str -> parse(2) and comparing (1) and (2) + """ + monkeypatch.chdir(request.fspath.dirname) + with open(query_file, encoding="UTF-8") as file: + content = file.read() + for query in content.split(";"): + if query.strip(): + assert compare_query_strings_fixture(query, str(parse(query))) diff --git a/datajunction-server/tests/sql/parsing/test_ast.py b/datajunction-server/tests/sql/parsing/test_ast.py new file mode 100644 index 000000000..9218601e8 --- /dev/null +++ b/datajunction-server/tests/sql/parsing/test_ast.py @@ -0,0 +1,1283 @@ +""" +testing ast Nodes and their methods +""" + +from typing import cast + +import pytest +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession + +from datajunction_server.errors import DJException +from datajunction_server.sql.parsing import ast, types +from datajunction_server.sql.parsing.backends.antlr4 import parse +from tests.sql.utils import compare_query_strings + + +@pytest.mark.asyncio +async def test_ast_compile_table( + session: AsyncSession, + client_with_roads: AsyncClient, +): + """ + Test compiling the primary table from a query + + Includes client_with_roads fixture so that roads examples are loaded into session + """ + query = parse("SELECT hard_hat_id, last_name, first_name FROM default.hard_hats") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.select.from_.relations[0].primary.compile(ctx) # type: ignore + assert not exc.errors + + node = query.select.from_.relations[ # type: ignore + 0 + ].primary._dj_node + assert node + assert node.name == "default.hard_hats" + + +@pytest.mark.asyncio +async def test_ast_compile_table_missing_node(session: AsyncSession): + """ + Test compiling a table when the node is missing + """ + query = parse("SELECT a FROM foo") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.select.from_.relations[0].primary.compile(ctx) # type: ignore + assert "No node `foo` exists of kind" in exc.errors[0].message + + query = parse("SELECT a FROM foo, bar, baz") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.select.from_.relations[0].primary.compile(ctx) # type: ignore + assert "No node `foo` exists of kind" in exc.errors[0].message + await query.select.from_.relations[1].primary.compile(ctx) # type: ignore + assert "No node `bar` exists of kind" in exc.errors[1].message + await query.select.from_.relations[2].primary.compile(ctx) # type: ignore + assert "No node `baz` exists of kind" in exc.errors[2].message + + query = parse("SELECT a FROM foo LEFT JOIN bar") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.select.from_.relations[0].primary.compile(ctx) # type: ignore + assert "No node `foo` exists of kind" in exc.errors[0].message + await query.select.from_.relations[0].extensions[0].right.compile(ctx) # type: ignore + assert "No node `bar` exists of kind" in exc.errors[1].message + + query = parse("SELECT a FROM foo LEFT JOIN (SELECT b FROM bar) b") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.select.from_.relations[0].primary.compile(ctx) # type: ignore + assert "No node `foo` exists of kind" in exc.errors[0].message + await ( + query.select.from_.relations[0].extensions[0].right.select.from_.relations # type: ignore + )[0].primary.compile( + ctx, + ) + assert "No node `bar` exists of kind" in exc.errors[1].message + + +@pytest.mark.asyncio +async def test_ast_compile_query( + session: AsyncSession, + client_with_roads: AsyncClient, +): + """ + Test compiling an entire query + """ + query = parse("SELECT hard_hat_id, last_name, first_name FROM default.hard_hats") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + + node = query.select.from_.relations[ # type: ignore + 0 + ].primary._dj_node + assert node + assert node.name == "default.hard_hats" + + +@pytest.mark.asyncio +async def test_ast_compile_query_missing_columns( + session: AsyncSession, + client_with_roads: AsyncClient, +): + """ + Test compiling a query with missing columns + """ + query = parse("SELECT hard_hat_id, column_foo, column_bar FROM default.hard_hats") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert ( + "Column `column_foo` does not exist on any valid table." + in exc.errors[0].message + ) + assert ( + "Column `column_bar` does not exist on any valid table." + in exc.errors[1].message + ) + + node = query.select.from_.relations[ # type: ignore + 0 + ].primary._dj_node + assert node + assert node.name == "default.hard_hats" + + +@pytest.mark.asyncio +async def test_ast_compile_missing_references(session: AsyncSession): + """ + Test getting dependencies from a query that has dangling references when set not to raise + """ + query = parse("select a, b, c from does_not_exist") + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + _, danglers = await query.extract_dependencies(ctx) + assert "does_not_exist" in danglers + + +@pytest.mark.asyncio +async def test_ast_compile_raise_on_ambiguous_column( + session: AsyncSession, + client_with_basic: AsyncClient, +): + """ + Test raising on ambiguous column + """ + query = parse( + "SELECT country FROM basic.transform.country_agg a " + "LEFT JOIN basic.dimension.countries b on a.country = b.country", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert ( + "Column `country` found in multiple tables. Consider using fully qualified name." + in exc.errors[0].message + ) + + +@pytest.mark.asyncio +async def test_ast_compile_having( + session: AsyncSession, + client_with_dbt: AsyncClient, +): + """ + Test using having + """ + query = parse( + "SELECT order_date, status FROM dbt.source.jaffle_shop.orders " + "GROUP BY dbt.dimension.customers.id " + "HAVING dbt.dimension.customers.id=1", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + + node = query.select.from_.relations[0].primary._dj_node # type: ignore + assert node + assert node.name == "dbt.source.jaffle_shop.orders" + + +@pytest.mark.asyncio +async def test_ast_compile_explode(session: AsyncSession): + """ + Test explode + """ + query_str = """ + SELECT EXPLODE(foo.my_map) AS (col1, col2) + FROM (SELECT MAP(1.0, '2', 3.0, '4') AS my_map) AS foo + """ + query = parse(query_str) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + + assert query.columns[0].name == ast.Name( # type: ignore + name="col1", + quote_style="", + namespace=None, + ) + assert query.columns[1].name == ast.Name( # type: ignore + name="col2", + quote_style="", + namespace=None, + ) + assert isinstance(query.columns[0].type, types.FloatType) + assert isinstance(query.columns[1].type, types.StringType) + + assert compare_query_strings(str(query), query_str) + + +@pytest.mark.asyncio +async def test_ast_compile_lateral_view_explode1(session: AsyncSession): + """ + Test lateral view explode + """ + + query = parse( + """SELECT a, b, c, c_age.col, d_age.col + FROM (SELECT 1 as a, 2 as b, 3 as c, ARRAY(30,60) as d, ARRAY(40,80) as e) AS foo + LATERAL VIEW EXPLODE(d) c_age + LATERAL VIEW EXPLODE(e) d_age; + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + + assert not exc.errors + + assert query.columns[0].is_compiled() + assert query.columns[1].is_compiled() + assert query.columns[2].is_compiled() + assert query.columns[3].is_compiled() + assert query.columns[4].is_compiled() + assert query.columns[0].name == ast.Name( # type: ignore + name="a", + quote_style="", + namespace=None, + ) + assert query.columns[1].name == ast.Name( # type: ignore + name="b", + quote_style="", + namespace=None, + ) + assert query.columns[2].name == ast.Name( # type: ignore + name="c", + quote_style="", + namespace=None, + ) + assert query.columns[3].name == ast.Name( # type: ignore + name="col", + quote_style="", + namespace=ast.Name(name="c_age", quote_style="", namespace=None), + ) + assert query.columns[4].name == ast.Name( # type: ignore + name="col", + quote_style="", + namespace=ast.Name(name="d_age", quote_style="", namespace=None), + ) + assert isinstance(query.columns[0].type, types.IntegerType) + assert isinstance(query.columns[1].type, types.IntegerType) + assert isinstance(query.columns[2].type, types.IntegerType) + assert isinstance(query.columns[3].type, types.IntegerType) + assert isinstance(query.columns[4].type, types.IntegerType) + assert query.columns[0].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[1].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[2].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[3].table.alias_or_name == ast.Name( # type: ignore + name="c_age", + quote_style="", + namespace=None, + ) + assert query.columns[4].table.alias_or_name == ast.Name( # type: ignore + name="d_age", + quote_style="", + namespace=None, + ) + + +@pytest.mark.asyncio +async def test_ast_compile_lateral_view_explode2(session: AsyncSession): + """ + Test lateral view explode + """ + + query = parse( + """SELECT a, b, c, c_age, d_age + FROM (SELECT 1 as a, 2 as b, 3 as c, ARRAY(30,60) as d, ARRAY(40,80) as e) AS foo + LATERAL VIEW EXPLODE(d) AS c_age + LATERAL VIEW EXPLODE(e) AS d_age;""", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + + assert query.columns[0].is_compiled() + assert query.columns[1].is_compiled() + assert query.columns[2].is_compiled() + assert query.columns[3].is_compiled() + assert query.columns[4].is_compiled() + assert query.columns[0].name == ast.Name( # type: ignore + name="a", + quote_style="", + namespace=None, + ) + assert query.columns[1].name == ast.Name( # type: ignore + name="b", + quote_style="", + namespace=None, + ) + assert query.columns[2].name == ast.Name( # type: ignore + name="c", + quote_style="", + namespace=None, + ) + assert query.columns[3].name == ast.Name( # type: ignore + name="c_age", + quote_style="", + namespace=None, + ) + assert query.columns[4].name == ast.Name( # type: ignore + name="d_age", + quote_style="", + namespace=None, + ) + assert isinstance(query.columns[0].type, types.IntegerType) + assert isinstance(query.columns[1].type, types.IntegerType) + assert isinstance(query.columns[2].type, types.IntegerType) + assert isinstance(query.columns[3].type, types.IntegerType) + assert isinstance(query.columns[4].type, types.IntegerType) + assert query.columns[0].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[1].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[2].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[3].table.alias_or_name == ast.Name( # type: ignore + name="EXPLODE", + quote_style="", + namespace=None, + ) + assert query.columns[4].table.alias_or_name == ast.Name( # type: ignore + name="EXPLODE", + quote_style="", + namespace=None, + ) + + +@pytest.mark.asyncio +async def test_ast_compile_lateral_view_explode3(session: AsyncSession): + """ + Test lateral view explode of array constant + """ + + query = parse( + """SELECT a, b, c, d, e, v.en + FROM (SELECT 1 as a, 2 as b, 3 as c) AS foo + LATERAL VIEW EXPLODE(ARRAY(30, 60)) AS d + LATERAL VIEW EXPLODE(ARRAY(40, 80)) AS e + LATERAL VIEW EXPLODE(ARRAY(100, 200)) v AS en;""", + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert parse(str(query)) == query + assert "LATERAL VIEW EXPLODE(ARRAY(100, 200)) v AS en" in str(query) + + assert query.columns[0].is_compiled() + assert query.columns[1].is_compiled() + assert query.columns[2].is_compiled() + assert query.columns[3].is_compiled() + assert query.columns[4].is_compiled() + assert query.columns[5].is_compiled() + assert query.columns[0].name == ast.Name( # type: ignore + name="a", + quote_style="", + namespace=None, + ) + assert query.columns[1].name == ast.Name( # type: ignore + name="b", + quote_style="", + namespace=None, + ) + assert query.columns[2].name == ast.Name( # type: ignore + name="c", + quote_style="", + namespace=None, + ) + assert query.columns[3].name == ast.Name( # type: ignore + name="d", + quote_style="", + namespace=None, + ) + assert query.columns[4].name == ast.Name( # type: ignore + name="e", + quote_style="", + namespace=None, + ) + assert query.columns[5].name == ast.Name( # type: ignore + name="en", + quote_style="", + namespace=ast.Name(name="v"), + ) + assert isinstance(query.columns[0].type, types.IntegerType) + assert isinstance(query.columns[1].type, types.IntegerType) + assert isinstance(query.columns[2].type, types.IntegerType) + assert isinstance(query.columns[3].type, types.IntegerType) + assert isinstance(query.columns[4].type, types.IntegerType) + assert isinstance(query.columns[5].type, types.IntegerType) + assert query.columns[0].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[1].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[2].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[3].table.alias_or_name == ast.Name( # type: ignore + name="EXPLODE", + quote_style="", + namespace=None, + ) + assert query.columns[4].table.alias_or_name == ast.Name( # type: ignore + name="EXPLODE", + quote_style="", + namespace=None, + ) + assert query.columns[5].table.alias_or_name == ast.Name( # type: ignore + name="v", + quote_style="", + namespace=None, + ) + + +@pytest.mark.asyncio +async def test_ast_compile_lateral_view_explode4( + session: AsyncSession, + client: AsyncClient, +): + """ + Test lateral view explode of an upstream column + """ + await client.post("/namespaces/default/") + response = await client.post("/catalogs/", json={"name": "default"}) + assert response.status_code in (200, 201) + response = await client.post( + "/nodes/source/", + json={ + "columns": [ + {"name": "a", "type": "int"}, + ], + "description": "Placeholder source node", + "mode": "published", + "name": "default.a", + "catalog": "default", + "schema_": "a", + "table": "a", + }, + ) + assert response.status_code in (200, 201) + response = await client.post( + "/nodes/transform/", + json={ + "description": "A projection with an array", + "query": "SELECT ARRAY(30, 60) as foo_array FROM default.a", + "mode": "published", + "name": "default.foo_array_example", + }, + ) + assert response.status_code in (200, 201) + + query = parse( + """ + SELECT foo_array, a, b + FROM default.foo_array_example + LATERAL VIEW EXPLODE(foo_array) AS a + LATERAL VIEW EXPLODE(foo_array) AS b; + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + + assert query.columns[0].is_compiled() + assert query.columns[1].is_compiled() + assert query.columns[2].is_compiled() + assert query.columns[0].name == ast.Name( # type: ignore + name="foo_array", + quote_style="", + namespace=None, + ) + assert query.columns[1].name == ast.Name( # type: ignore + name="a", + quote_style="", + namespace=None, + ) + assert query.columns[2].name == ast.Name( # type: ignore + name="b", + quote_style="", + namespace=None, + ) + assert isinstance(query.columns[0].type, types.ListType) + assert isinstance(query.columns[1].type, types.IntegerType) + assert isinstance(query.columns[2].type, types.IntegerType) + assert query.columns[0].table.alias_or_name == ast.Name( # type: ignore + name="foo_array_example", + quote_style="", + namespace=ast.Name(name="default", quote_style="", namespace=None), + ) + assert query.columns[1].table.alias_or_name == ast.Name( # type: ignore + name="EXPLODE", + quote_style="", + namespace=None, + ) + assert query.columns[2].table.alias_or_name == ast.Name( # type: ignore + name="EXPLODE", + quote_style="", + namespace=None, + ) + + +@pytest.mark.asyncio +async def test_ast_compile_lateral_view_explode5(session: AsyncSession): + """ + Test both a lateral and horizontal explode + """ + + query = parse( + """SELECT a, b, c, d.col, e.col, EXPLODE(ARRAY(30, 60)) + FROM (SELECT 1 as a, 2 as b, 3 as c) AS foo + LATERAL VIEW EXPLODE(ARRAY(30, 60)) d + LATERAL VIEW EXPLODE(ARRAY(40, 80)) e; + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + + assert not exc.errors + + assert query.columns[0].is_compiled() + assert query.columns[1].is_compiled() + assert query.columns[2].is_compiled() + assert query.columns[3].is_compiled() + assert query.columns[4].is_compiled() + assert query.columns[0].name == ast.Name( # type: ignore + name="a", + quote_style="", + namespace=None, + ) + assert query.columns[1].name == ast.Name( # type: ignore + name="b", + quote_style="", + namespace=None, + ) + assert query.columns[2].name == ast.Name( # type: ignore + name="c", + quote_style="", + namespace=None, + ) + assert query.columns[3].name == ast.Name( # type: ignore + name="col", + quote_style="", + namespace=ast.Name(name="d", quote_style="", namespace=None), + ) + assert query.columns[4].name == ast.Name( # type: ignore + name="col", + quote_style="", + namespace=ast.Name(name="e", quote_style="", namespace=None), + ) + assert query.columns[5].name == ast.Name( # type: ignore + name="col", + quote_style="", + namespace=None, + ) + assert isinstance(query.columns[0].type, types.IntegerType) + assert isinstance(query.columns[1].type, types.IntegerType) + assert isinstance(query.columns[2].type, types.IntegerType) + assert isinstance(query.columns[3].type, types.IntegerType) + assert isinstance(query.columns[4].type, types.IntegerType) + assert isinstance(query.columns[5].type, types.IntegerType) + assert query.columns[0].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[1].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[2].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[3].table.alias_or_name == ast.Name( # type: ignore + name="d", + quote_style="", + namespace=None, + ) + assert query.columns[4].table.alias_or_name == ast.Name( # type: ignore + name="e", + quote_style="", + namespace=None, + ) + assert query.columns[5].table is None # type: ignore + + +@pytest.mark.asyncio +async def test_ast_compile_lateral_view_explode6(session: AsyncSession): + """ + Test lateral view explode of a map (table aliased) + """ + + query = parse( + """SELECT a, b, c, c_age.key, c_age.value, d_age.key, d_age.value + FROM ( + SELECT 1 as a, 2 as b, 3 as c, MAP('a',1,'b',2) as d, MAP('c',1,'d',2) as e + ) AS foo + LATERAL VIEW EXPLODE(d) c_age + LATERAL VIEW EXPLODE(e) d_age; + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + + assert not exc.errors + + assert query.columns[0].is_compiled() + assert query.columns[1].is_compiled() + assert query.columns[2].is_compiled() + assert query.columns[3].is_compiled() + assert query.columns[4].is_compiled() + assert query.columns[5].is_compiled() + assert query.columns[6].is_compiled() + assert query.columns[0].name == ast.Name( # type: ignore + name="a", + quote_style="", + namespace=None, + ) + assert query.columns[1].name == ast.Name( # type: ignore + name="b", + quote_style="", + namespace=None, + ) + assert query.columns[2].name == ast.Name( # type: ignore + name="c", + quote_style="", + namespace=None, + ) + assert query.columns[3].name == ast.Name( # type: ignore + name="key", + quote_style="", + namespace=ast.Name("c_age"), + ) + assert query.columns[4].name == ast.Name( # type: ignore + name="value", + quote_style="", + namespace=ast.Name("c_age"), + ) + assert query.columns[5].name == ast.Name( # type: ignore + name="key", + quote_style="", + namespace=ast.Name("d_age"), + ) + assert query.columns[6].name == ast.Name( # type: ignore + name="value", + quote_style="", + namespace=ast.Name("d_age"), + ) + assert isinstance(query.columns[0].type, types.IntegerType) + assert isinstance(query.columns[1].type, types.IntegerType) + assert isinstance(query.columns[2].type, types.IntegerType) + assert isinstance(query.columns[3].type, types.StringType) + assert isinstance(query.columns[4].type, types.IntegerType) + assert isinstance(query.columns[5].type, types.StringType) + assert isinstance(query.columns[6].type, types.IntegerType) + assert query.columns[0].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[1].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[2].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[3].table.alias_or_name == ast.Name( # type: ignore + name="c_age", + quote_style="", + namespace=None, + ) + assert query.columns[4].table.alias_or_name == ast.Name( # type: ignore + name="c_age", + quote_style="", + namespace=None, + ) + assert query.columns[5].table.alias_or_name == ast.Name( # type: ignore + name="d_age", + quote_style="", + namespace=None, + ) + assert query.columns[6].table.alias_or_name == ast.Name( # type: ignore + name="d_age", + quote_style="", + namespace=None, + ) + + +@pytest.mark.asyncio +async def test_ast_compile_lateral_view_explode7(session: AsyncSession): + """ + Test lateral view explode of a map (column aliased) + """ + + query = parse( + """SELECT a, b, c, k1, v1, k2, v2 + FROM ( + SELECT 1 as a, 2 as b, 3 as c, MAP('a',1,'b',2) as d, MAP('c',1,'d',2) as e + ) AS foo + LATERAL VIEW EXPLODE(d) AS k1, v1 + LATERAL VIEW EXPLODE(e) AS k2, v2; + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + + assert not exc.errors + + assert query.columns[0].is_compiled() + assert query.columns[1].is_compiled() + assert query.columns[2].is_compiled() + assert query.columns[3].is_compiled() + assert query.columns[4].is_compiled() + assert query.columns[5].is_compiled() + assert query.columns[6].is_compiled() + assert query.columns[0].name == ast.Name( # type: ignore + name="a", + quote_style="", + namespace=None, + ) + assert query.columns[1].name == ast.Name( # type: ignore + name="b", + quote_style="", + namespace=None, + ) + assert query.columns[2].name == ast.Name( # type: ignore + name="c", + quote_style="", + namespace=None, + ) + assert query.columns[3].name == ast.Name( # type: ignore + name="k1", + quote_style="", + namespace=None, + ) + assert query.columns[4].name == ast.Name( # type: ignore + name="v1", + quote_style="", + namespace=None, + ) + assert query.columns[5].name == ast.Name( # type: ignore + name="k2", + quote_style="", + namespace=None, + ) + assert query.columns[6].name == ast.Name( # type: ignore + name="v2", + quote_style="", + namespace=None, + ) + assert isinstance(query.columns[0].type, types.IntegerType) + assert isinstance(query.columns[1].type, types.IntegerType) + assert isinstance(query.columns[2].type, types.IntegerType) + assert isinstance(query.columns[3].type, types.StringType) + assert isinstance(query.columns[4].type, types.IntegerType) + assert isinstance(query.columns[5].type, types.StringType) + assert isinstance(query.columns[6].type, types.IntegerType) + assert query.columns[0].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[1].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[2].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[3].table.alias_or_name == ast.Name( # type: ignore + name="EXPLODE", + quote_style="", + namespace=None, + ) + assert query.columns[4].table.alias_or_name == ast.Name( # type: ignore + name="EXPLODE", + quote_style="", + namespace=None, + ) + assert query.columns[5].table.alias_or_name == ast.Name( # type: ignore + name="EXPLODE", + quote_style="", + namespace=None, + ) + assert query.columns[6].table.alias_or_name == ast.Name( # type: ignore + name="EXPLODE", + quote_style="", + namespace=None, + ) + + +@pytest.mark.asyncio +async def test_ast_compile_lateral_view_explode8(session: AsyncSession): + """ + Test lateral view explode of a map (both table and column aliased) + """ + + query = parse( + """SELECT a, b, c, c_age.k1, c_age.v1, d_age.k2, d_age.v2 + FROM ( + SELECT 1 as a, 2 as b, 3 as c, MAP('a',1,'b',2) as d, MAP('c',1,'d',2) as e + ) AS foo + LATERAL VIEW EXPLODE(d) c_age AS k1, v1 + LATERAL VIEW EXPLODE(e) d_age AS k2, v2; + """, + ) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + + assert not exc.errors + + assert query.columns[0].is_compiled() + assert query.columns[1].is_compiled() + assert query.columns[2].is_compiled() + assert query.columns[3].is_compiled() + assert query.columns[4].is_compiled() + assert query.columns[5].is_compiled() + assert query.columns[6].is_compiled() + assert query.columns[0].name == ast.Name( # type: ignore + name="a", + quote_style="", + namespace=None, + ) + assert query.columns[1].name == ast.Name( # type: ignore + name="b", + quote_style="", + namespace=None, + ) + assert query.columns[2].name == ast.Name( # type: ignore + name="c", + quote_style="", + namespace=None, + ) + assert query.columns[3].name == ast.Name( # type: ignore + name="k1", + quote_style="", + namespace=ast.Name("c_age"), + ) + assert query.columns[4].name == ast.Name( # type: ignore + name="v1", + quote_style="", + namespace=ast.Name("c_age"), + ) + assert query.columns[5].name == ast.Name( # type: ignore + name="k2", + quote_style="", + namespace=ast.Name("d_age"), + ) + assert query.columns[6].name == ast.Name( # type: ignore + name="v2", + quote_style="", + namespace=ast.Name("d_age"), + ) + assert isinstance(query.columns[0].type, types.IntegerType) + assert isinstance(query.columns[1].type, types.IntegerType) + assert isinstance(query.columns[2].type, types.IntegerType) + assert isinstance(query.columns[3].type, types.StringType) + assert isinstance(query.columns[4].type, types.IntegerType) + assert isinstance(query.columns[5].type, types.StringType) + assert isinstance(query.columns[6].type, types.IntegerType) + assert query.columns[0].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[1].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[2].table.alias_or_name == ast.Name( # type: ignore + name="foo", + quote_style="", + namespace=None, + ) + assert query.columns[3].table.alias_or_name == ast.Name( # type: ignore + name="c_age", + quote_style="", + namespace=None, + ) + assert query.columns[4].table.alias_or_name == ast.Name( # type: ignore + name="c_age", + quote_style="", + namespace=None, + ) + assert query.columns[5].table.alias_or_name == ast.Name( # type: ignore + name="d_age", + quote_style="", + namespace=None, + ) + assert query.columns[6].table.alias_or_name == ast.Name( # type: ignore + name="d_age", + quote_style="", + namespace=None, + ) + + +@pytest.mark.asyncio +async def test_ast_compile_inline_table(session: AsyncSession): + """ + Test parsing and compiling an inline table with VALUES (...) + """ + query_str_explicit_columns = """SELECT + w.a AS one, + w.b AS two, + w.c AS three, + w.d AS four +FROM VALUES + ('1', 1, 2, 1), + ('11', 1, 3, 1), + ('111', 1, 2, 1), + ('1111', 1, 3, 1), + ('11111', 1, 4, 1), + ('111111', 1, 5, 1) AS w(a, b, c, d)""" + expected_columns = [ + ("one", types.StringType()), + ("two", types.IntegerType()), + ("three", types.IntegerType()), + ("four", types.IntegerType()), + ] + expected_values = [ + [ + ast.String(value="'1'"), + ast.Number(value=1, _type=None), + ast.Number(value=2, _type=None), + ast.Number(value=1, _type=None), + ], + [ + ast.String(value="'11'"), + ast.Number(value=1, _type=None), + ast.Number(value=3, _type=None), + ast.Number(value=1, _type=None), + ], + [ + ast.String(value="'111'"), + ast.Number(value=1, _type=None), + ast.Number(value=2, _type=None), + ast.Number(value=1, _type=None), + ], + [ + ast.String(value="'1111'"), + ast.Number(value=1, _type=None), + ast.Number(value=3, _type=None), + ast.Number(value=1, _type=None), + ], + [ + ast.String(value="'11111'"), + ast.Number(value=1, _type=None), + ast.Number(value=4, _type=None), + ast.Number(value=1, _type=None), + ], + [ + ast.String(value="'111111'"), + ast.Number(value=1, _type=None), + ast.Number(value=5, _type=None), + ast.Number(value=1, _type=None), + ], + ] + expected_table_name = ast.Name( # type: ignore + name="w", + quote_style="", + namespace=None, + ) + query = parse(query_str_explicit_columns) + exc = DJException() + assert not exc.errors + + ctx = ast.CompileContext(session=session, exception=exc) + assert parse(str(query)) == query + assert compare_query_strings(str(query), query_str_explicit_columns) + + await query.compile(ctx) + assert [ + (col.alias_or_name.name, col.type) # type: ignore + for col in query.select.projection + ] == expected_columns + assert query.select.from_.relations[0].primary.values == expected_values # type: ignore + assert query.columns[0].table.alias_or_name == expected_table_name # type: ignore + + query_str_implicit_columns = """SELECT + w.col1 AS one, + w.col2 AS two, + w.col3 AS three, + w.col4 AS four +FROM VALUES + ('1', 1, 2, 1), + ('11', 1, 3, 1), + ('111', 1, 2, 1), + ('1111', 1, 3, 1), + ('11111', 1, 4, 1), + ('111111', 1, 5, 1) AS w""" + query = parse(query_str_implicit_columns) + exc = DJException() + assert not exc.errors + + ctx = ast.CompileContext(session=session, exception=exc) + assert parse(str(query)) == query + assert compare_query_strings(str(query), query_str_implicit_columns) + + await query.compile(ctx) + assert [ + (col.alias_or_name.name, col.type) # type: ignore + for col in query.select.projection + ] == expected_columns + assert query.select.from_.relations[0].primary.values == expected_values # type: ignore + assert query.columns[0].table.alias_or_name == expected_table_name # type: ignore + + query_str = """SELECT tab.source FROM VALUES ('a'), ('b'), ('c') AS tab(source)""" + query = parse(query_str) + exc = DJException() + assert not exc.errors + assert str(parse(str(query))) == str(parse(query_str)) + + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert [ + (col.alias_or_name.name, col.type) # type: ignore + for col in query.select.projection + ] == [("source", types.StringType())] + assert [ + val[0].value + for val in query.select.from_.relations[0].primary.values # type: ignore + ] == ["'a'", "'b'", "'c'"] + assert query.columns[0].table.alias_or_name == ast.Name( # type: ignore + name="tab", + quote_style="", + namespace=None, + ) + + query_str = """SELECT tab.col1 FROM VALUES ('a'), ('b'), ('c') AS tab""" + query = parse(query_str) + exc = DJException() + assert not exc.errors + assert str(parse(str(query))) == str(parse(query_str)) + + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert [ + (col.alias_or_name.name, col.type) # type: ignore + for col in query.select.projection + ] == [("col1", types.StringType())] + assert [ + val[0].value + for val in query.select.from_.relations[0].primary.values # type: ignore + ] == ["'a'", "'b'", "'c'"] + assert query.columns[0].table.alias_or_name == ast.Name( # type: ignore + name="tab", + quote_style="", + namespace=None, + ) + + +@pytest.mark.asyncio +async def test_ast_subscript_handling(session: AsyncSession): + """ + Test parsing a query with subscripts + """ + query_str = """SELECT + w.a[1]['a'] as a_, + w.a[1]['b'] as b_ +FROM VALUES + (array(named_struct('a', 1, 'b', 2)), array(named_struct('a', 300, 'b', 20))) AS w(a)""" + query = parse(str(query_str)) + assert str(parse(str(query))) == str(parse(str(query_str))) + exc = DJException() + ctx = ast.CompileContext(session=session, exception=exc) + await query.compile(ctx) + assert not exc.errors + assert [ + (col.alias_or_name.name, col.type) # type: ignore + for col in query.select.projection + ] == [("a_", types.IntegerType()), ("b_", types.IntegerType())] + + +@pytest.mark.asyncio +async def test_ast_hints(): + """ + Test that parsing a query with hints will yield an AST that includes the hints + """ + # Test join hints for broadcast joins + query_str = ( + """SELECT /*+ BROADCAST(t1) */ * FROM t1 INNER JOIN t2 ON t1.key = t2.key""" + ) + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "BROADCAST" + assert [col.name.name for col in query.select.hints[0].parameters] == ["t1"] + assert "/*+ BROADCAST(t1) */" in str(query) + + query_str = ( + """SELECT /*+ BROADCASTJOIN(t1) */ * FROM t1 INNER JOIN t2 ON t1.key = t2.key""" + ) + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "BROADCASTJOIN" + assert [col.name.name for col in query.select.hints[0].parameters] == ["t1"] + assert "/*+ BROADCASTJOIN(t1) */" in str(query) + + query_str = ( + """SELECT /*+ MAPJOIN(t2) */ * FROM t1 right JOIN t2 ON t1.key = t2.key""" + ) + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "MAPJOIN" + assert [col.name.name for col in query.select.hints[0].parameters] == ["t2"] + assert "/*+ MAPJOIN(t2) */" in str(query) + + # Test join hints for shuffle sort merge join + query_str = ( + """SELECT /*+ SHUFFLE_MERGE(t1) */ * FROM t1 INNER JOIN t2 ON t1.key = t2.key""" + ) + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "SHUFFLE_MERGE" + assert [col.name.name for col in query.select.hints[0].parameters] == ["t1"] + assert "/*+ SHUFFLE_MERGE(t1) */" in str(query) + + query_str = ( + """SELECT /*+ MERGEJOIN(t2) */ * FROM t1 INNER JOIN t2 ON t1.key = t2.key""" + ) + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "MERGEJOIN" + assert [col.name.name for col in query.select.hints[0].parameters] == ["t2"] + assert "/*+ MERGEJOIN(t2) */" in str(query) + + query_str = """SELECT /*+ MERGE(t1) */ * FROM t1 INNER JOIN t2 ON t1.key = t2.key""" + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "MERGE" + assert [col.name.name for col in query.select.hints[0].parameters] == ["t1"] + assert "/*+ MERGE(t1) */" in str(query) + + # Test join hints for shuffle hash join + query_str = ( + """SELECT /*+ SHUFFLE_HASH(t1) */ * FROM t1 INNER JOIN t2 ON t1.key = t2.key""" + ) + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "SHUFFLE_HASH" + assert [col.name.name for col in query.select.hints[0].parameters] == ["t1"] + assert "/*+ SHUFFLE_HASH(t1) */" in str(query) + + # Test join hints for shuffle-and-replicate nested loop join + query_str = "SELECT /*+ SHUFFLE_REPLICATE_NL(t1) */ * FROM t1 INNER JOIN t2 ON t1.key = t2.key" + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "SHUFFLE_REPLICATE_NL" + assert [col.name.name for col in query.select.hints[0].parameters] == ["t1"] + assert "/*+ SHUFFLE_REPLICATE_NL(t1) */" in str(query) + + # Test multiple join hints + query_str = ( + "SELECT /*+ BROADCAST(t1), MERGE(t1, t2) */ * FROM t1 " + "INNER JOIN t2 ON t1.key = t2.key" + ) + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "BROADCAST" + assert [col.name.name for col in query.select.hints[0].parameters] == ["t1"] + assert cast(ast.Hint, query.select.hints[1]).name.name == "MERGE" + assert [col.name.name for col in query.select.hints[1].parameters] == ["t1", "t2"] + assert "/*+ BROADCAST(t1), MERGE(t1, t2) */" in str(query) + + # Test partitioning hints + query_str = """SELECT /*+ REPARTITION(c) */ c, b, a FROM t""" + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "REPARTITION" + assert [str(col) for col in query.select.hints[0].parameters] == ["c"] + assert "/*+ REPARTITION(c) */" in str(query) + + query_str = """SELECT /*+ COALESCE(3) */ * FROM t""" + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "COALESCE" + assert [str(col) for col in query.select.hints[0].parameters] == ["3"] + assert "/*+ COALESCE(3) */" in str(query) + + query_str = """SELECT /*+ REPARTITION(3) */ * FROM t""" + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "REPARTITION" + assert [str(col) for col in query.select.hints[0].parameters] == ["3"] + assert "/*+ REPARTITION(3) */" in str(query) + + query_str = """SELECT /*+ REPARTITION(3, c) */ * FROM t""" + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "REPARTITION" + assert [str(col) for col in query.select.hints[0].parameters] == ["3", "c"] + assert "/*+ REPARTITION(3, c) */" in str(query) + + query_str = """SELECT /*+ REPARTITION_BY_RANGE(c) */ * FROM t""" + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "REPARTITION_BY_RANGE" + assert [str(col) for col in query.select.hints[0].parameters] == ["c"] + assert "/*+ REPARTITION_BY_RANGE(c) */" in str(query) + + query_str = """SELECT /*+ REPARTITION_BY_RANGE(3, c) */ * FROM t""" + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "REPARTITION_BY_RANGE" + assert [str(col) for col in query.select.hints[0].parameters] == ["3", "c"] + assert "/*+ REPARTITION_BY_RANGE(3, c) */" in str(query) + + query_str = """SELECT /*+ REBALANCE */ * FROM t""" + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "REBALANCE" + assert [str(col) for col in query.select.hints[0].parameters] == [] + assert "/*+ REBALANCE */" in str(query) + + query_str = """SELECT /*+ REBALANCE(3) */ * FROM t""" + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "REBALANCE" + assert [str(col) for col in query.select.hints[0].parameters] == ["3"] + assert "/*+ REBALANCE(3) */" in str(query) + + query_str = """SELECT /*+ REBALANCE(c) */ * FROM t""" + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "REBALANCE" + assert [str(col) for col in query.select.hints[0].parameters] == ["c"] + assert "/*+ REBALANCE(c) */" in str(query) + + query_str = """SELECT /*+ REBALANCE(3, c) */ * FROM t""" + query = parse(str(query_str)) + assert cast(ast.Hint, query.select.hints[0]).name.name == "REBALANCE" + assert [str(col) for col in query.select.hints[0].parameters] == ["3", "c"] + assert "/*+ REBALANCE(3, c) */" in str(query) diff --git a/datajunction-server/tests/sql/utils.py b/datajunction-server/tests/sql/utils.py new file mode 100644 index 000000000..991d20f62 --- /dev/null +++ b/datajunction-server/tests/sql/utils.py @@ -0,0 +1,48 @@ +""" +Helper functions. +""" + +import os + +from sqlalchemy.sql import Select + +from datajunction_server.sql.parsing.backends.antlr4 import parse + +TPCDS_QUERY_SET = ["tpcds_q01", "tpcds_q99"] + + +def query_to_string(query: Select) -> str: + """ + Helper function to compile a SQLAlchemy query to a string. + """ + return str(query.compile(compile_kwargs={"literal_binds": True})) + + +def compare_query_strings(str1: str, str2: str) -> bool: + """ + compare two query strings + """ + return parse(str(str1)).compare(parse(str(str2))) + + +def assert_query_strings_equal(str1: str, str2: str): + """ + Assert that two query strings are equal + """ + assert str(parse(str(str1))) == str(parse(str(str2))) + + +def read_query(name: str) -> str: + """ + Read a tpcds query given filename e.g. tpcds_q01.sql + """ + with open( + os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "parsing", + "queries", + name, + ), + encoding="utf-8", + ) as file: + return file.read() diff --git a/datajunction-server/tests/superset_test.py b/datajunction-server/tests/superset_test.py new file mode 100644 index 000000000..58b61d788 --- /dev/null +++ b/datajunction-server/tests/superset_test.py @@ -0,0 +1,70 @@ +""" +Tests for the Superset DB engine spec. +""" + +from pytest_mock import MockerFixture +from requests_mock.mocker import Mocker +from yarl import URL + +from datajunction_server.superset import DJEngineSpec + + +def test_select_star() -> None: + """ + Test ``select_star``. + """ + assert DJEngineSpec.select_star() == ( + "SELECT 'DJ does not support data preview, since the `metrics` table is a " + "virtual table representing the whole repository of metrics. An " + "administrator should configure the DJ database with the " + "`disable_data_preview` attribute set to `true` in the `extra` field.' AS " + "warning" + ) + + +def test_get_metrics(mocker: MockerFixture, requests_mock: Mocker) -> None: + """ + Test ``get_metrics``. + """ + database = mocker.MagicMock() + with database.get_sqla_engine_with_context() as engine: + engine.connect().connection.base_url = URL( + "https://localhost:8000/0", + ) + requests_mock.get( + "https://localhost:8000/0/metrics/", + json=["core.num_comments"], + ) + inspector = mocker.MagicMock() + assert DJEngineSpec.get_metrics(database, inspector, "some-table", "main") == [ + { + "metric_name": "core.num_comments", + "expression": '"core.num_comments"', + "description": "", + }, + ] + + +def test_get_view_names(mocker: MockerFixture) -> None: + """ + Test ``get_view_names``. + """ + database = mocker.MagicMock() + inspector = mocker.MagicMock() + assert DJEngineSpec.get_view_names(database, inspector, "main") == set() + + +def test_execute(mocker: MockerFixture) -> None: + """ + Test ``execute``. + + The method is almost identical to the superclass, with the only difference that it + quotes identifiers starting with an underscore. + """ + cursor = mocker.MagicMock() + super_ = mocker.patch("datajunction_server.superset.super") + DJEngineSpec.execute(cursor, "SELECT time AS __timestamp FROM table") + super_().execute.assert_called_with( + cursor, + 'SELECT time AS "__timestamp" FROM table', + ) diff --git a/datajunction-server/tests/transpilation_test.py b/datajunction-server/tests/transpilation_test.py new file mode 100644 index 000000000..8c752f6da --- /dev/null +++ b/datajunction-server/tests/transpilation_test.py @@ -0,0 +1,88 @@ +"""Tests the transpilation plugins.""" + +import pytest +from pytest_mock import MockerFixture + +from datajunction_server.errors import DJPluginNotFoundException +from datajunction_server.models.engine import Dialect +from datajunction_server.models.metric import TranslatedSQL +from datajunction_server.models.sql import GeneratedSQL, NodeNameVersion +from datajunction_server.transpilation import get_transpilation_plugin + + +class MockSettings: + """ + Mock settings object + """ + + sql_transpilation_library = "sqlglot" + + +def test_get_transpilation_plugin() -> None: + """ + Test ``get_transpilation_plugin`` + """ + # Will raise an error with for an unknown transpilation library + with pytest.raises(DJPluginNotFoundException) as excinfo: + get_transpilation_plugin("somepackage") + assert "No SQL transpilation plugin found for package `somepackage`!" in str( + excinfo, + ) + + package_name = "sqlglot" + known = get_transpilation_plugin(package_name) + assert known.package_name == package_name + assert known.transpile_sql("1") == "1" + assert ( + known.transpile_sql( + "1", + input_dialect=Dialect.SPARK, + output_dialect=Dialect.TRINO, + ) + == "1" + ) + assert known.transpile_sql("1", output_dialect=Dialect.TRINO) == "1" + + +def test_translated_sql(mocker: MockerFixture) -> None: + """ + Verify that the TranslatedSQL object will call the configured transpilation plugin + """ + mock_settings = mocker.MagicMock() + mock_settings.return_value = MockSettings() + mocker.patch("datajunction_server.models.metric.get_settings", mock_settings) + translated_sql = TranslatedSQL( + sql="1", + columns=[], + dialect=Dialect.SPARK, + ) + assert translated_sql.sql == "1" + generated_sql = GeneratedSQL( + node=NodeNameVersion(name="a", version="v1.0"), + sql="1", + columns=[], + dialect=Dialect.SPARK, + ) + assert generated_sql.sql == "1" + + +def test_druid_sql(mocker: MockerFixture) -> None: + """ + Verify that the TranslatedSQL object will call the configured transpilation plugin + """ + mock_settings = mocker.MagicMock() + mock_settings.return_value = MockSettings() + mocker.patch("datajunction_server.models.metric.get_settings", mock_settings) + translated_sql = TranslatedSQL( + sql="SELECT 1", + columns=[], + dialect=Dialect.DRUID, + ) + assert translated_sql.sql == "SELECT\n 1" + generated_sql = GeneratedSQL( + node=NodeNameVersion(name="a", version="v1.0"), + sql="SELECT 1", + columns=[], + dialect=Dialect.DRUID, + ) + assert generated_sql.sql == "SELECT 1" diff --git a/datajunction-server/tests/utils_test.py b/datajunction-server/tests/utils_test.py new file mode 100644 index 000000000..d15bf2004 --- /dev/null +++ b/datajunction-server/tests/utils_test.py @@ -0,0 +1,172 @@ +""" +Tests for ``datajunction_server.utils``. +""" + +import logging +from unittest.mock import patch + +import pytest +from pytest_mock import MockerFixture +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from starlette.background import BackgroundTasks +from testcontainers.postgres import PostgresContainer +from yarl import URL + +from datajunction_server.config import Settings +from datajunction_server.database.user import OAuthProvider, User +from datajunction_server.errors import DJException +from datajunction_server.utils import ( + Version, + get_and_update_current_user, + get_engine, + get_issue_url, + get_query_service_client, + get_session, + get_settings, + setup_logging, +) + + +def test_setup_logging() -> None: + """ + Test ``setup_logging``. + """ + setup_logging("debug") + assert logging.root.level == logging.DEBUG + + with pytest.raises(ValueError) as excinfo: + setup_logging("invalid") + assert str(excinfo.value) == "Invalid log level: invalid" + + +@pytest.mark.asyncio +async def test_get_session(mocker: MockerFixture) -> None: + """ + Test ``get_session``. + """ + with patch( + "fastapi.BackgroundTasks", + mocker.MagicMock(autospec=BackgroundTasks), + ) as background_tasks: + background_tasks.side_effect = lambda x, y: None + session = await anext(get_session()) + assert isinstance(session, AsyncSession) + + +def test_get_settings(mocker: MockerFixture) -> None: + """ + Test ``get_settings``. + """ + mocker.patch("datajunction_server.utils.load_dotenv") + Settings = mocker.patch( + "datajunction_server.utils.Settings", + ) + + # should be already cached, since it's called by the Celery app + get_settings() + Settings.assert_not_called() + + +def test_get_issue_url() -> None: + """ + Test ``get_issue_url``. + """ + assert get_issue_url() == URL( + "https://github.com/DataJunction/dj/issues/new", + ) + assert get_issue_url( + baseurl=URL("https://example.org/"), + title="Title with spaces", + body="This is the body", + labels=["help", "troubleshoot"], + ) == URL( + "https://example.org/?title=Title+with+spaces&" + "body=This+is+the+body&labels=help,troubleshoot", + ) + + +def test_get_engine( + mocker: MockerFixture, + settings: Settings, + postgres_container: PostgresContainer, +) -> None: + """ + Test ``get_engine``. + """ + connection_url = postgres_container.get_connection_url() + settings.index = connection_url + mocker.patch("datajunction_server.utils.get_settings", return_value=settings) + engine = get_engine() + assert engine.pool.size() == settings.db_pool_size + assert engine.pool.timeout() == settings.db_pool_timeout + assert engine.pool.overflow() == -settings.db_max_overflow + + +def test_get_query_service_client(mocker: MockerFixture, settings: Settings) -> None: + """ + Test ``get_query_service_client``. + """ + settings.query_service = "http://query_service:8001" + mocker.patch("datajunction_server.utils.get_settings", return_value=settings) + query_service_client = get_query_service_client() + assert query_service_client.uri == "http://query_service:8001" # type: ignore + + +def test_version_parse() -> None: + """ + Test version parsing + """ + ver = Version.parse("v1.0") + assert ver.major == 1 + assert ver.minor == 0 + assert str(ver.next_major_version()) == "v2.0" + assert str(ver.next_minor_version()) == "v1.1" + assert str(ver.next_minor_version().next_minor_version()) == "v1.2" + + ver = Version.parse("v21.12") + assert ver.major == 21 + assert ver.minor == 12 + assert str(ver.next_major_version()) == "v22.0" + assert str(ver.next_minor_version()) == "v21.13" + assert str(ver.next_minor_version().next_minor_version()) == "v21.14" + assert str(ver.next_major_version().next_minor_version()) == "v22.1" + + with pytest.raises(DJException) as excinfo: + Version.parse("0") + assert str(excinfo.value) == "Unparseable version 0!" + + +@pytest.mark.asyncio +async def test_get_and_update_current_user(session: AsyncSession): + """ + Test upserting the current user + """ + example_user = User( + id=1, + username="userfoo", + password="passwordfoo", + name="djuser", + email="userfoo@datajunction.io", + oauth_provider=OAuthProvider.BASIC, + ) + + # Confirm that the current user is returned after upserting + current_user = await get_and_update_current_user( + session=session, + current_user=example_user, + ) + assert current_user.id == example_user.id + assert current_user.username == example_user.username + + # Confirm that the user was upserted + result = await session.execute(select(User).where(User.username == "userfoo")) + found_user = result.unique().scalar_one_or_none() + assert found_user.id == 1 + assert found_user.username == "userfoo" + assert ( + found_user.password is None + ) # If the user is added via upsert, auth is externally managed + assert found_user.name == "djuser" + assert found_user.email == "userfoo@datajunction.io" + assert found_user.oauth_provider == "basic" diff --git a/datajunction-ui/.babel-plugin-macrosrc.js b/datajunction-ui/.babel-plugin-macrosrc.js new file mode 100644 index 000000000..7ea38d073 --- /dev/null +++ b/datajunction-ui/.babel-plugin-macrosrc.js @@ -0,0 +1,5 @@ +module.exports = { + styledComponents: { + displayName: process.env.NODE_ENV !== 'production', + }, +}; diff --git a/datajunction-ui/.env b/datajunction-ui/.env new file mode 100644 index 000000000..cb841a92a --- /dev/null +++ b/datajunction-ui/.env @@ -0,0 +1,3 @@ +REACT_APP_DJ_URL=http://localhost:8000 +REACT_USE_SSE=true +REACT_ENABLE_GOOGLE_OAUTH=true \ No newline at end of file diff --git a/datajunction-ui/.eslintrc.js b/datajunction-ui/.eslintrc.js new file mode 100644 index 000000000..4402f5bab --- /dev/null +++ b/datajunction-ui/.eslintrc.js @@ -0,0 +1,20 @@ +const fs = require('fs'); +const path = require('path'); + +const prettierOptions = JSON.parse( + fs.readFileSync(path.resolve(__dirname, '.prettierrc'), 'utf8'), +); + +module.exports = { + extends: ['react-app', 'prettier'], + plugins: ['prettier'], + rules: { + 'prettier/prettier': ['error', prettierOptions], + }, + overrides: [ + { + files: ['**/*.ts?(x)'], + rules: { 'prettier/prettier': ['warn', prettierOptions] }, + }, + ], +}; diff --git a/datajunction-ui/.gitattributes b/datajunction-ui/.gitattributes new file mode 100644 index 000000000..37bdee27c --- /dev/null +++ b/datajunction-ui/.gitattributes @@ -0,0 +1,201 @@ +# From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes + +## GITATTRIBUTES FOR WEB PROJECTS +# +# These settings are for any web project. +# +# Details per file setting: +# text These files should be normalized (i.e. convert CRLF to LF). +# binary These files are binary and should be left untouched. +# +# Note that binary is a macro for -text -diff. +###################################################################### + +# Auto detect +## Handle line endings automatically for files detected as +## text and leave all files detected as binary untouched. +## This will handle all files NOT defined below. +* text=auto + +# Source code +*.bash text eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf +*.coffee text +*.css text +*.htm text diff=html +*.html text diff=html +*.inc text +*.ini text +*.js text +*.json text +*.jsx text +*.less text +*.ls text +*.map text -diff +*.od text +*.onlydata text +*.php text diff=php +*.pl text +*.ps1 text eol=crlf +*.py text diff=python +*.rb text diff=ruby +*.sass text +*.scm text +*.scss text diff=css +*.sh text eol=lf +*.sql text +*.styl text +*.tag text +*.ts text +*.tsx text +*.xml text +*.xhtml text diff=html + +# Docker +Dockerfile text + +# Documentation +*.ipynb text +*.markdown text +*.md text +*.mdwn text +*.mdown text +*.mkd text +*.mkdn text +*.mdtxt text +*.mdtext text +*.txt text +AUTHORS text +CHANGELOG text +CHANGES text +CONTRIBUTING text +COPYING text +copyright text +*COPYRIGHT* text +INSTALL text +license text +LICENSE text +NEWS text +readme text +*README* text +TODO text + +# Templates +*.dot text +*.ejs text +*.haml text +*.handlebars text +*.hbs text +*.hbt text +*.jade text +*.latte text +*.mustache text +*.njk text +*.phtml text +*.tmpl text +*.tpl text +*.twig text +*.vue text + +# Configs +*.cnf text +*.conf text +*.config text +.editorconfig text +.env text +.gitattributes text +.gitconfig text +.htaccess text +*.lock text -diff +package-lock.json text -diff +*.toml text +*.yaml text +*.yml text +browserslist text +Makefile text +makefile text + +# Heroku +Procfile text + +# Graphics +*.ai binary +*.bmp binary +*.eps binary +*.gif binary +*.gifv binary +*.ico binary +*.jng binary +*.jp2 binary +*.jpg binary +*.jpeg binary +*.jpx binary +*.jxr binary +*.pdf binary +*.png binary +*.psb binary +*.psd binary +# SVG treated as an asset (binary) by default. +*.svg text +# If you want to treat it as binary, +# use the following line instead. +# *.svg binary +*.svgz binary +*.tif binary +*.tiff binary +*.wbmp binary +*.webp binary + +# Audio +*.kar binary +*.m4a binary +*.mid binary +*.midi binary +*.mp3 binary +*.ogg binary +*.ra binary + +# Video +*.3gpp binary +*.3gp binary +*.as binary +*.asf binary +*.asx binary +*.fla binary +*.flv binary +*.m4v binary +*.mng binary +*.mov binary +*.mp4 binary +*.mpeg binary +*.mpg binary +*.ogv binary +*.swc binary +*.swf binary +*.webm binary + +# Archives +*.7z binary +*.gz binary +*.jar binary +*.rar binary +*.tar binary +*.zip binary + +# Fonts +*.ttf binary +*.eot binary +*.otf binary +*.woff binary +*.woff2 binary + +# Executables +*.exe binary +*.pyc binary + +# RC files (like .babelrc or .eslintrc) +*.*rc text + +# Ignore files (like .npmignore or .gitignore) +*.*ignore text \ No newline at end of file diff --git a/datajunction-ui/.gitignore b/datajunction-ui/.gitignore new file mode 100644 index 000000000..2c287157f --- /dev/null +++ b/datajunction-ui/.gitignore @@ -0,0 +1,35 @@ +# Don't check auto-generated stuff into git +coverage +build +node_modules +dist +stats.json +.pnp +.pnp.js +dist + +# misc +.DS_Store +npm-debug.log* + +# yarn +yarn-debug.log* +yarn-error.log* +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# env +.env.development.local +.env.test.local +.env.production.local + +# boilerplate internals +generated-cra-app +.cra-template-rb +template +.eslintcache diff --git a/datajunction-ui/.husky/pre-commit b/datajunction-ui/.husky/pre-commit new file mode 100755 index 000000000..028978219 --- /dev/null +++ b/datajunction-ui/.husky/pre-commit @@ -0,0 +1,6 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn checkTs +yarn lint-staged + diff --git a/datajunction-ui/.npmrc b/datajunction-ui/.npmrc new file mode 100644 index 000000000..aacab550c --- /dev/null +++ b/datajunction-ui/.npmrc @@ -0,0 +1,3 @@ +auto-install-peers=true +dedupe-peer-dependents=true +save-exact = true diff --git a/datajunction-ui/.nvmrc b/datajunction-ui/.nvmrc new file mode 100644 index 000000000..518633e16 --- /dev/null +++ b/datajunction-ui/.nvmrc @@ -0,0 +1 @@ +lts/fermium diff --git a/datajunction-ui/.prettierignore b/datajunction-ui/.prettierignore new file mode 100644 index 000000000..e88431261 --- /dev/null +++ b/datajunction-ui/.prettierignore @@ -0,0 +1,6 @@ +build/ +node_modules/ +package-lock.json +yarn.lock +dist/ +coverage/ diff --git a/datajunction-ui/.prettierrc b/datajunction-ui/.prettierrc new file mode 100644 index 000000000..b88daf722 --- /dev/null +++ b/datajunction-ui/.prettierrc @@ -0,0 +1,9 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "arrowParens": "avoid" +} diff --git a/datajunction-ui/.stylelintrc b/datajunction-ui/.stylelintrc new file mode 100644 index 000000000..9e72e47c6 --- /dev/null +++ b/datajunction-ui/.stylelintrc @@ -0,0 +1,7 @@ +{ + "processors": ["stylelint-processor-styled-components"], + "extends": [ + "stylelint-config-recommended", + "stylelint-config-styled-components" + ] +} diff --git a/datajunction-ui/LICENSE b/datajunction-ui/LICENSE new file mode 100644 index 000000000..874d3043d --- /dev/null +++ b/datajunction-ui/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2023 DJ + +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. + diff --git a/datajunction-ui/Makefile b/datajunction-ui/Makefile new file mode 100644 index 000000000..7139e30f5 --- /dev/null +++ b/datajunction-ui/Makefile @@ -0,0 +1,9 @@ +dev-release: + yarn version --prerelease --preid dev --no-git-tag-version + npm publish + +test: + yarn test --coverage --watchAll --runInBand + +lint: + npm run lint \ No newline at end of file diff --git a/datajunction-ui/README.md b/datajunction-ui/README.md new file mode 100644 index 000000000..c6bbd93a0 --- /dev/null +++ b/datajunction-ui/README.md @@ -0,0 +1,10 @@ +# DataJunction UI + +A view-only UI for the DataJunction metrics platform. + +To run: + +```bash +yarn install +yarn start +``` diff --git a/datajunction-ui/dj-logo.svg b/datajunction-ui/dj-logo.svg new file mode 100644 index 000000000..bac4c69e8 --- /dev/null +++ b/datajunction-ui/dj-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/datajunction-ui/internals/testing/loadable.mock.tsx b/datajunction-ui/internals/testing/loadable.mock.tsx new file mode 100644 index 000000000..ed19a3a59 --- /dev/null +++ b/datajunction-ui/internals/testing/loadable.mock.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; + +export function ExportedFunc() { + return
My lazy-loaded component
; +} +export default ExportedFunc; diff --git a/datajunction-ui/package.json b/datajunction-ui/package.json new file mode 100644 index 000000000..65fff8530 --- /dev/null +++ b/datajunction-ui/package.json @@ -0,0 +1,200 @@ +{ + "name": "datajunction-ui", + "version": "0.0.1a87", + "description": "DataJunction Metrics Platform UI", + "module": "src/index.tsx", + "repository": { + "type": "git", + "url": "git+https://github.com/DataJunction/dj-ui.git" + }, + "keywords": [ + "datajunction", + "metrics", + "metrics-platform", + "semantic-layer" + ], + "author": "DataJunction Authors", + "license": "MIT", + "bugs": { + "url": "https://github.com/DataJunction/dj/issues" + }, + "homepage": "https://github.com/DataJunction/dj#readme", + "dependencies": { + "@babel/core": "^7.18.2", + "@babel/preset-env": "^7.18.2", + "@babel/preset-react": "7.18.6", + "@codemirror/lang-sql": "^6.4.0", + "@reduxjs/toolkit": "1.8.5", + "@testing-library/jest-dom": "6.1.2", + "@testing-library/react": "14.0.0", + "@types/fontfaceobserver": "^2.1.0", + "@types/jest": "29.5.14", + "@types/node": "^14.18.27", + "@types/react": "^18.0.20", + "@types/react-dom": "^18.0.6", + "@types/react-redux": "^7.1.24", + "@types/react-select": "5.0.1", + "@types/react-test-renderer": "^18.0.0", + "@types/rimraf": "^3.0.2", + "@types/shelljs": "^0.8.11", + "@types/testing-library__jest-dom": "^5.14.5", + "@types/webpack": "^5.28.0", + "@types/webpack-env": "^1.18.0", + "@uiw/codemirror-extensions-basic-setup": "4.21.12", + "@uiw/codemirror-extensions-langs": "4.21.12", + "@uiw/react-codemirror": "4.21.12", + "babel-loader": "9.1.2", + "chalk": "4.1.2", + "codemirror": "^6.0.0", + "cronstrue": "2.27.0", + "cross-env": "7.0.3", + "css-loader": "6.8.1", + "dagre": "^0.8.5", + "datajunction": "0.0.1-rc.0", + "file-loader": "6.2.0", + "fontfaceobserver": "2.3.0", + "formik": "2.4.3", + "fuse.js": "6.6.2", + "husky": "8.0.1", + "i18next": "21.9.2", + "i18next-browser-languagedetector": "6.1.5", + "i18next-scanner": "4.0.0", + "inquirer": "7.3.3", + "inquirer-directory": "2.2.0", + "js-cookie": "3.0.5", + "lint-staged": "13.0.3", + "node-plop": "0.26.3", + "plop": "2.7.6", + "prettier": "2.7.1", + "react": "18.2.0", + "react-app-polyfill": "3.0.0", + "react-cookie": "4.1.1", + "react-diff-view": "3.2.1", + "react-dom": "18.2.0", + "react-helmet-async": "1.3.0", + "react-i18next": "11.18.6", + "react-is": "18.2.0", + "react-markdown": "9.0.1", + "react-querybuilder": "6.5.1", + "react-redux": "7.2.8", + "react-router-dom": "6.3.0", + "react-scripts": "5.0.1", + "react-select": "5.7.3", + "react-syntax-highlighter": "^15.5.0", + "react-test-renderer": "18.2.0", + "reactflow": "^11.7.0", + "redux-injectors": "2.1.0", + "redux-saga": "1.2.1", + "rimraf": "3.0.2", + "sanitize.css": "13.0.0", + "sass": "1.66.1", + "sass-loader": "13.3.2", + "serve": "14.0.1", + "shelljs": "0.8.5", + "sql-formatter": "^12.2.0", + "style-loader": "3.3.3", + "stylelint": "14.12.0", + "stylelint-config-recommended": "9.0.0", + "ts-loader": "9.4.2", + "ts-node": "10.9.1", + "typescript": "4.6.4", + "unidiff": "1.0.4", + "web-vitals": "2.1.4", + "webpack": "5.81.0", + "webpack-cli": "5.0.2", + "webpack-dev-server": "4.13.3", + "yup": "1.3.2" + }, + "scripts": { + "webpack-start": "webpack-dev-server --open", + "webpack-build": "webpack", + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "start:prod": "yarn run build && serve -s build", + "test:generators": "ts-node ./internals/testing/generators/test-generators.ts", + "checkTs": "tsc --noEmit", + "eslint": "eslint --ext js,ts,tsx", + "lint": "yarn run eslint src", + "lint:fix": "yarn run eslint --fix src", + "lint:css": "stylelint src/**/*.css", + "generate": "plop --plopfile internals/generators/plopfile.ts", + "cleanAndSetup": "ts-node ./internals/scripts/clean.ts", + "prettify": "prettier --write", + "extract-messages": "i18next-scanner --config=internals/extractMessages/i18next-scanner.config.js", + "prepublishOnly": "webpack --mode=production" + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "engines": { + "node": ">=14.x" + }, + "lint-staged": { + "*.{ts,tsx,js,jsx}": [ + "yarn run eslint --fix" + ], + "*.{md,json}": [ + "prettier --write" + ] + }, + "jest": { + "transformIgnorePatterns": [ + "!node_modules/" + ], + "collectCoverageFrom": [ + "src/**/*.{js,jsx,ts,tsx}", + "!src/**/*/*.d.ts", + "!src/**/*/Loadable.{js,jsx,ts,tsx}", + "!src/**/*/messages.ts", + "!src/**/*/types.ts", + "!src/index.tsx" + ], + "coverageThreshold": { + "global": { + "statements": 87, + "branches": 70, + "lines": 80, + "functions": 83 + } + }, + "moduleNameMapper": { + "unist-util-visit-parents/do-not-use-color": "/node_modules/unist-util-visit-parents/lib/color.js", + "^#minpath$": "/node_modules/vfile/lib/minpath.browser.js", + "^#minproc$": "/node_modules/vfile/lib/minproc.browser.js", + "^#minurl$": "/node_modules/vfile/lib/minurl.browser.js" + } + }, + "resolutions": { + "@codemirror/state": "6.2.0", + "@codemirror/view": "6.2.0", + "@lezer/common": "^1.0.0" + }, + "devDependencies": { + "@babel/plugin-proposal-class-properties": "7.18.6", + "@babel/plugin-proposal-private-property-in-object": "7.21.11", + "@testing-library/user-event": "14.4.3", + "eslint-config-prettier": "8.8.0", + "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-react-hooks": "4.6.0", + "html-webpack-plugin": "5.5.1", + "jest": "^29.5.0", + "jest-fetch-mock": "3.0.3", + "jest-watch-typeahead": "2.2.2", + "mini-css-extract-plugin": "2.7.6", + "resize-observer-polyfill": "1.5.1" + } +} diff --git a/datajunction-ui/public/favicon.ico b/datajunction-ui/public/favicon.ico new file mode 100644 index 000000000..0707abe6f Binary files /dev/null and b/datajunction-ui/public/favicon.ico differ diff --git a/datajunction-ui/public/index.html b/datajunction-ui/public/index.html new file mode 100644 index 000000000..d42b092e0 --- /dev/null +++ b/datajunction-ui/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + DataJunction App + + + + +
+ + + + + diff --git a/datajunction-ui/public/manifest.json b/datajunction-ui/public/manifest.json new file mode 100644 index 000000000..167002495 --- /dev/null +++ b/datajunction-ui/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "DataJunction UI", + "name": "DataJunction UI", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/datajunction-ui/public/robots.txt b/datajunction-ui/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/datajunction-ui/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/datajunction-ui/src/__tests__/reportWebVitals.test.jsx b/datajunction-ui/src/__tests__/reportWebVitals.test.jsx new file mode 100644 index 000000000..650061192 --- /dev/null +++ b/datajunction-ui/src/__tests__/reportWebVitals.test.jsx @@ -0,0 +1,44 @@ +import reportWebVitals from '../reportWebVitals'; + +// Mocking the web-vitals module +jest.mock('web-vitals', () => ({ + getCLS: jest.fn(), + getFID: jest.fn(), + getFCP: jest.fn(), + getLCP: jest.fn(), + getTTFB: jest.fn(), +})); + +describe('reportWebVitals', () => { + // Mock web-vitals functions + const mockGetCLS = jest.fn(); + const mockGetFID = jest.fn(); + const mockGetFCP = jest.fn(); + const mockGetLCP = jest.fn(); + const mockGetTTFB = jest.fn(); + + beforeAll(() => { + jest.doMock('web-vitals', () => ({ + getCLS: mockGetCLS, + getFID: mockGetFID, + getFCP: mockGetFCP, + getLCP: mockGetLCP, + getTTFB: mockGetTTFB, + })); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does not call the web vitals functions if onPerfEntry is not a function', async () => { + await reportWebVitals(undefined); + + const { getCLS, getFID, getFCP, getLCP, getTTFB } = require('web-vitals'); + expect(getCLS).not.toHaveBeenCalled(); + expect(getFID).not.toHaveBeenCalled(); + expect(getFCP).not.toHaveBeenCalled(); + expect(getLCP).not.toHaveBeenCalled(); + expect(getTTFB).not.toHaveBeenCalled(); + }); +}); diff --git a/datajunction-ui/src/app/__tests__/__snapshots__/index.test.tsx.snap b/datajunction-ui/src/app/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000..26612b60c --- /dev/null +++ b/datajunction-ui/src/app/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render and match the snapshot 1`] = ` + + + + + +`; diff --git a/datajunction-ui/src/app/__tests__/index.test.tsx b/datajunction-ui/src/app/__tests__/index.test.tsx new file mode 100644 index 000000000..7d3cd5b48 --- /dev/null +++ b/datajunction-ui/src/app/__tests__/index.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { createRenderer } from 'react-test-renderer/shallow'; + +import { App } from '../index'; + +const renderer = createRenderer(); + +describe('', () => { + it('should render and match the snapshot', () => { + renderer.render(); + const renderedOutput = renderer.getRenderOutput(); + expect(renderedOutput).toMatchSnapshot(); + }); +}); diff --git a/datajunction-ui/src/app/components/AddNodeDropdown.jsx b/datajunction-ui/src/app/components/AddNodeDropdown.jsx new file mode 100644 index 000000000..1e7d381d5 --- /dev/null +++ b/datajunction-ui/src/app/components/AddNodeDropdown.jsx @@ -0,0 +1,41 @@ +export default function AddNodeDropdown({ namespace }) { + return ( + + +
+ + + ); +} diff --git a/datajunction-ui/src/app/components/ListGroupItem.jsx b/datajunction-ui/src/app/components/ListGroupItem.jsx new file mode 100644 index 000000000..6f934827d --- /dev/null +++ b/datajunction-ui/src/app/components/ListGroupItem.jsx @@ -0,0 +1,25 @@ +import { Component } from 'react'; +import Markdown from 'react-markdown'; + +export default class ListGroupItem extends Component { + render() { + const { label, value } = this.props; + return ( +
+
+
+
{label}
+

+ {value} +

+
+
+
+ ); + } +} diff --git a/datajunction-ui/src/app/components/NamespaceHeader.jsx b/datajunction-ui/src/app/components/NamespaceHeader.jsx new file mode 100644 index 000000000..4a48a3f5c --- /dev/null +++ b/datajunction-ui/src/app/components/NamespaceHeader.jsx @@ -0,0 +1,31 @@ +import { Component } from 'react'; +import HorizontalHierarchyIcon from '../icons/HorizontalHierarchyIcon'; + +export default class NamespaceHeader extends Component { + render() { + const { namespace } = this.props; + const namespaceParts = namespace.split('.'); + const namespaceList = namespaceParts.map((piece, index) => { + return ( +
  • + + {piece} + +
  • + ); + }); + return ( +
      +
    1. + + + +
    2. + {namespaceList} +
    + ); + } +} diff --git a/datajunction-ui/src/app/components/NodeListActions.jsx b/datajunction-ui/src/app/components/NodeListActions.jsx new file mode 100644 index 000000000..4e2dbf25b --- /dev/null +++ b/datajunction-ui/src/app/components/NodeListActions.jsx @@ -0,0 +1,69 @@ +import DJClientContext from '../providers/djclient'; +import * as React from 'react'; +import DeleteIcon from '../icons/DeleteIcon'; +import EditIcon from '../icons/EditIcon'; +import { Form, Formik } from 'formik'; +import { useContext } from 'react'; +import { displayMessageAfterSubmit } from '../../utils/form'; + +export default function NodeListActions({ nodeName }) { + const [editButton, setEditButton] = React.useState(); + const [deleteButton, setDeleteButton] = React.useState(); + + const djClient = useContext(DJClientContext).DataJunctionAPI; + const deleteNode = async (values, { setStatus }) => { + if ( + !window.confirm('Deleting node ' + values.nodeName + '. Are you sure?') + ) { + return; + } + const { status, json } = await djClient.deactivate(values.nodeName); + if (status === 200 || status === 201 || status === 204) { + setStatus({ + success: <>Successfully deleted node {values.nodeName}, + }); + setEditButton(''); // hide the Edit button + setDeleteButton(''); // hide the Delete button + } else { + setStatus({ + failure: `${json.message}`, + }); + } + }; + + const initialValues = { + nodeName: nodeName, + }; + + return ( +
    + + {editButton} + + + {function Render({ status, setFieldValue }) { + return ( +
    + {displayMessageAfterSubmit(status)} + { + <> + + + } + + ); + }} +
    +
    + ); +} diff --git a/datajunction-ui/src/app/components/NodeMaterializationDelete.jsx b/datajunction-ui/src/app/components/NodeMaterializationDelete.jsx new file mode 100644 index 000000000..b18ce64aa --- /dev/null +++ b/datajunction-ui/src/app/components/NodeMaterializationDelete.jsx @@ -0,0 +1,80 @@ +import DJClientContext from '../providers/djclient'; +import * as React from 'react'; +import DeleteIcon from '../icons/DeleteIcon'; +import { Form, Formik } from 'formik'; +import { useContext } from 'react'; +import { displayMessageAfterSubmit } from '../../utils/form'; + +export default function NodeMaterializationDelete({ + nodeName, + materializationName, +}) { + const [deleteButton, setDeleteButton] = React.useState(); + + const djClient = useContext(DJClientContext).DataJunctionAPI; + const deleteNode = async (values, { setStatus }) => { + if ( + !window.confirm( + 'Deleting materialization job ' + + values.materializationName + + '. Are you sure?', + ) + ) { + return; + } + const { status, json } = await djClient.deleteMaterialization( + values.nodeName, + values.materializationName, + ); + if (status === 200 || status === 201 || status === 204) { + window.location.reload(); + setStatus({ + success: ( + <> + Successfully deleted materialization job:{' '} + {values.materializationName} + + ), + }); + setDeleteButton(''); // hide the Delete button + } else { + setStatus({ + failure: `${json.message}`, + }); + } + }; + + const initialValues = { + nodeName: nodeName, + materializationName: materializationName, + }; + + return ( +
    + + {function Render({ status, setFieldValue }) { + return ( +
    + {displayMessageAfterSubmit(status)} + { + <> + + + } + + ); + }} +
    +
    + ); +} diff --git a/datajunction-ui/src/app/components/QueryInfo.jsx b/datajunction-ui/src/app/components/QueryInfo.jsx new file mode 100644 index 000000000..a1e062e16 --- /dev/null +++ b/datajunction-ui/src/app/components/QueryInfo.jsx @@ -0,0 +1,172 @@ +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { solarizedDark } from 'react-syntax-highlighter/src/styles/hljs'; +import React from 'react'; + +export default function QueryInfo({ + id, + state, + engine_name, + engine_version, + errors, + links, + output_table, + scheduled, + started, + finished, + numRows, + isList = false, +}) { + return isList === false ? ( +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Query IDEngineStateScheduledStartedErrorsLinksOutput TableNumber of Rows
    + {id} + + + {engine_name} + {' - '} + {engine_version} + + {state}{scheduled}{started} + {errors?.length ? ( + errors.map((e, idx) => ( +

    + + {e} + +

    + )) + ) : ( + <> + )} +
    + {links?.length ? ( + links.map((link, idx) => ( +

    + + {link} + +

    + )) + ) : ( + <> + )} +
    {output_table}{numRows}
    + + ) : ( +
    +
      +
    • + {' '} + + {links?.length ? ( + + {id} + + ) : ( + id + )} + +
    • +
    • + + {state} +
    • +
    • + {' '} + + {engine_name} + {' - '} + {engine_version} + +
    • +
    • + {scheduled} +
    • +
    • + {started} +
    • +
    • + {finished} +
    • +
    • + {' '} + {errors?.length ? ( + errors.map(error => ( +
      + + {error} + +
      + )) + ) : ( + <> + )} +
    • +
    • + {' '} + {links?.length ? ( + links.map((link, idx) => ( +

      + + {link} + +

      + )) + ) : ( + <> + )} +
    • +
    • + {output_table} +
    • +
    • + {numRows} +
    • +
    +
    + ); +} diff --git a/datajunction-ui/src/app/components/Search.jsx b/datajunction-ui/src/app/components/Search.jsx new file mode 100644 index 000000000..4bec888ed --- /dev/null +++ b/datajunction-ui/src/app/components/Search.jsx @@ -0,0 +1,94 @@ +import { useState, useEffect, useContext } from 'react'; +import DJClientContext from '../providers/djclient'; +import Fuse from 'fuse.js'; + +import './search.css'; + +export default function Search() { + const [fuse, setFuse] = useState(); + const [searchValue, setSearchValue] = useState(''); + const [searchResults, setSearchResults] = useState([]); + + const djClient = useContext(DJClientContext).DataJunctionAPI; + + const truncate = str => { + if (str === null) { + return ''; + } + return str.length > 100 ? str.substring(0, 90) + '...' : str; + }; + + useEffect(() => { + const fetchNodes = async () => { + try { + const [data, tags] = await Promise.all([ + djClient.nodeDetails(), + djClient.listTags(), + ]); + const allEntities = data.concat( + (tags || []).map(tag => { + tag.type = 'tag'; + return tag; + }), + ); + const fuse = new Fuse(allEntities || [], { + keys: [ + 'name', // will be assigned a `weight` of 1 + { name: 'description', weight: 2 }, + { name: 'display_name', weight: 3 }, + { name: 'type', weight: 4 }, + { name: 'tag_type', weight: 5 }, + ], + }); + setFuse(fuse); + } catch (error) { + console.error('Error fetching nodes or tags:', error); + } + }; + fetchNodes(); + }, []); + + const handleChange = e => { + setSearchValue(e.target.value); + if (fuse) { + setSearchResults(fuse.search(e.target.value).map(result => result.item)); + } + }; + + return ( +
    +
    { + e.preventDefault(); + }} + > + +
    +
    + {searchResults.map(item => { + const itemUrl = + item.type !== 'tag' ? `/nodes/${item.name}` : `/tags/${item.name}`; + return ( + +
    + + {item.type} + + {item.display_name} ({item.name}){' '} + {item.description ? '- ' : ' '} + {truncate(item.description || '')} +
    +
    + ); + })} +
    +
    + ); +} diff --git a/datajunction-ui/src/app/components/Tab.jsx b/datajunction-ui/src/app/components/Tab.jsx new file mode 100644 index 000000000..491d27084 --- /dev/null +++ b/datajunction-ui/src/app/components/Tab.jsx @@ -0,0 +1,25 @@ +import { Component } from 'react'; + +export default class Tab extends Component { + render() { + const { id, onClick, selectedTab } = this.props; + return ( +
    +
    +
    + +
    +
    +
    + ); + } +} diff --git a/datajunction-ui/src/app/components/ToggleSwitch.jsx b/datajunction-ui/src/app/components/ToggleSwitch.jsx new file mode 100644 index 000000000..08c6674b2 --- /dev/null +++ b/datajunction-ui/src/app/components/ToggleSwitch.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const ToggleSwitch = ({ checked, onChange, toggleName }) => ( + <> + onChange(e.target.checked)} + /> + {' '} + {toggleName} + +); + +export default ToggleSwitch; diff --git a/datajunction-ui/src/app/components/__tests__/ListGroupItem.test.tsx b/datajunction-ui/src/app/components/__tests__/ListGroupItem.test.tsx new file mode 100644 index 000000000..372af7ce5 --- /dev/null +++ b/datajunction-ui/src/app/components/__tests__/ListGroupItem.test.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { createRenderer } from 'react-test-renderer/shallow'; + +import ListGroupItem from '../ListGroupItem'; + +const renderer = createRenderer(); + +describe('', () => { + it('should render and match the snapshot', () => { + renderer.render( + Something} />, + ); + const renderedOutput = renderer.getRenderOutput(); + expect(renderedOutput).toMatchSnapshot(); + }); +}); diff --git a/datajunction-ui/src/app/components/__tests__/NamespaceHeader.test.jsx b/datajunction-ui/src/app/components/__tests__/NamespaceHeader.test.jsx new file mode 100644 index 000000000..bb2f7f862 --- /dev/null +++ b/datajunction-ui/src/app/components/__tests__/NamespaceHeader.test.jsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { createRenderer } from 'react-test-renderer/shallow'; + +import NamespaceHeader from '../NamespaceHeader'; + +const renderer = createRenderer(); + +describe('', () => { + it('should render and match the snapshot', () => { + renderer.render(); + const renderedOutput = renderer.getRenderOutput(); + expect(renderedOutput).toMatchSnapshot(); + }); +}); diff --git a/datajunction-ui/src/app/components/__tests__/NodeListActions.test.jsx b/datajunction-ui/src/app/components/__tests__/NodeListActions.test.jsx new file mode 100644 index 000000000..bb1143e0b --- /dev/null +++ b/datajunction-ui/src/app/components/__tests__/NodeListActions.test.jsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import fetchMock from 'jest-fetch-mock'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../setupTests'; +import DJClientContext from '../../providers/djclient'; +import NodeListActions from '../NodeListActions'; + +describe('', () => { + beforeEach(() => { + fetchMock.resetMocks(); + jest.clearAllMocks(); + window.scrollTo = jest.fn(); + }); + + const renderElement = djClient => { + return render( + + + , + ); + }; + + const initializeMockDJClient = () => { + return { + DataJunctionAPI: { + deactivate: jest.fn(), + }, + }; + }; + + it('deletes a node when clicked', async () => { + global.confirm = () => true; + const mockDjClient = initializeMockDJClient(); + mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({ + status: 204, + json: { name: 'source.warehouse.schema.some_table' }, + }); + + renderElement(mockDjClient); + + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalled(); + expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalledWith( + 'default.hard_hat', + ); + }); + expect( + screen.getByText('Successfully deleted node default.hard_hat'), + ).toBeInTheDocument(); + }, 60000); + + it('skips a node deletion during confirm', async () => { + global.confirm = () => false; + const mockDjClient = initializeMockDJClient(); + mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({ + status: 204, + json: { name: 'source.warehouse.schema.some_table' }, + }); + + renderElement(mockDjClient); + + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.deactivate).not.toBeCalled(); + }); + }, 60000); + + it('fail deleting a node when clicked', async () => { + global.confirm = () => true; + const mockDjClient = initializeMockDJClient(); + mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({ + status: 777, + json: { message: 'source.warehouse.schema.some_table' }, + }); + + renderElement(mockDjClient); + + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalled(); + expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalledWith( + 'default.hard_hat', + ); + }); + expect( + screen.getByText('source.warehouse.schema.some_table'), + ).toBeInTheDocument(); + }, 60000); +}); diff --git a/datajunction-ui/src/app/components/__tests__/QueryInfo.test.jsx b/datajunction-ui/src/app/components/__tests__/QueryInfo.test.jsx new file mode 100644 index 000000000..ac5ad368c --- /dev/null +++ b/datajunction-ui/src/app/components/__tests__/QueryInfo.test.jsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import QueryInfo from '../QueryInfo'; + +describe('', () => { + const defaultProps = { + id: '123', + state: 'Running', + engine_name: 'Spark SQL', + engine_version: '1.0', + errors: ['Error 1', 'Error 2'], + links: ['http://example.com', 'http://example2.com'], + output_table: 'table1', + scheduled: '2023-09-06', + started: '2023-09-07', + numRows: 1000, + }; + + it('renders without crashing', () => { + render(); + }); + + it('displays correct query information', () => { + render(); + + expect(screen.getByText(defaultProps.id)).toBeInTheDocument(); + expect( + screen.getByText( + `${defaultProps.engine_name} - ${defaultProps.engine_version}`, + ), + ).toBeInTheDocument(); + expect(screen.getByText(defaultProps.state)).toBeInTheDocument(); + expect(screen.getByText(defaultProps.scheduled)).toBeInTheDocument(); + expect(screen.getByText(defaultProps.started)).toBeInTheDocument(); + expect(screen.getByText(defaultProps.output_table)).toBeInTheDocument(); + expect(screen.getByText(String(defaultProps.numRows))).toBeInTheDocument(); + defaultProps.errors.forEach(error => { + expect(screen.getByText(error)).toBeInTheDocument(); + }); + defaultProps.links.forEach(link => { + expect(screen.getByText(link)).toHaveAttribute('href', link); + }); + }); + + it('does not render errors and links when they are not provided', () => { + render(); + + defaultProps.errors.forEach(error => { + expect(screen.queryByText(error)).not.toBeInTheDocument(); + }); + defaultProps.links.forEach(link => { + expect(screen.queryByText(link)).not.toBeInTheDocument(); + }); + }); +}); diff --git a/datajunction-ui/src/app/components/__tests__/Search.test.jsx b/datajunction-ui/src/app/components/__tests__/Search.test.jsx new file mode 100644 index 000000000..7b43bfd62 --- /dev/null +++ b/datajunction-ui/src/app/components/__tests__/Search.test.jsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import Search from '../Search'; +import DJClientContext from '../../providers/djclient'; +import { Root } from '../../pages/Root'; +import { HelmetProvider } from 'react-helmet-async'; + +describe('', () => { + const mockDjClient = { + logout: jest.fn(), + nodeDetails: async () => [ + { + name: 'default.repair_orders', + display_name: 'Default: Repair Orders', + description: 'Repair orders', + version: 'v1.0', + type: 'source', + status: 'valid', + mode: 'published', + updated_at: '2023-08-21T16:48:52.880498+00:00', + }, + { + name: 'default.repair_order_details', + display_name: 'Default: Repair Order Details', + description: 'Details on repair orders', + version: 'v1.0', + type: 'source', + status: 'valid', + mode: 'published', + updated_at: '2023-08-21T16:48:52.981201+00:00', + }, + ], + listTags: async () => [ + { + description: 'something', + display_name: 'Report A', + tag_metadata: {}, + name: 'report.a', + tag_type: 'report', + }, + { + description: 'report B', + display_name: 'Report B', + tag_metadata: {}, + name: 'report.b', + tag_type: 'report', + }, + ], + }; + + it('displays search results correctly', () => { + render( + + + + + , + ); + const searchInput = screen.queryByPlaceholderText('Search'); + fireEvent.change(searchInput, { target: { value: 'Repair' } }); + expect(searchInput.value).toBe('Repair'); + }); +}); diff --git a/datajunction-ui/src/app/components/__tests__/Tab.test.jsx b/datajunction-ui/src/app/components/__tests__/Tab.test.jsx new file mode 100644 index 000000000..72fdd19c4 --- /dev/null +++ b/datajunction-ui/src/app/components/__tests__/Tab.test.jsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { render, fireEvent } from '@testing-library/react'; + +import Tab from '../Tab'; + +describe('', () => { + it('renders without crashing', () => { + render(); + }); + + it('has the active class when selectedTab matches id', () => { + const { container } = render(); + expect(container.querySelector('.col')).toHaveClass('active'); + }); + + it('does not have the active class when selectedTab does not match id', () => { + const { container } = render(); + expect(container.querySelector('.col')).not.toHaveClass('active'); + }); + + it('calls onClick when the button is clicked', () => { + const onClickMock = jest.fn(); + const { getByRole } = render(); + fireEvent.click(getByRole('button')); + expect(onClickMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/datajunction-ui/src/app/components/__tests__/ToggleSwitch.test.jsx b/datajunction-ui/src/app/components/__tests__/ToggleSwitch.test.jsx new file mode 100644 index 000000000..a6919b61a --- /dev/null +++ b/datajunction-ui/src/app/components/__tests__/ToggleSwitch.test.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import ToggleSwitch from '../ToggleSwitch'; + +describe('', () => { + const defaultProps = { + checked: false, + onChange: jest.fn(), + toggleName: 'Toggle Switch', + }; + + it('renders without crashing', () => { + render(); + }); + + it('displays the correct toggle name', () => { + render(); + expect(screen.getByText(defaultProps.toggleName)).toBeInTheDocument(); + }); + + it('reflects the checked state correctly', () => { + render(); + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toBeChecked(); + }); + + it('calls onChange with the correct value when toggled', () => { + render(); + const checkbox = screen.getByRole('checkbox'); + + fireEvent.click(checkbox); + expect(defaultProps.onChange).toHaveBeenCalledWith(true); + + fireEvent.click(checkbox); + expect(checkbox).not.toBeChecked(); + }); + + it('is unchecked by default if no checked prop is provided', () => { + render(); + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).not.toBeChecked(); + }); +}); diff --git a/datajunction-ui/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap b/datajunction-ui/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap new file mode 100644 index 000000000..0c503bd3a --- /dev/null +++ b/datajunction-ui/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render and match the snapshot 1`] = ` +
    +
    +
    +
    + Name +
    +

    + + + Something + + +

    +
    +
    +
    +`; diff --git a/datajunction-ui/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap b/datajunction-ui/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap new file mode 100644 index 000000000..5a6c781b9 --- /dev/null +++ b/datajunction-ui/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render and match the snapshot 1`] = ` +
      +
    1. + + + +
    2. +
    3. + + shared + +
    4. +
    5. + + dimensions + +
    6. +
    7. + + accounts + +
    8. +
    +`; diff --git a/datajunction-ui/src/app/components/djgraph/Collapse.jsx b/datajunction-ui/src/app/components/djgraph/Collapse.jsx new file mode 100644 index 000000000..6f4d3024a --- /dev/null +++ b/datajunction-ui/src/app/components/djgraph/Collapse.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { DJNodeDimensions } from './DJNodeDimensions'; +import { DJNodeColumns } from './DJNodeColumns'; + +export default function Collapse({ collapsed, text, data }) { + const [isCollapsed, setIsCollapsed] = React.useState(collapsed); + + const limit = 5; + return ( + <> +
    + {data.type === 'metric' ? ( + + ) : ( + '' + )} +
    + {data.type !== 'metric' + ? isCollapsed + ? DJNodeColumns({ data: data, limit: limit }) + : DJNodeColumns({ data: data, limit: 100 }) + : DJNodeDimensions(data)} +
    + {data.type !== 'metric' && data.column_names.length > limit ? ( + + ) : ( + '' + )} +
    + + ); +} diff --git a/datajunction-ui/src/app/components/djgraph/DJNode.jsx b/datajunction-ui/src/app/components/djgraph/DJNode.jsx new file mode 100644 index 000000000..82c8fcc80 --- /dev/null +++ b/datajunction-ui/src/app/components/djgraph/DJNode.jsx @@ -0,0 +1,89 @@ +import { memo } from 'react'; +import { Handle, Position } from 'reactflow'; +import Collapse from './Collapse'; + +function capitalize(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +export function DJNode({ id, data }) { + const handleWrapperStyle = { + display: 'flex', + position: 'absolute', + height: '100%', + flexDirection: 'column', + top: '50%', + justifyContent: 'space-between', + }; + const handleWrapperStyleRight = { ...handleWrapperStyle, ...{ right: 0 } }; + + const handleStyle = { + width: '12px', + height: '12px', + borderRadius: '12px', + background: 'transparent', + border: '4px solid transparent', + cursor: 'pointer', + position: 'absolute', + top: '0px', + left: 0, + }; + const handleStyleLeft = percentage => { + return { + ...handleStyle, + ...{ + transform: 'translate(-' + percentage + '%, -50%)', + }, + }; + }; + const highlightNodeClass = + data.is_current === true ? ' dj-node_highlight' : ''; + return ( + <> +
    +
    + +
    +
    +
    + {data.name + .split('.') + .slice(0, data.name.split('.').length - 1) + .join(' \u25B6 ')} +
    +
    + +
    + +
    +
    + + ); +} + +export default memo(DJNode); diff --git a/datajunction-ui/src/app/components/djgraph/DJNodeColumns.jsx b/datajunction-ui/src/app/components/djgraph/DJNodeColumns.jsx new file mode 100644 index 000000000..8f4f7180c --- /dev/null +++ b/datajunction-ui/src/app/components/djgraph/DJNodeColumns.jsx @@ -0,0 +1,75 @@ +import { Handle } from 'reactflow'; +import React from 'react'; + +export function DJNodeColumns({ data, limit }) { + const handleWrapperStyle = { + display: 'flex', + position: 'absolute', + height: '100%', + flexDirection: 'column', + top: '50%', + justifyContent: 'space-between', + }; + const handleWrapperStyleRight = { ...handleWrapperStyle, ...{ right: 0 } }; + + const handleStyle = { + width: '12px', + height: '12px', + borderRadius: '12px', + background: 'transparent', + border: '4px solid transparent', + cursor: 'pointer', + position: 'absolute', + top: '0px', + left: 0, + }; + const handleStyleLeft = percentage => { + return { + ...handleStyle, + ...{ + transform: 'translate(-' + percentage + '%, -50%)', + }, + }; + }; + return data.column_names.slice(0, limit).map(col => ( +
    +
    + +
    +
    + {data.primary_key.includes(col.name) ? ( + {col.name} (PK) + ) : ( + <>{col.name} + )} + + {col.type} + +
    +
    + +
    +
    + )); +} diff --git a/datajunction-ui/src/app/components/djgraph/DJNodeDimensions.jsx b/datajunction-ui/src/app/components/djgraph/DJNodeDimensions.jsx new file mode 100644 index 000000000..e40140205 --- /dev/null +++ b/datajunction-ui/src/app/components/djgraph/DJNodeDimensions.jsx @@ -0,0 +1,75 @@ +import { useContext, useEffect, useState } from 'react'; +import DJClientContext from '../../providers/djclient'; + +export function DJNodeDimensions(data) { + const [dimensions, setDimensions] = useState([]); + const djClient = useContext(DJClientContext).DataJunctionAPI; + useEffect(() => { + if (data.type === 'metric') { + async function getDimensions() { + try { + const metricData = await djClient.metric(data.name); + setDimensions(metricData.dimensions); + } catch (err) { + console.log(err); + } + } + getDimensions(); + } + }, [data, djClient]); + const dimensionsToObject = dimensions => { + return dimensions.map(dim => { + const [attribute, ...nodeName] = dim.name.split('.').reverse(); + return { + dimension: nodeName.reverse().join('.'), + path: dim.path, + column: attribute, + }; + }); + }; + const groupedDimensions = dims => + dims.reduce((acc, current) => { + const dimKey = current.dimension + ' via ' + current.path.slice(-1); + acc[dimKey] = acc[dimKey] || { + dimension: current.dimension, + path: current.path.slice(-1), + columns: [], + }; + acc[dimKey].columns.push(current.column); + return acc; + }, {}); + const dimensionsRenderer = grouped => + Object.entries(grouped).map(([dimKey, dimValue]) => { + if (Array.isArray(dimValue.columns)) { + const attributes = dimValue.columns.map(col => { + return ( + + {col} + + ); + }); + return ( +
    +
    + {dimValue.dimension}{' '} +
    + {dimValue.path} +
    +
    +
    {attributes}
    +
    + ); + } + return <>; + }); + return ( + <> + {dimensions.length <= 0 + ? '' + : dimensionsRenderer(groupedDimensions(dimensionsToObject(dimensions)))} + + ); +} diff --git a/datajunction-ui/src/app/components/djgraph/LayoutFlow.jsx b/datajunction-ui/src/app/components/djgraph/LayoutFlow.jsx new file mode 100644 index 000000000..1c5ec23c8 --- /dev/null +++ b/datajunction-ui/src/app/components/djgraph/LayoutFlow.jsx @@ -0,0 +1,106 @@ +import React, { useCallback, useEffect, useMemo } from 'react'; +import ReactFlow, { + addEdge, + MiniMap, + Controls, + Background, + useNodesState, + useEdgesState, +} from 'reactflow'; + +import '../../../styles/dag.css'; +import 'reactflow/dist/style.css'; +import DJNode from '../../components/djgraph/DJNode'; +import dagre from 'dagre'; + +const getLayoutedElements = ( + nodes, + edges, + direction = 'LR', + nodeWidth = 600, +) => { + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + + const isHorizontal = direction === 'TB'; + dagreGraph.setGraph({ + rankdir: direction, + nodesep: 40, + ranksep: 10, + ranker: 'longest-path', + }); + const nodeHeightTracker = {}; + + nodes.forEach(node => { + const minColumnsLength = node.data.column_names.filter( + col => col.order > 0, + ).length; + nodeHeightTracker[node.id] = Math.min(minColumnsLength, 5) * 40 + 250; + dagreGraph.setNode(node.id, { + width: nodeWidth, + height: nodeHeightTracker[node.id], + }); + }); + + edges.forEach(edge => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + nodes.forEach(node => { + const nodeWithPosition = dagreGraph.node(node.id); + node.targetPosition = isHorizontal ? 'left' : 'top'; + node.sourcePosition = isHorizontal ? 'right' : 'bottom'; + node.position = { + x: nodeWithPosition.x - nodeWidth / 2, + y: nodeWithPosition.y - nodeHeightTracker[node.id] / 3, + }; + node.width = nodeWidth; + node.height = nodeHeightTracker[node.id]; + return node; + }); + + return { nodes: nodes, edges: edges }; +}; + +const LayoutFlow = (djNode, saveGraph) => { + const nodeTypes = useMemo(() => ({ DJNode: DJNode }), []); + + // These are used internally by ReactFlow (to update the nodes on the ReactFlow pane) + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + const minimapStyle = { + height: 100, + width: 150, + }; + + useEffect(() => { + saveGraph(getLayoutedElements, setNodes, setEdges).catch(console.error); + }, [djNode]); + + const onConnect = useCallback( + params => setEdges(eds => addEdge(params, eds)), + [setEdges], + ); + return ( +
    + + + + + +
    + ); +}; +export default LayoutFlow; diff --git a/datajunction-ui/src/app/components/djgraph/__tests__/Collapse.test.jsx b/datajunction-ui/src/app/components/djgraph/__tests__/Collapse.test.jsx new file mode 100644 index 000000000..77a3a7c44 --- /dev/null +++ b/datajunction-ui/src/app/components/djgraph/__tests__/Collapse.test.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import Collapse from '../Collapse'; +import { mocks } from '../../../../mocks/mockNodes'; +import { ReactFlowProvider } from 'reactflow'; + +jest.mock('../DJNodeDimensions', () => ({ + DJNodeDimensions: jest.fn(data =>
    DJNodeDimensions content
    ), +})); + +describe('', () => { + const defaultProps = { + collapsed: true, + text: 'Dimensions', + data: mocks.mockMetricNode, + }; + + it('renders without crashing', () => { + render(); + }); + + it('renders toggle for metric type', () => { + const { getByText } = render( + , + ); + const button = screen.getByText('▶ Show Dimensions'); + fireEvent.click(button); + expect(getByText('▼ Hide Dimensions')).toBeInTheDocument(); + }); + + it('toggles More/Less button text for non-metric type on click', () => { + defaultProps.text = 'Columns'; + const { getByText } = render( + + { + return `column-${idx}`; + }), + primary_key: [], + }} + /> + , + ); + const button = getByText('▶ More Columns'); + fireEvent.click(button); + expect(getByText('▼ Less Columns')).toBeInTheDocument(); + }); +}); diff --git a/datajunction-ui/src/app/components/djgraph/__tests__/DJNode.test.tsx b/datajunction-ui/src/app/components/djgraph/__tests__/DJNode.test.tsx new file mode 100644 index 000000000..478c51a2e --- /dev/null +++ b/datajunction-ui/src/app/components/djgraph/__tests__/DJNode.test.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { createRenderer } from 'react-test-renderer/shallow'; + +import { DJNode } from '../DJNode'; + +const renderer = createRenderer(); + +describe('', () => { + it('should render and match the snapshot', () => { + renderer.render( + , + ); + const renderedOutput = renderer.getRenderOutput(); + expect(renderedOutput).toMatchSnapshot(); + }); +}); diff --git a/datajunction-ui/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx b/datajunction-ui/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx new file mode 100644 index 000000000..757b99379 --- /dev/null +++ b/datajunction-ui/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { DJNodeColumns } from '../DJNodeColumns'; +import { ReactFlowProvider } from 'reactflow'; + +describe('', () => { + const defaultProps = { + data: { + name: 'TestName', + type: 'metric', + column_names: [ + { name: 'col1', type: 'int' }, + { name: 'col2', type: 'string' }, + { name: 'col3', type: 'float' }, + ], + primary_key: ['col1'], + }, + limit: 10, + }; + + const domTestingLib = require('@testing-library/dom'); + const { queryHelpers } = domTestingLib; + + const queryByAttribute = attribute => + queryHelpers.queryAllByAttribute.bind(null, attribute); + + function getByAttribute(container, id, attribute, ...rest) { + const result = queryByAttribute(attribute)(container, id, ...rest); + return result[0]; + } + + function DJNodeColumnsWithProvider(props) { + return ( + + + + ); + } + + it('renders without crashing', () => { + render(); + }); + + it('renders columns correctly', () => { + const { container } = render( + , + ); + + // renders column names + defaultProps.data.column_names.forEach(col => { + expect(screen.getByText(col.name, { exact: false })).toBeInTheDocument(); + }); + + // appends (PK) to primary key column name + expect(screen.getByText('col1 (PK)')).toBeInTheDocument(); + + // renders column type badge correctly + defaultProps.data.column_names.forEach(col => { + expect(screen.getByText(col.type)).toBeInTheDocument(); + }); + + // renders handles correctly + defaultProps.data.column_names.forEach(col => { + expect( + getByAttribute( + container, + defaultProps.data.name + '.' + col.name, + 'data-handleid', + ), + ).toBeInTheDocument(); + }); + }); + + it('renders limited columns based on the limit prop', () => { + const limitedProps = { + ...defaultProps, + limit: 2, + }; + + render(); + expect(screen.queryByText('col3')).not.toBeInTheDocument(); + }); +}); diff --git a/datajunction-ui/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx b/datajunction-ui/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx new file mode 100644 index 000000000..05e9c7fec --- /dev/null +++ b/datajunction-ui/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { DJNodeDimensions } from '../DJNodeDimensions'; +import DJClientContext from '../../../providers/djclient'; + +const mockMetric = jest.fn(); +describe('', () => { + const defaultProps = { + type: 'metric', + name: 'TestMetric', + }; + const mockDJClient = () => { + return { + DataJunctionAPI: { + metric: mockMetric, + }, + }; + }; + + const DJNodeDimensionsWithContext = (djClient, props) => { + return ( + + + + ); + }; + + beforeEach(() => { + // Reset the mock before each test + mockMetric.mockReset(); + }); + + it('fetches dimensions for metric type', async () => { + mockMetric.mockResolvedValueOnce({ + dimensions: [{ name: 'test.dimension' }], + }); + const djClient = mockDJClient(); + + render( + + + , + ); + waitFor(() => { + expect(mockMetric).toHaveBeenCalledWith(defaultProps.name); + }); + }); + + it('renders dimensions correctly after processing', async () => { + const testDimensions = [ + { + name: 'default.us_state.state_name', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.state', + ], + }, + { + name: 'default.us_state.state_region', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.state', + ], + }, + { + name: 'default.us_state.state_region_description', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.state', + ], + }, + ]; + mockMetric.mockResolvedValueOnce({ dimensions: testDimensions }); + const djClient = mockDJClient(); + + const { findByText } = render( + + + , + ); + + for (const dim of testDimensions) { + const [attribute, ...nodeName] = dim.name.split('.').reverse(); + const dimension = nodeName.reverse().join('.'); + expect(await findByText(attribute)).toBeInTheDocument(); + expect(await findByText(dimension)).toBeInTheDocument(); + } + }); + + it('does not fetch dimensions if type is not metric', () => { + const djClient = mockDJClient(); + render( + + + , + ); + expect(mockMetric).not.toHaveBeenCalled(); + }); + + it('handles errors gracefully', async () => { + mockMetric.mockRejectedValueOnce(new Error('API error')); + + const djClient = mockDJClient(); + render( + + + , + ); + + expect(await mockMetric).toHaveBeenCalledWith(defaultProps.name); + }); +}); diff --git a/datajunction-ui/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap b/datajunction-ui/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap new file mode 100644 index 000000000..dc168caf5 --- /dev/null +++ b/datajunction-ui/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap @@ -0,0 +1,117 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render and match the snapshot 1`] = ` + +
    +
    + +
    +
    +
    + shared ▶ dimensions +
    +
    +
    + + Source + +
    + + + +
    +
    + +
    +
    +
    +`; diff --git a/datajunction-ui/src/app/components/forms/Action.jsx b/datajunction-ui/src/app/components/forms/Action.jsx new file mode 100644 index 000000000..d23b2956c --- /dev/null +++ b/datajunction-ui/src/app/components/forms/Action.jsx @@ -0,0 +1,8 @@ +export class Action { + static Add = new Action('add'); + static Edit = new Action('edit'); + + constructor(name) { + this.name = name; + } +} diff --git a/datajunction-ui/src/app/components/forms/NodeNameField.jsx b/datajunction-ui/src/app/components/forms/NodeNameField.jsx new file mode 100644 index 000000000..d5e194dd2 --- /dev/null +++ b/datajunction-ui/src/app/components/forms/NodeNameField.jsx @@ -0,0 +1,64 @@ +import { ErrorMessage, Field } from 'formik'; +import { FormikSelect } from '../../pages/AddEditNodePage/FormikSelect'; +import { FullNameField } from '../../pages/AddEditNodePage/FullNameField'; +import React, { useContext, useEffect, useState } from 'react'; +import DJClientContext from '../../providers/djclient'; +import { useParams } from 'react-router-dom'; + +/* + * This component creates the namespace selector, display name input, and + * derived name fields in a form. It can be reused any time we need to create + * a new node. + */ + +export default function NodeNameField() { + const [namespaces, setNamespaces] = useState([]); + const djClient = useContext(DJClientContext).DataJunctionAPI; + let { initialNamespace } = useParams(); + + useEffect(() => { + const fetchData = async () => { + const namespaces = await djClient.namespaces(); + setNamespaces( + namespaces.map(m => ({ + value: m['namespace'], + label: m['namespace'], + })), + ); + }; + fetchData().catch(console.error); + }, [djClient, djClient.metrics]); + + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} diff --git a/datajunction-ui/src/app/components/search.css b/datajunction-ui/src/app/components/search.css new file mode 100644 index 000000000..94a80aa36 --- /dev/null +++ b/datajunction-ui/src/app/components/search.css @@ -0,0 +1,17 @@ +.search-box { + display: flex; + height: 50%; +} + +.search-results { + position: absolute; + z-index: 1000; + width: 75%; + background-color: rgba(244, 244, 244, 0.8); + border-radius: 1rem; +} + +.search-result-item { + text-decoration: wavy; + padding: 0.5rem; +} diff --git a/datajunction-ui/src/app/constants.js b/datajunction-ui/src/app/constants.js new file mode 100644 index 000000000..40b794e46 --- /dev/null +++ b/datajunction-ui/src/app/constants.js @@ -0,0 +1,2 @@ +export const AUTH_COOKIE = '__dj'; +export const LOGGED_IN_FLAG_COOKIE = '__djlif'; diff --git a/datajunction-ui/src/app/icons/AddItemIcon.jsx b/datajunction-ui/src/app/icons/AddItemIcon.jsx new file mode 100644 index 000000000..d0ee68b24 --- /dev/null +++ b/datajunction-ui/src/app/icons/AddItemIcon.jsx @@ -0,0 +1,16 @@ +const AddItemIcon = props => ( + + + +); +export default AddItemIcon; diff --git a/datajunction-ui/src/app/icons/AlertIcon.jsx b/datajunction-ui/src/app/icons/AlertIcon.jsx new file mode 100644 index 000000000..cca225000 --- /dev/null +++ b/datajunction-ui/src/app/icons/AlertIcon.jsx @@ -0,0 +1,32 @@ +const AlertIcon = props => ( + + alert_fill + + + + + + + + + +); +export default AlertIcon; diff --git a/datajunction-ui/src/app/icons/CollapsedIcon.jsx b/datajunction-ui/src/app/icons/CollapsedIcon.jsx new file mode 100644 index 000000000..53318555d --- /dev/null +++ b/datajunction-ui/src/app/icons/CollapsedIcon.jsx @@ -0,0 +1,15 @@ +const CollapsedIcon = props => ( + + + +); + +export default CollapsedIcon; diff --git a/datajunction-ui/src/app/icons/CommitIcon.jsx b/datajunction-ui/src/app/icons/CommitIcon.jsx new file mode 100644 index 000000000..7fa685915 --- /dev/null +++ b/datajunction-ui/src/app/icons/CommitIcon.jsx @@ -0,0 +1,45 @@ +import * as React from 'react'; + +const CommitIcon = props => ( + + + + + + +); +export default CommitIcon; diff --git a/datajunction-ui/src/app/icons/DJLogo.jsx b/datajunction-ui/src/app/icons/DJLogo.jsx new file mode 100644 index 000000000..bbb28e157 --- /dev/null +++ b/datajunction-ui/src/app/icons/DJLogo.jsx @@ -0,0 +1,36 @@ +const DJLogo = props => ( + + + + + + + + + + +); +export default DJLogo; diff --git a/datajunction-ui/src/app/icons/DeleteIcon.jsx b/datajunction-ui/src/app/icons/DeleteIcon.jsx new file mode 100644 index 000000000..e754e69c4 --- /dev/null +++ b/datajunction-ui/src/app/icons/DeleteIcon.jsx @@ -0,0 +1,21 @@ +const DeleteIcon = props => ( + + + + + + +); + +export default DeleteIcon; diff --git a/datajunction-ui/src/app/icons/DiffIcon.jsx b/datajunction-ui/src/app/icons/DiffIcon.jsx new file mode 100644 index 000000000..2c91f8f65 --- /dev/null +++ b/datajunction-ui/src/app/icons/DiffIcon.jsx @@ -0,0 +1,63 @@ +const DiffIcon = props => ( + + + + + + + + + +); +export default DiffIcon; diff --git a/datajunction-ui/src/app/icons/EditIcon.jsx b/datajunction-ui/src/app/icons/EditIcon.jsx new file mode 100644 index 000000000..36a2dbbb6 --- /dev/null +++ b/datajunction-ui/src/app/icons/EditIcon.jsx @@ -0,0 +1,18 @@ +const EditIcon = props => ( + + + + +); +export default EditIcon; diff --git a/datajunction-ui/src/app/icons/ExpandedIcon.jsx b/datajunction-ui/src/app/icons/ExpandedIcon.jsx new file mode 100644 index 000000000..bfa45e140 --- /dev/null +++ b/datajunction-ui/src/app/icons/ExpandedIcon.jsx @@ -0,0 +1,15 @@ +const ExpandedIcon = props => ( + + + +); + +export default ExpandedIcon; diff --git a/datajunction-ui/src/app/icons/FilterIcon.jsx b/datajunction-ui/src/app/icons/FilterIcon.jsx new file mode 100644 index 000000000..3dd7f29d3 --- /dev/null +++ b/datajunction-ui/src/app/icons/FilterIcon.jsx @@ -0,0 +1,7 @@ +const FilterIcon = props => ( + + + + +); +export default FilterIcon; diff --git a/datajunction-ui/src/app/icons/HorizontalHierarchyIcon.jsx b/datajunction-ui/src/app/icons/HorizontalHierarchyIcon.jsx new file mode 100644 index 000000000..183897cae --- /dev/null +++ b/datajunction-ui/src/app/icons/HorizontalHierarchyIcon.jsx @@ -0,0 +1,15 @@ +const HorizontalHierarchyIcon = props => ( + + + +); + +export default HorizontalHierarchyIcon; diff --git a/datajunction-ui/src/app/icons/InvalidIcon.jsx b/datajunction-ui/src/app/icons/InvalidIcon.jsx new file mode 100644 index 000000000..d59a41ece --- /dev/null +++ b/datajunction-ui/src/app/icons/InvalidIcon.jsx @@ -0,0 +1,14 @@ +const InvalidIcon = props => ( + + + +); + +export default InvalidIcon; diff --git a/datajunction-ui/src/app/icons/LoadingIcon.jsx b/datajunction-ui/src/app/icons/LoadingIcon.jsx new file mode 100644 index 000000000..ae3390e30 --- /dev/null +++ b/datajunction-ui/src/app/icons/LoadingIcon.jsx @@ -0,0 +1,14 @@ +import '../../styles/loading.css'; + +export default function LoadingIcon({ centered = true }) { + const content = ( +
    +
    +
    +
    +
    +
    + ); + + return centered ?
    {content}
    : content; +} diff --git a/datajunction-ui/src/app/icons/PythonIcon.jsx b/datajunction-ui/src/app/icons/PythonIcon.jsx new file mode 100644 index 000000000..219618a6c --- /dev/null +++ b/datajunction-ui/src/app/icons/PythonIcon.jsx @@ -0,0 +1,52 @@ +const PythonIcon = props => ( + + + + + + + + + + + + + + + + + + +); + +export default PythonIcon; diff --git a/datajunction-ui/src/app/icons/TableIcon.jsx b/datajunction-ui/src/app/icons/TableIcon.jsx new file mode 100644 index 000000000..e56776ec4 --- /dev/null +++ b/datajunction-ui/src/app/icons/TableIcon.jsx @@ -0,0 +1,14 @@ +const TableIcon = props => ( + + + +); + +export default TableIcon; diff --git a/datajunction-ui/src/app/icons/ValidIcon.jsx b/datajunction-ui/src/app/icons/ValidIcon.jsx new file mode 100644 index 000000000..b8f7cdfbf --- /dev/null +++ b/datajunction-ui/src/app/icons/ValidIcon.jsx @@ -0,0 +1,14 @@ +const ValidIcon = props => ( + + + +); + +export default ValidIcon; diff --git a/datajunction-ui/src/app/index.tsx b/datajunction-ui/src/app/index.tsx new file mode 100644 index 000000000..fbaebbd03 --- /dev/null +++ b/datajunction-ui/src/app/index.tsx @@ -0,0 +1,132 @@ +/** + * This component is the skeleton around the actual pages, and only contains + * components that should be seen on all pages, like the logo or navigation bar. + */ + +import * as React from 'react'; +import { Helmet } from 'react-helmet-async'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; + +import { NamespacePage } from './pages/NamespacePage/Loadable'; +import { NodePage } from './pages/NodePage/Loadable'; +import RevisionDiff from './pages/NodePage/RevisionDiff'; +import { SQLBuilderPage } from './pages/SQLBuilderPage/Loadable'; +import { CubeBuilderPage } from './pages/CubeBuilderPage/Loadable'; +import { TagPage } from './pages/TagPage/Loadable'; +import { AddEditNodePage } from './pages/AddEditNodePage/Loadable'; +import { AddEditTagPage } from './pages/AddEditTagPage/Loadable'; +import { NotFoundPage } from './pages/NotFoundPage/Loadable'; +import { LoginPage } from './pages/LoginPage'; +import { RegisterTablePage } from './pages/RegisterTablePage'; +import { Root } from './pages/Root/Loadable'; +import DJClientContext from './providers/djclient'; +import { DataJunctionAPI } from './services/DJService'; +import { CookiesProvider, useCookies } from 'react-cookie'; +import * as Constants from './constants'; + +export function App() { + const [cookies] = useCookies([Constants.LOGGED_IN_FLAG_COOKIE]); + return ( + + + {cookies.__djlif || process.env.REACT_DISABLE_AUTH === 'true' ? ( + <> + + + + + + } + children={ + <> + + } /> + } + /> + } + /> + } + /> + } /> + + + } key="index" /> + + } + key="namespaces" + /> + + } + > + } + > + + } + /> + } + /> + + + } + /> + } + /> + + } + /> + + } /> + + + } + /> + } /> + + + + ) : ( + + )} + + + ); +} diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/AlertMessage.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/AlertMessage.jsx new file mode 100644 index 000000000..f143dce40 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/AlertMessage.jsx @@ -0,0 +1,10 @@ +import AlertIcon from '../../icons/AlertIcon'; + +export const AlertMessage = ({ message }) => { + return ( +
    + + {message} +
    + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/ColumnsSelect.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/ColumnsSelect.jsx new file mode 100644 index 000000000..624d256fc --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/ColumnsSelect.jsx @@ -0,0 +1,80 @@ +/** + * Component for selecting node columns based on the current form state + */ +import { ErrorMessage, useFormikContext } from 'formik'; +import { useContext, useMemo, useState, useEffect } from 'react'; +import DJClientContext from '../../providers/djclient'; +import { FormikSelect } from './FormikSelect'; + +export const ColumnsSelect = ({ + defaultValue, + fieldName, + label, + labelStyle = {}, + isMulti = false, +}) => { + const djClient = useContext(DJClientContext).DataJunctionAPI; + + // Used to pull out current form values for node validation + const { values } = useFormikContext(); + + // The available columns, determined from validating the node query + const [availableColumns, setAvailableColumns] = useState([]); + const selectableOptions = useMemo(() => { + if (availableColumns && availableColumns.length > 0) { + return availableColumns; + } + }, [availableColumns]); + + // Fetch columns by validating the latest node query + const fetchColumns = async () => { + try { + const { status, json } = await djClient.validateNode( + values.type, + values.name, + values.display_name, + values.description, + values.query, + ); + if (json?.columns) { + setAvailableColumns( + json.columns.map(col => ({ value: col.name, label: col.name })), + ); + } + } catch (error) { + console.error('Error fetching columns:', error); + } + }; + + useEffect(() => { + fetchColumns(); + }, [values.type, values.name, values.query]); + + return selectableOptions === undefined ? ( + '' + ) : ( +
    + + + + { + return { + value: val, + label: val, + }; + }) + : { value: defaultValue, label: defaultValue } + } + selectOptions={selectableOptions} + formikFieldName={fieldName} + onFocus={event => fetchColumns(event)} + isMulti={isMulti} + /> + +
    + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/DescriptionField.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/DescriptionField.jsx new file mode 100644 index 000000000..04714d5d1 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/DescriptionField.jsx @@ -0,0 +1,17 @@ +import { ErrorMessage, Field } from 'formik'; + +export const DescriptionField = () => { + return ( +
    + + + +
    + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/DisplayNameField.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/DisplayNameField.jsx new file mode 100644 index 000000000..f66d6f6d6 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/DisplayNameField.jsx @@ -0,0 +1,16 @@ +import { ErrorMessage, Field } from 'formik'; + +export const DisplayNameField = () => { + return ( +
    + + + +
    + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/FormikSelect.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/FormikSelect.jsx new file mode 100644 index 000000000..7ac99be87 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/FormikSelect.jsx @@ -0,0 +1,48 @@ +/** + * A React Select component for use in Formik forms. + */ +import { useField } from 'formik'; +import Select from 'react-select'; + +export const FormikSelect = ({ + selectOptions, + formikFieldName, + placeholder, + defaultValue, + style, + className = 'SelectInput', + isMulti = false, + onFocus = event => {}, +}) => { + // eslint-disable-next-line no-unused-vars + const [field, _, helpers] = useField(formikFieldName); + const { setValue } = helpers; + + // handles both multi-select and single-select cases + const getValue = options => { + if (options) { + return isMulti ? options.map(option => option.value) : options.value; + } else { + return isMulti ? [] : ''; + } + }; + + return ( + + {!!meta.touched && !!meta.error &&
    {meta.error}
    } + + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/Loadable.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/Loadable.jsx new file mode 100644 index 000000000..4b79131ee --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/Loadable.jsx @@ -0,0 +1,20 @@ +/** + * Asynchronously loads the component for the Node page + */ + +import * as React from 'react'; +import { lazyLoad } from '../../../utils/loadable'; + +export const LazyAddEditNodePage = props => { + return lazyLoad( + () => import('./index'), + module => module.AddEditNodePage, + { + fallback:
    , + }, + )(props); +}; + +export const AddEditNodePage = props => { + return ; +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx new file mode 100644 index 000000000..6b164026f --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx @@ -0,0 +1,60 @@ +/** + * Metric unit select component + */ +import { ErrorMessage, Field } from 'formik'; +import { useContext, useEffect, useState } from 'react'; +import DJClientContext from '../../providers/djclient'; +import { labelize } from '../../../utils/form'; + +export const MetricMetadataFields = () => { + const djClient = useContext(DJClientContext).DataJunctionAPI; + + // Metric metadata + const [metricUnits, setMetricUnits] = useState([]); + const [metricDirections, setMetricDirections] = useState([]); + + // Get metric metadata values + useEffect(() => { + const fetchData = async () => { + const metadata = await djClient.listMetricMetadata(); + setMetricDirections(metadata.directions); + setMetricUnits(metadata.units); + }; + fetchData().catch(console.error); + }, [djClient]); + + return ( + <> +
    + + + + + {metricDirections.map(direction => ( + + ))} + +
    +
    + + + + + {metricUnits.map(unit => ( + + ))} + +
    + + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/MetricQueryField.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/MetricQueryField.jsx new file mode 100644 index 000000000..e54dd2017 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/MetricQueryField.jsx @@ -0,0 +1,71 @@ +/** + * Metric aggregate expression input field, which consists of a CodeMirror SQL + * editor with autocompletion for node columns and syntax highlighting. + */ +import React from 'react'; +import { ErrorMessage, Field, useFormikContext } from 'formik'; +import CodeMirror from '@uiw/react-codemirror'; +import { langs } from '@uiw/codemirror-extensions-langs'; + +export const MetricQueryField = ({ djClient, value }) => { + const [schema, setSchema] = React.useState([]); + const formik = useFormikContext(); + const sqlExt = langs.sql({ schema: schema }); + + const initialAutocomplete = async context => { + // Based on the selected upstream, we load the upstream node's columns + // into the autocomplete schema + const nodeName = formik.values['upstream_node']; + const nodeDetails = await djClient.node(nodeName); + nodeDetails.columns.forEach(col => { + schema[col.name] = []; + }); + setSchema(schema); + }; + + const updateFormik = val => { + formik.setFieldValue('aggregate_expression', val); + }; + + return ( +
    + + + +
    + { + updateFormik(value); + }} + /> +
    +
    + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/NamespaceField.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/NamespaceField.jsx new file mode 100644 index 000000000..15e5a25cd --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/NamespaceField.jsx @@ -0,0 +1,40 @@ +import { ErrorMessage } from 'formik'; +import { FormikSelect } from './FormikSelect'; +import { useContext, useEffect, useState } from 'react'; +import DJClientContext from '../../providers/djclient'; + +export const NamespaceField = ({ initialNamespace }) => { + const djClient = useContext(DJClientContext).DataJunctionAPI; + + const [namespaces, setNamespaces] = useState([]); + + // Get namespaces, only necessary when creating a node + useEffect(() => { + const fetchData = async () => { + const namespaces = await djClient.namespaces(); + setNamespaces( + namespaces.map(m => ({ + value: m['namespace'], + label: m['namespace'], + })), + ); + }; + fetchData().catch(console.error); + }, [djClient]); + + return ( +
    + + + +
    + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/NodeModeField.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/NodeModeField.jsx new file mode 100644 index 000000000..96eb26667 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/NodeModeField.jsx @@ -0,0 +1,14 @@ +import { ErrorMessage, Field } from 'formik'; + +export const NodeModeField = () => { + return ( +
    + + + + + + +
    + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/NodeQueryField.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/NodeQueryField.jsx new file mode 100644 index 000000000..01a7fbc6d --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/NodeQueryField.jsx @@ -0,0 +1,94 @@ +/** + * SQL query input field, which consists of a CodeMirror SQL editor with autocompletion + * (for node names and columns) and syntax highlighting. + */ +import React from 'react'; +import { ErrorMessage, Field, useFormikContext } from 'formik'; +import CodeMirror from '@uiw/react-codemirror'; +import { langs } from '@uiw/codemirror-extensions-langs'; + +export const NodeQueryField = ({ djClient, value }) => { + const [schema, setSchema] = React.useState([]); + const formik = useFormikContext(); + const sqlExt = langs.sql({ schema: schema }); + + const initialAutocomplete = async context => { + // Based on the parsed prefix, we load node names with that prefix + // into the autocomplete schema. At this stage we don't load the columns + // to save on unnecessary calls + const word = context.matchBefore(/[\.\w]*/); + const matches = await djClient.nodes(word.text); + matches.forEach(nodeName => { + if (schema[nodeName] === undefined) { + schema[nodeName] = []; + setSchema(schema); + } + }); + }; + + const updateFormik = val => { + formik.setFieldValue('query', val); + }; + + const updateAutocomplete = async (value, _) => { + // If a particular node has been chosen, load the columns of that node into + // the autocomplete schema for column-level autocompletion + for (var nodeName in schema) { + if ( + value.includes(nodeName) && + (!schema.hasOwnProperty(nodeName) || + (schema.hasOwnProperty(nodeName) && schema[nodeName].length === 0)) + ) { + const nodeDetails = await djClient.node(nodeName); + schema[nodeName] = nodeDetails.columns.map(col => col.name); + setSchema(schema); + } + } + }; + + return ( +
    + + + +
    + { + updateFormik(value); + updateAutocomplete(value, viewUpdate); + }} + /> +
    +
    + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx new file mode 100644 index 000000000..b476baf21 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx @@ -0,0 +1,54 @@ +/** + * Required dimensions select component + */ +import { ErrorMessage, useFormikContext } from 'formik'; +import React, { useContext, useEffect, useState } from 'react'; +import DJClientContext from '../../providers/djclient'; +import { FormikSelect } from './FormikSelect'; + +export const RequiredDimensionsSelect = ({ + defaultValue, + style, + className = 'MultiSelectInput', +}) => { + const djClient = useContext(DJClientContext).DataJunctionAPI; + + // Used to pull out current form values for node validation + const { values } = useFormikContext(); + + // Select options, i.e., the available dimensions + const [selectOptions, setSelectOptions] = useState([]); + + useEffect(() => { + const fetchData = async () => { + if (values.upstream_node) { + const data = await djClient.node(values.upstream_node); + setSelectOptions( + data.columns.map(col => { + return { + value: col.name, + label: col.name, + }; + }), + ); + } + }; + fetchData().catch(console.error); + }, [djClient, values.upstream_node]); + + return ( +
    + + + +
    + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/TagsField.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/TagsField.jsx new file mode 100644 index 000000000..a8f760933 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/TagsField.jsx @@ -0,0 +1,47 @@ +/** + * Tags select field + */ +import { ErrorMessage } from 'formik'; +import { useContext, useEffect, useState } from 'react'; +import DJClientContext from '../../providers/djclient'; +import { FormikSelect } from './FormikSelect'; + +export const TagsField = ({ defaultValue }) => { + const djClient = useContext(DJClientContext).DataJunctionAPI; + + // All available tags + const [tags, setTags] = useState([]); + + useEffect(() => { + const fetchData = async () => { + const tags = await djClient.listTags(); + setTags( + tags.map(tag => ({ + value: tag.name, + label: tag.display_name, + })), + ); + }; + fetchData().catch(console.error); + }, [djClient]); + + return ( +
    + + + + + +
    + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx new file mode 100644 index 000000000..fb91acb0f --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx @@ -0,0 +1,49 @@ +/** + * Upstream node select field + */ +import { ErrorMessage } from 'formik'; +import { useContext, useEffect, useState } from 'react'; +import DJClientContext from '../../providers/djclient'; +import { FormikSelect } from './FormikSelect'; + +export const UpstreamNodeField = ({ defaultValue }) => { + const djClient = useContext(DJClientContext).DataJunctionAPI; + + // All available nodes + const [availableNodes, setAvailableNodes] = useState([]); + + useEffect(() => { + async function fetchData() { + const sources = await djClient.nodesWithType('source'); + const transforms = await djClient.nodesWithType('transform'); + const dimensions = await djClient.nodesWithType('dimension'); + const nodes = sources.concat(transforms).concat(dimensions); + setAvailableNodes( + nodes.map(node => { + return { + value: node, + label: node, + }; + }), + ); + } + fetchData(); + }, [djClient]); + + return ( +
    + + + + + +
    + ); +}; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx new file mode 100644 index 000000000..e24ea055e --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import fetchMock from 'jest-fetch-mock'; +import userEvent from '@testing-library/user-event'; +import { + initializeMockDJClient, + renderCreateNode, + renderEditNode, + testElement, +} from './index.test'; +import { mocks } from '../../../../mocks/mockNodes'; + +describe('AddEditNodePage submission failed', () => { + beforeEach(() => { + fetchMock.resetMocks(); + jest.clearAllMocks(); + window.scrollTo = jest.fn(); + }); + + it('for creating a node', async () => { + const mockDjClient = initializeMockDJClient(); + mockDjClient.DataJunctionAPI.createNode.mockReturnValue({ + status: 500, + json: { message: 'Some columns in the primary key [] were not found' }, + }); + mockDjClient.DataJunctionAPI.listTags.mockReturnValue([ + { name: 'purpose', display_name: 'Purpose' }, + { name: 'intent', display_name: 'Intent' }, + ]); + + const element = testElement(mockDjClient); + const { container } = renderCreateNode(element); + + await userEvent.type( + screen.getByLabelText('Display Name *'), + 'Some Test Metric', + ); + await userEvent.type( + screen.getByLabelText('Query *'), + 'SELECT * FROM test', + ); + await userEvent.click(screen.getByText('Create dimension')); + + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.createNode).toBeCalled(); + expect(mockDjClient.DataJunctionAPI.createNode).toBeCalledWith( + 'dimension', + 'default.some_test_metric', + 'Some Test Metric', + '', + 'SELECT * FROM test', + 'published', + 'default', + null, + undefined, + undefined, + undefined, + ); + expect( + screen.getByText(/Some columns in the primary key \[] were not found/), + ).toBeInTheDocument(); + }); + + // After failed creation, it should return a failure message + expect(container.getElementsByClassName('alert')).toMatchSnapshot(); + }, 60000); + + it('for editing a node', async () => { + const mockDjClient = initializeMockDJClient(); + mockDjClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); + mockDjClient.DataJunctionAPI.patchNode.mockReturnValue({ + status: 500, + json: { message: 'Update failed' }, + }); + + mockDjClient.DataJunctionAPI.tagsNode.mockReturnValue({ + status: 404, + json: { message: 'Some tags were not found' }, + }); + + mockDjClient.DataJunctionAPI.listTags.mockReturnValue([ + { name: 'purpose', display_name: 'Purpose' }, + { name: 'intent', display_name: 'Intent' }, + ]); + + const element = testElement(mockDjClient); + renderEditNode(element); + + await userEvent.type(screen.getByLabelText('Display Name *'), '!!!'); + await userEvent.type(screen.getByLabelText('Description'), '!!!'); + await userEvent.click(screen.getByText('Save')); + await waitFor(async () => { + expect(mockDjClient.DataJunctionAPI.patchNode).toBeCalledTimes(1); + expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalled(); + expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledWith( + 'default.num_repair_orders', + ['purpose'], + ); + expect(mockDjClient.DataJunctionAPI.tagsNode).toReturnWith({ + json: { message: 'Some tags were not found' }, + status: 404, + }); + + expect(await screen.getByText('Update failed')).toBeInTheDocument(); + }); + }, 60000); +}); diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx new file mode 100644 index 000000000..de803b977 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx @@ -0,0 +1,270 @@ +import React from 'react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import fetchMock from 'jest-fetch-mock'; +import userEvent from '@testing-library/user-event'; +import { + initializeMockDJClient, + renderCreateMetric, + renderCreateNode, + renderEditNode, + renderEditTransformNode, + testElement, +} from './index.test'; +import { mocks } from '../../../../mocks/mockNodes'; +import { render } from '../../../../setupTests'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import DJClientContext from '../../../providers/djclient'; +import { AddEditNodePage } from '../index'; + +describe('AddEditNodePage submission succeeded', () => { + beforeEach(() => { + fetchMock.resetMocks(); + jest.clearAllMocks(); + window.scrollTo = jest.fn(); + }); + + it('for creating a dimension/transform node', async () => { + const mockDjClient = initializeMockDJClient(); + mockDjClient.DataJunctionAPI.createNode.mockReturnValue({ + status: 200, + json: { name: 'default.some_test_dim' }, + }); + + mockDjClient.DataJunctionAPI.tagsNode.mockReturnValue({ + status: 200, + json: { message: 'Success' }, + }); + + mockDjClient.DataJunctionAPI.listTags.mockReturnValue([ + { name: 'purpose', display_name: 'Purpose' }, + { name: 'intent', display_name: 'Intent' }, + ]); + + const element = testElement(mockDjClient); + const { container } = renderCreateNode(element); + + await userEvent.type( + screen.getByLabelText('Display Name *'), + 'Some Test Dim', + ); + await userEvent.type( + screen.getByLabelText('Query *'), + 'SELECT a, b, c FROM test', + ); + await userEvent.click(screen.getByText('Create dimension')); + + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.createNode).toBeCalled(); + expect(mockDjClient.DataJunctionAPI.createNode).toBeCalledWith( + 'dimension', + 'default.some_test_dim', + 'Some Test Dim', + '', + 'SELECT a, b, c FROM test', + 'published', + 'default', + null, + undefined, + undefined, + undefined, + ); + expect(screen.getByText(/default.some_test_dim/)).toBeInTheDocument(); + }); + + // After successful creation, it should return a success message + expect(screen.getByTestId('success')).toHaveTextContent( + 'Successfully created node default.some_test_dim', + ); + }, 60000); + + it('for creating a metric node', async () => { + const mockDjClient = initializeMockDJClient(); + mockDjClient.DataJunctionAPI.createNode.mockReturnValue({ + status: 200, + json: { name: 'default.some_test_metric' }, + }); + + mockDjClient.DataJunctionAPI.tagsNode.mockReturnValue({ + status: 200, + json: { message: 'Success' }, + }); + + mockDjClient.DataJunctionAPI.nodesWithType + .mockReturnValueOnce(['default.test1']) + .mockReturnValueOnce(['default.test2']) + .mockReturnValueOnce([]); + + mockDjClient.DataJunctionAPI.listTags.mockReturnValue([ + { name: 'purpose', display_name: 'Purpose' }, + { name: 'intent', display_name: 'Intent' }, + ]); + + mockDjClient.DataJunctionAPI.listMetricMetadata.mockReturnValue( + mocks.metricMetadata, + ); + + const element = testElement(mockDjClient); + const { container } = renderCreateMetric(element); + + await userEvent.type( + screen.getByLabelText('Display Name *'), + 'Some Test Metric', + ); + const selectUpstream = screen.getByTestId('select-upstream-node'); + fireEvent.keyDown(selectUpstream.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('default.repair_orders')); + + await userEvent.type( + screen.getByLabelText('Aggregate Expression *'), + 'SUM(a)', + ); + await userEvent.click(screen.getByText('Create metric')); + + await waitFor( + () => { + expect(mockDjClient.DataJunctionAPI.createNode).toBeCalled(); + expect(mockDjClient.DataJunctionAPI.createNode).toBeCalledWith( + 'metric', + 'default.some_test_metric', + 'Some Test Metric', + '', + 'SELECT SUM(a) FROM default.repair_orders', + 'published', + 'default', + null, + undefined, + undefined, + undefined, + ); + expect( + screen.getByText(/default.some_test_metric/), + ).toBeInTheDocument(); + }, + { timeout: 10000 }, + ); + + // After successful creation, it should return a success message + expect(screen.getByTestId('success')).toHaveTextContent( + 'Successfully created node default.some_test_metric', + ); + }, 60000); + + it('for editing a transform or dimension node', async () => { + const mockDjClient = initializeMockDJClient(); + + mockDjClient.DataJunctionAPI.node.mockReturnValue(mocks.mockTransformNode); + mockDjClient.DataJunctionAPI.patchNode = jest.fn(); + mockDjClient.DataJunctionAPI.patchNode.mockReturnValue({ + status: 201, + json: { name: 'default.repair_order_transform', type: 'transform' }, + }); + + mockDjClient.DataJunctionAPI.tagsNode.mockReturnValue({ + status: 200, + json: { message: 'Success' }, + }); + + mockDjClient.DataJunctionAPI.listTags.mockReturnValue([ + { name: 'purpose', display_name: 'Purpose' }, + { name: 'intent', display_name: 'Intent' }, + ]); + + const element = testElement(mockDjClient); + const { getByTestId } = renderEditTransformNode(element); + + await userEvent.type(screen.getByLabelText('Display Name *'), '!!!'); + await userEvent.type(screen.getByLabelText('Description'), '!!!'); + await userEvent.click(screen.getByText('Save')); + + const selectTags = getByTestId('select-tags'); + fireEvent.keyDown(selectTags.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('Purpose')); + + await waitFor(async () => { + expect(mockDjClient.DataJunctionAPI.patchNode).toBeCalledTimes(1); + expect(mockDjClient.DataJunctionAPI.patchNode).toBeCalledWith( + 'default.repair_order_transform', + 'Default: Repair Order Transform!!!', + 'Repair order dimension!!!', + 'SELECT repair_order_id, municipality_id, hard_hat_id, dispatcher_id FROM default.repair_orders', + 'published', + [], + undefined, + undefined, + undefined, + ); + expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledTimes(1); + expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledWith( + 'default.repair_order_transform', + [], + ); + + expect(mockDjClient.DataJunctionAPI.listMetricMetadata).toBeCalledTimes( + 0, + ); + expect( + await screen.getByText(/Successfully updated transform node/), + ).toBeInTheDocument(); + }); + }, 1000000); + + it('for editing a metric node', async () => { + const mockDjClient = initializeMockDJClient(); + + mockDjClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); + mockDjClient.DataJunctionAPI.metric.mockReturnValue(mocks.mockMetricNode); + mockDjClient.DataJunctionAPI.patchNode = jest.fn(); + mockDjClient.DataJunctionAPI.patchNode.mockReturnValue({ + status: 201, + json: { name: 'default.num_repair_orders', type: 'metric' }, + }); + + mockDjClient.DataJunctionAPI.tagsNode.mockReturnValue({ + status: 200, + json: { message: 'Success' }, + }); + + mockDjClient.DataJunctionAPI.listTags.mockReturnValue([ + { name: 'purpose', display_name: 'Purpose' }, + { name: 'intent', display_name: 'Intent' }, + ]); + + const element = testElement(mockDjClient); + const { getByTestId } = renderEditNode(element); + + await userEvent.type(screen.getByLabelText('Display Name *'), '!!!'); + await userEvent.type(screen.getByLabelText('Description'), '!!!'); + await userEvent.click(screen.getByText('Save')); + + const selectTags = getByTestId('select-tags'); + fireEvent.keyDown(selectTags.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('Purpose')); + + await waitFor(async () => { + expect(mockDjClient.DataJunctionAPI.patchNode).toBeCalledTimes(1); + expect(mockDjClient.DataJunctionAPI.patchNode).toBeCalledWith( + 'default.num_repair_orders', + 'Default: Num Repair Orders!!!', + 'Number of repair orders!!!', + 'SELECT count(repair_order_id) FROM default.repair_orders', + 'published', + [], + 'neutral', + 'unitless', + undefined, + ); + expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledTimes(1); + expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledWith( + 'default.num_repair_orders', + ['purpose'], + ); + + expect(mockDjClient.DataJunctionAPI.listMetricMetadata).toBeCalledTimes( + 1, + ); + expect( + await screen.getByText(/Successfully updated metric node/), + ).toBeInTheDocument(); + }); + }, 1000000); +}); diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx new file mode 100644 index 000000000..186df0119 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Formik, Form } from 'formik'; +import { FormikSelect } from '../FormikSelect'; + +describe('FormikSelect', () => { + const namespaces = [ + { value: 'default', label: 'default' }, + { value: 'basic', label: 'basic' }, + { value: 'basic.one', label: 'basic.one' }, + { value: 'basic.two', label: 'basic.two' }, + ]; + + const singleSelect = () => { + const utils = render( + +
    + + +
    , + ); + + const selectInput = screen.getByRole('combobox'); + return { + ...utils, + selectInput, + }; + }; + + const multiSelect = () => { + const utils = render( + +
    + + +
    , + ); + + const selectInput = screen.getByRole('combobox'); + return { + ...utils, + selectInput, + }; + }; + + it('renders the single select component with provided options', () => { + singleSelect(); + userEvent.click(screen.getByRole('combobox')); // to open the dropdown + expect(screen.getByText('basic.one')).toBeInTheDocument(); + }); + + it('renders the multi-select component with provided options', () => { + multiSelect(); + userEvent.click(screen.getByRole('combobox')); // to open the dropdown + expect(screen.getByText('basic.one')).toBeInTheDocument(); + }); +}); diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx new file mode 100644 index 000000000..f5b854baa --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { Formik, Form } from 'formik'; +import { FullNameField } from '../FullNameField'; + +describe('FullNameField', () => { + const setup = initialValues => { + return render( + +
    + + +
    , + ); + }; + + it('generates the full name based on namespace and display name', () => { + setup({ namespace: 'cats', display_name: 'Jasper the Cat' }); + const fullNameInput = screen.getByRole('textbox'); + waitFor(() => { + expect(fullNameInput.value).toBe('cats.jasper_the_cat'); + }); + }); + + it('does not set the full name if namespace or display name is missing', () => { + setup({ namespace: '', display_name: '' }); + + const fullNameInput = screen.getByRole('textbox'); + expect(fullNameInput.value).toBe(''); + }); +}); diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx new file mode 100644 index 000000000..870ed57bf --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Formik, Form } from 'formik'; +import { NodeQueryField } from '../NodeQueryField'; + +describe('NodeQueryField', () => { + const mockDjClient = { + nodes: jest.fn(), + node: jest.fn(), + }; + const renderWithFormik = (djClient = mockDjClient) => { + return render( + +
    + + +
    , + ); + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders without crashing', () => { + renderWithFormik(); + expect(screen.getByRole('textbox')).toBeInTheDocument(); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); +}); diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap new file mode 100644 index 000000000..b15071d48 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AddEditNodePage Create node page renders with the selected nodeType and namespace 1`] = `HTMLCollection []`; + +exports[`AddEditNodePage submission failed for creating a node 1`] = ` +HTMLCollection [ +
    + + + alert_fill + + + + + + + + + + + Some columns in the primary key [] were not found +
    , +] +`; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap new file mode 100644 index 000000000..e53884268 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AddEditNodePage Create node page renders with the selected nodeType and namespace 1`] = `HTMLCollection []`; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap new file mode 100644 index 000000000..e53884268 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AddEditNodePage Create node page renders with the selected nodeType and namespace 1`] = `HTMLCollection []`; diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/index.test.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/index.test.jsx new file mode 100644 index 000000000..75c8ca781 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/__tests__/index.test.jsx @@ -0,0 +1,216 @@ +import React from 'react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { screen, waitFor } from '@testing-library/react'; +import { render } from '../../../../setupTests'; +import fetchMock from 'jest-fetch-mock'; +import { AddEditNodePage } from '../index.jsx'; +import { mocks } from '../../../../mocks/mockNodes'; +import DJClientContext from '../../../providers/djclient'; + +fetchMock.enableMocks(); + +export const initializeMockDJClient = () => { + return { + DataJunctionAPI: { + namespace: _ => { + return [ + { + name: 'default.contractors', + display_name: 'Default: Contractors', + version: 'v1.0', + type: 'source', + status: 'valid', + mode: 'published', + updated_at: '2023-08-21T16:48:53.246914+00:00', + }, + { + name: 'default.num_repair_orders', + display_name: 'Default: Num Repair Orders', + version: 'v1.0', + type: 'metric', + status: 'valid', + mode: 'published', + updated_at: '2023-08-21T16:48:56.841704+00:00', + }, + ]; + }, + metrics: {}, + namespaces: () => { + return [ + { + namespace: 'default', + num_nodes: 33, + }, + { + namespace: 'default123', + num_nodes: 0, + }, + ]; + }, + createNode: jest.fn(), + patchNode: jest.fn(), + node: jest.fn(), + tagsNode: jest.fn(), + listTags: jest.fn().mockReturnValue([]), + metric: jest.fn().mockReturnValue(mocks.mockMetricNode), + nodesWithType: jest + .fn() + .mockReturnValueOnce(['a']) + .mockReturnValueOnce(['b']) + .mockReturnValueOnce(['default.repair_orders']), + listMetricMetadata: jest.fn().mockReturnValue({ + directions: ['higher_is_better', 'lower_is_better', 'neutral'], + units: [ + { name: 'dollar', label: 'Dollar' }, + { name: 'second', label: 'Second' }, + ], + }), + }, + }; +}; + +export const testElement = djClient => { + return ( + + + + ); +}; + +export const renderCreateNode = element => { + return render( + + + + + , + ); +}; + +export const renderCreateMetric = element => { + return render( + + + + + , + ); +}; + +export const renderEditNode = element => { + return render( + + + + + , + ); +}; + +export const renderEditTransformNode = element => { + return render( + + + + + , + ); +}; + +describe('AddEditNodePage', () => { + beforeEach(() => { + fetchMock.resetMocks(); + jest.clearAllMocks(); + window.scrollTo = jest.fn(); + }); + + it('Create node page renders with the selected nodeType and namespace', async () => { + const mockDjClient = initializeMockDJClient(); + const element = testElement(mockDjClient); + const { container } = renderCreateNode(element); + + // The node type should be included in the page title + await waitFor(() => { + expect( + container.getElementsByClassName('node_type__metric'), + ).toMatchSnapshot(); + + // The namespace should be set to the one provided in params + screen + .getAllByText('default') + .forEach(element => expect(element).toBeInTheDocument()); + }); + }); + + it('Edit node page renders with the selected node', async () => { + const mockDjClient = initializeMockDJClient(); + mockDjClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); + + const element = testElement(mockDjClient); + renderEditNode(element); + + await waitFor(() => { + // Should be an edit node page + expect(screen.getByText('Edit')).toBeInTheDocument(); + + // The node name should be loaded onto the page + expect(screen.getByText('default.num_repair_orders')).toBeInTheDocument(); + + // The node type should be loaded onto the page + expect(screen.getByText('metric')).toBeInTheDocument(); + + // The description should be populated + expect(screen.getByText('Number of repair orders')).toBeInTheDocument(); + + // The upstream node should be populated + expect(screen.getByText('default.repair_orders')).toBeInTheDocument(); + + // The aggregate expression should be populated + expect(screen.getByText('count')).toBeInTheDocument(); + expect(screen.getByText('(repair_order_id)')).toBeInTheDocument(); + }); + }); + + it('Verify edit page node not found', async () => { + const mockDjClient = initializeMockDJClient(); + mockDjClient.DataJunctionAPI.node = jest.fn(); + mockDjClient.DataJunctionAPI.node.mockReturnValue({ + message: 'A node with name `default.num_repair_orders` does not exist.', + errors: [], + warnings: [], + }); + const element = testElement(mockDjClient); + renderEditNode(element); + + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.node).toBeCalledTimes(1); + expect( + screen.getByText('Node default.num_repair_orders does not exist!'), + ).toBeInTheDocument(); + }); + }, 60000); + + it('Verify only transforms, metrics, and dimensions can be edited', async () => { + const mockDjClient = initializeMockDJClient(); + mockDjClient.DataJunctionAPI.node = jest.fn(); + mockDjClient.DataJunctionAPI.node.mockReturnValue({ + namespace: 'default', + type: 'source', + name: 'default.repair_orders', + display_name: 'Default: Repair Orders', + }); + const element = testElement(mockDjClient); + renderEditNode(element); + + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.node).toBeCalledTimes(1); + expect( + screen.getByText( + 'Node default.num_repair_orders is of type source and cannot be edited', + ), + ).toBeInTheDocument(); + }); + }, 60000); +}); diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/index.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/index.jsx new file mode 100644 index 000000000..af29e726d --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditNodePage/index.jsx @@ -0,0 +1,465 @@ +/** + * Node add + edit page for transforms, metrics, and dimensions. The creation and edit flow for these + * node types is largely the same, with minor differences handled server-side. For the `query` + * field, this page will render a CodeMirror SQL editor with autocompletion and syntax highlighting. + */ +import { ErrorMessage, Form, Formik } from 'formik'; +import NamespaceHeader from '../../components/NamespaceHeader'; +import { useContext, useEffect, useState } from 'react'; +import DJClientContext from '../../providers/djclient'; +import 'styles/node-creation.scss'; +import { useParams, useNavigate } from 'react-router-dom'; +import { FullNameField } from './FullNameField'; +import { MetricQueryField } from './MetricQueryField'; +import { displayMessageAfterSubmit } from '../../../utils/form'; +import { NodeQueryField } from './NodeQueryField'; +import { MetricMetadataFields } from './MetricMetadataFields'; +import { UpstreamNodeField } from './UpstreamNodeField'; +import { TagsField } from './TagsField'; +import { NamespaceField } from './NamespaceField'; +import { AlertMessage } from './AlertMessage'; +import { DisplayNameField } from './DisplayNameField'; +import { DescriptionField } from './DescriptionField'; +import { NodeModeField } from './NodeModeField'; +import { RequiredDimensionsSelect } from './RequiredDimensionsSelect'; +import LoadingIcon from '../../icons/LoadingIcon'; +import { ColumnsSelect } from './ColumnsSelect'; + +class Action { + static Add = new Action('add'); + static Edit = new Action('edit'); + + constructor(name) { + this.name = name; + } +} + +export function AddEditNodePage({ extensions = {} }) { + const djClient = useContext(DJClientContext).DataJunctionAPI; + const navigate = useNavigate(); + + let { nodeType, initialNamespace, name } = useParams(); + const action = name !== undefined ? Action.Edit : Action.Add; + + const initialValues = { + name: action === Action.Edit ? name : '', + namespace: action === Action.Add ? initialNamespace : '', + display_name: '', + query: '', + type: nodeType, + description: '', + primary_key: '', + mode: 'published', + }; + + const validator = values => { + const errors = {}; + if (!values.name) { + errors.name = 'Required'; + } + if (values.type !== 'metric' && !values.query) { + errors.query = 'Required'; + } + return errors; + }; + + const handleSubmit = async (values, { setSubmitting, setStatus }) => { + if (action === Action.Add) { + await createNode(values, setStatus).then(_ => { + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); + setSubmitting(false); + }); + } else { + await patchNode(values, setStatus).then(_ => { + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); + setSubmitting(false); + }); + } + }; + const submitHandlers = [handleSubmit]; + + const pageTitle = + action === Action.Add ? ( +

    + Create{' '} + + {nodeType} + +

    + ) : ( +

    Edit

    + ); + + const staticFieldsInEdit = node => ( + <> +
    + {name} +
    +
    + {node.type} +
    + + ); + + const primaryKeyToList = primaryKey => { + return primaryKey.map(columnName => columnName.trim()); + }; + + const createNode = async (values, setStatus) => { + const { status, json } = await djClient.createNode( + nodeType, + values.name, + values.display_name, + values.description, + values.type === 'metric' + ? `SELECT ${values.aggregate_expression} FROM ${values.upstream_node}` + : values.query, + values.mode, + values.namespace, + values.primary_key ? primaryKeyToList(values.primary_key) : null, + values.metric_direction, + values.metric_unit, + values.required_dimensions, + ); + if (status === 200 || status === 201) { + if (values.tags) { + await djClient.tagsNode(values.name, values.tags); + } + setStatus({ + success: ( + <> + Successfully created {json.type} node{' '} +
    {json.name}! + + ), + }); + } else { + setStatus({ + failure: `${json.message}`, + }); + } + }; + + const patchNode = async (values, setStatus) => { + const { status, json } = await djClient.patchNode( + values.name, + values.display_name, + values.description, + values.type === 'metric' + ? `SELECT ${values.aggregate_expression} FROM ${values.upstream_node}` + : values.query, + values.mode, + values.primary_key ? primaryKeyToList(values.primary_key) : null, + values.metric_direction, + values.metric_unit, + values.required_dimensions, + ); + const tagsResponse = await djClient.tagsNode( + values.name, + values.tags.map(tag => tag), + ); + if ((status === 200 || status === 201) && tagsResponse.status === 200) { + setStatus({ + success: ( + <> + Successfully updated {json.type} node{' '} + {json.name}! + + ), + }); + } else { + setStatus({ + failure: `${json.message}`, + }); + } + }; + + const fullNameInput = ( +
    + + + +
    + ); + + const nodeCanBeEdited = nodeType => { + return new Set(['transform', 'metric', 'dimension']).has(nodeType); + }; + + const getExistingNodeData = async name => { + const data = await djClient.node(name); + if (data.type === 'metric') { + const metric = await djClient.metric(name); + data.upstream_node = metric.upstream_node; + data.expression = metric.expression; + data.required_dimensions = metric.required_dimensions; + } + return data; + }; + + const primaryKeyFromNode = node => { + return node.columns + .filter( + col => + col.attributes && + col.attributes.filter( + attr => attr.attribute_type.name === 'primary_key', + ).length > 0, + ) + .map(col => col.name); + }; + + const runValidityChecks = (data, setNode, setMessage) => { + // Check if node exists + if (data.message !== undefined) { + setNode(null); + setMessage(`Node ${name} does not exist!`); + return; + } + + // Check if node type can be edited + if (!nodeCanBeEdited(data.type)) { + setNode(null); + if (data.type === 'cube') { + navigate(`/nodes/${data.name}/edit-cube`); + } + setMessage(`Node ${name} is of type ${data.type} and cannot be edited`); + } + }; + + const updateFieldsWithNodeData = ( + data, + setFieldValue, + setNode, + setSelectTags, + setSelectPrimaryKey, + setSelectUpstreamNode, + setSelectRequiredDims, + ) => { + // Update fields with existing data to prepare for edit + const fields = [ + 'display_name', + 'query', + 'type', + 'description', + 'primary_key', + 'mode', + 'tags', + 'expression', + 'upstream_node', + ]; + const primaryKey = primaryKeyFromNode(data); + fields.forEach(field => { + if (field === 'primary_key') { + setFieldValue(field, primaryKey); + } else if (field === 'tags') { + setFieldValue( + field, + data[field].map(tag => tag.name), + ); + } else { + setFieldValue(field, data[field] || '', false); + } + }); + if (data.metric_metadata?.direction) { + setFieldValue('metric_direction', data.metric_metadata.direction); + } + if (data.metric_metadata?.unit) { + setFieldValue( + 'metric_unit', + data.metric_metadata.unit.name.toLowerCase(), + ); + } + if (data.expression) { + setFieldValue('aggregate_expression', data.expression); + } + if (data.upstream_node) { + setFieldValue('upstream_node', data.upstream_node); + } + setNode(data); + + // For react-select fields, we have to explicitly set the entire + // field rather than just the values + setSelectTags( + { + return { value: t.name, label: t.display_name }; + })} + />, + ); + setSelectPrimaryKey( + , + ); + setSelectRequiredDims( + { + return { value: dim, label: dim }; + })} + />, + ); + setSelectUpstreamNode( + , + ); + }; + + return ( +
    + +
    +
    + {pageTitle} +
    + { + try { + submitHandlers.map(handler => + handler(values, { setSubmitting, setStatus }), + ); + } catch (error) { + console.error('Error in submission', error); + } finally { + setSubmitting(false); + } + }} + > + {function Render({ isSubmitting, status, setFieldValue }) { + const [node, setNode] = useState([]); + const [selectPrimaryKey, setSelectPrimaryKey] = useState(null); + const [selectRequiredDims, setSelectRequiredDims] = + useState(null); + const [selectUpstreamNode, setSelectUpstreamNode] = + useState(null); + const [selectTags, setSelectTags] = useState(null); + const [message, setMessage] = useState(''); + + useEffect(() => { + const fetchData = async () => { + if (action === Action.Edit) { + const data = await getExistingNodeData(name); + runValidityChecks(data, setNode, setMessage); + updateFieldsWithNodeData( + data, + setFieldValue, + setNode, + setSelectTags, + setSelectPrimaryKey, + setSelectUpstreamNode, + setSelectRequiredDims, + ); + } + }; + fetchData().catch(console.error); + }, [setFieldValue]); + return ( +
    + {displayMessageAfterSubmit(status)} + {action === Action.Edit && message ? ( + + ) : ( + <> + {action === Action.Add ? ( + + ) : ( + staticFieldsInEdit(node) + )} + + {action === Action.Add ? fullNameInput : ''} + +
    + {nodeType === 'metric' || node?.type === 'metric' ? ( + action === Action.Edit ? ( + selectUpstreamNode + ) : ( + + ) + ) : ( + '' + )} +
    +
    + {nodeType === 'metric' || node.type === 'metric' ? ( + + ) : ( + + )} +
    + {nodeType === 'metric' || node.type === 'metric' ? ( + + ) : ( + '' + )} + {nodeType !== 'metric' && node.type !== 'metric' ? ( + action === Action.Edit ? ( + selectPrimaryKey + ) : ( + + ) + ) : action === Action.Edit ? ( + selectRequiredDims + ) : ( + + )} + {Object.entries(extensions).map( + ([key, ExtensionComponent]) => ( +
    + + submitHandlers.indexOf(onSubmit) === -1 + ? submitHandlers.push(onSubmit) + : null + } + /> +
    + ), + )} + {action === Action.Edit ? selectTags : } + + + + + )} + + ); + }} +
    +
    +
    +
    +
    + ); +} diff --git a/datajunction-ui/src/app/pages/AddEditTagPage/Loadable.jsx b/datajunction-ui/src/app/pages/AddEditTagPage/Loadable.jsx new file mode 100644 index 000000000..b0c2a0ec6 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditTagPage/Loadable.jsx @@ -0,0 +1,16 @@ +/** + * Asynchronously loads the component for the Node page + */ + +import * as React from 'react'; +import { lazyLoad } from '../../../utils/loadable'; + +export const AddEditTagPage = () => { + return lazyLoad( + () => import('./index'), + module => module.AddEditTagPage, + { + fallback:
    , + }, + )(); +}; diff --git a/datajunction-ui/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx b/datajunction-ui/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx new file mode 100644 index 000000000..cf2b28e93 --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import fetchMock from 'jest-fetch-mock'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../../setupTests'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import DJClientContext from '../../../providers/djclient'; +import { AddEditTagPage } from '../index'; + +describe('', () => { + const initializeMockDJClient = () => { + return { + DataJunctionAPI: { + addTag: jest.fn(), + }, + }; + }; + + const mockDjClient = initializeMockDJClient(); + + beforeEach(() => { + fetchMock.resetMocks(); + jest.clearAllMocks(); + window.scrollTo = jest.fn(); + }); + + const renderAddEditTagPage = element => { + return render( + + + + + , + ); + }; + + const testElement = djClient => { + return ( + + + + ); + }; + + it('adds a tag correctly', async () => { + mockDjClient.DataJunctionAPI.addTag.mockReturnValue({ + status: 200, + json: { + name: 'amanita_muscaria', + display_name: 'Amanita Muscaria', + tag_type: 'fungi', + description: 'Fly agaric, is poisonous', + }, + }); + + const element = testElement(mockDjClient); + renderAddEditTagPage(element); + + await userEvent.type(screen.getByLabelText('Name'), 'amanita_muscaria'); + await userEvent.type( + screen.getByLabelText('Display Name'), + 'Amanita Muscaria', + ); + await userEvent.type(screen.getByLabelText('Tag Type'), 'fungi'); + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.addTag).toBeCalled(); + expect(mockDjClient.DataJunctionAPI.addTag).toBeCalledWith( + 'amanita_muscaria', + 'Amanita Muscaria', + 'fungi', + undefined, + ); + expect(screen.getByTestId('success')).toHaveTextContent( + 'Successfully added tag Amanita Muscaria', + ); + }); + }, 60000); + + it('fails to add a tag', async () => { + mockDjClient.DataJunctionAPI.addTag.mockReturnValue({ + status: 500, + json: { message: 'Tag exists' }, + }); + + const element = testElement(mockDjClient); + renderAddEditTagPage(element); + + await userEvent.click(screen.getByRole('button')); + + await userEvent.type(screen.getByLabelText('Name'), 'amanita_muscaria'); + await userEvent.type( + screen.getByLabelText('Display Name'), + 'Amanita Muscaria', + ); + await userEvent.type(screen.getByLabelText('Tag Type'), 'fungi'); + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.addTag).toBeCalled(); + expect(screen.getByTestId('failure')).toHaveTextContent( + 'alert_fillTag exists', + ); + }); + }, 60000); +}); diff --git a/datajunction-ui/src/app/pages/AddEditTagPage/index.jsx b/datajunction-ui/src/app/pages/AddEditTagPage/index.jsx new file mode 100644 index 000000000..4c3dab10c --- /dev/null +++ b/datajunction-ui/src/app/pages/AddEditTagPage/index.jsx @@ -0,0 +1,132 @@ +/** + * Add or edit tags + */ +import { ErrorMessage, Field, Form, Formik } from 'formik'; + +import NamespaceHeader from '../../components/NamespaceHeader'; +import React, { useContext } from 'react'; +import DJClientContext from '../../providers/djclient'; +import 'styles/node-creation.scss'; +import { displayMessageAfterSubmit } from '../../../utils/form'; + +export function AddEditTagPage() { + const djClient = useContext(DJClientContext).DataJunctionAPI; + const initialValues = { + name: '', + }; + + const validator = values => { + const errors = {}; + if (!values.name) { + errors.name = 'Required'; + } + return errors; + }; + + const handleSubmit = async (values, { setSubmitting, setStatus }) => { + const { status, json } = await djClient.addTag( + values.name, + values.display_name, + values.tag_type, + values.description, + ); + if (status === 200 || status === 201) { + setStatus({ + success: ( + <> + Successfully added tag{' '} + {json.display_name}. + + ), + }); + } else { + setStatus({ + failure: `${json.message}`, + }); + } + setSubmitting(false); + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); + }; + + return ( +
    + +
    +
    +

    + Add{' '} + + Tag + +

    +
    + + {function Render({ isSubmitting, status }) { + return ( +
    + {displayMessageAfterSubmit(status)} + { + <> +
    + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    + + + +
    + + + } +
    + ); + }} +
    +
    +
    +
    +
    + ); +} diff --git a/datajunction-ui/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx b/datajunction-ui/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx new file mode 100644 index 000000000..92acaf632 --- /dev/null +++ b/datajunction-ui/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx @@ -0,0 +1,154 @@ +/** + * A select component for picking dimensions + */ +import { useField, useFormikContext } from 'formik'; +import Select from 'react-select'; +import React, { useContext, useEffect, useState } from 'react'; +import DJClientContext from '../../providers/djclient'; +import { labelize } from '../../../utils/form'; + +export const DimensionsSelect = ({ cube }) => { + const djClient = useContext(DJClientContext).DataJunctionAPI; + const { values, setFieldValue } = useFormikContext(); + + // eslint-disable-next-line no-unused-vars + const [field, _, helpers] = useField('dimensions'); + const { setValue } = helpers; + + // All common dimensions for the selected metrics, grouped by the dimension node and path + const [allDimensionsOptions, setAllDimensionsOptions] = useState([]); + + // The selected dimensions, also grouped by dimension node and path + const [selectedDimensionsByGroup, setSelectedDimensionsByGroup] = useState( + {}, + ); + + // The existing cube node's dimensions, if editing a cube + const [defaultDimensions, setDefaultDimensions] = useState([]); + + useEffect(() => { + const fetchData = async () => { + let cubeDimensions = undefined; + if (cube) { + cubeDimensions = cube?.cube_elements + .filter(element => element.type === 'dimension') + .map(cubeDim => { + return { + value: cubeDim.node_name + '.' + cubeDim.name, + label: + labelize(cubeDim.name) + + (cubeDim.properties?.includes('primary_key') ? ' (PK)' : ''), + }; + }); + setDefaultDimensions(cubeDimensions); + setValue(cubeDimensions.map(m => m.value)); + } + + if (values.metrics && values.metrics.length > 0) { + // Populate the common dimensions list based on the selected metrics + const commonDimensions = await djClient.commonDimensions( + values.metrics, + ); + const grouped = Object.entries( + commonDimensions.reduce((group, dimension) => { + group[dimension.node_name + dimension.path] = + group[dimension.node_name + dimension.path] ?? []; + group[dimension.node_name + dimension.path].push(dimension); + return group; + }, {}), + ); + setAllDimensionsOptions(grouped); + + // Set the selected cube dimensions if an existing cube is being edited + if (cube) { + const currentSelectedDimensionsByGroup = {}; + grouped.forEach(grouping => { + const dimensionsInGroup = grouping[1]; + currentSelectedDimensionsByGroup[dimensionsInGroup[0].node_name] = + getValue( + cubeDimensions.filter( + dim => + dimensionsInGroup.filter(x => { + return dim.value === x.name; + }).length > 0, + ), + ); + setSelectedDimensionsByGroup(currentSelectedDimensionsByGroup); + setValue(Object.values(currentSelectedDimensionsByGroup).flat(2)); + }); + } + } else { + setAllDimensionsOptions([]); + } + }; + fetchData().catch(console.error); + }, [djClient, setFieldValue, setValue, values.metrics, cube]); + + // Retrieves the selected values as a list (since it is a multi-select) + const getValue = options => { + if (options) { + return options.map(option => option.value); + } else { + return []; + } + }; + + // Builds the block of dimensions selectors, grouped by node name + path + return allDimensionsOptions.map(grouping => { + const dimensionsInGroup = grouping[1]; + const groupHeader = ( +
    + + {dimensionsInGroup[0].node_display_name} + {' '} + via{' '} + + {dimensionsInGroup[0].path.join(' → ')} + +
    + ); + const dimensionGroupOptions = dimensionsInGroup.map(dim => { + return { + value: dim.name, + label: + labelize(dim.name.split('.').slice(-1)[0]) + + (dim.properties?.includes('primary_key') ? ' (PK)' : ''), + }; + }); + // + const cubeDimensions = defaultDimensions.filter( + dim => + dimensionGroupOptions.filter(x => { + return dim.value === x.value; + }).length > 0, + ); + return ( + <> + {groupHeader} + + { + setValue(getValue(selected)); + }} + noOptionsMessage={() => 'No metrics found.'} + isMulti + isClearable + closeMenuOnSelect={false} + /> + ); + } + }; + return render(); +}; diff --git a/datajunction-ui/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx b/datajunction-ui/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx new file mode 100644 index 000000000..32a5642aa --- /dev/null +++ b/datajunction-ui/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx @@ -0,0 +1,405 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import DJClientContext from '../../../providers/djclient'; +import { CubeBuilderPage } from '../index'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import React from 'react'; + +const mockDjClient = { + metrics: jest.fn(), + commonDimensions: jest.fn(), + createCube: jest.fn(), + namespaces: jest.fn(), + cube: jest.fn(), + node: jest.fn(), + listTags: jest.fn(), + tagsNode: jest.fn(), + patchCube: jest.fn(), +}; + +const mockMetrics = [ + 'default.num_repair_orders', + 'default.avg_repair_price', + 'default.total_repair_cost', +]; + +const mockCube = { + node_revision_id: 102, + node_id: 33, + type: 'cube', + name: 'default.repair_orders_cube', + display_name: 'Default: Repair Orders Cube', + version: 'v4.0', + description: 'Repairs cube', + availability: null, + cube_elements: [ + { + name: 'default_DOT_total_repair_cost', + display_name: 'Total Repair Cost', + node_name: 'default.total_repair_cost', + type: 'metric', + partition: null, + }, + { + name: 'default_DOT_num_repair_orders', + display_name: 'Num Repair Orders', + node_name: 'default.num_repair_orders', + type: 'metric', + partition: null, + }, + { + name: 'country', + display_name: 'Country', + node_name: 'default.hard_hat', + type: 'dimension', + partition: null, + }, + { + name: 'state', + display_name: 'State', + node_name: 'default.hard_hat', + type: 'dimension', + partition: null, + }, + ], + query: '', + columns: [ + { + name: 'default.total_repair_cost', + display_name: 'Total Repair Cost', + type: 'double', + attributes: [], + dimension: null, + partition: null, + }, + { + name: 'default.num_repair_orders', + display_name: 'Num Repair Orders', + type: 'bigint', + attributes: [], + dimension: null, + partition: null, + }, + { + name: 'default.hard_hat.country', + display_name: 'Country', + type: 'string', + attributes: [], + dimension: null, + partition: null, + }, + { + name: 'default.hard_hat.state', + display_name: 'State', + type: 'string', + attributes: [], + dimension: null, + partition: null, + }, + ], + updated_at: '2023-12-03T06:51:09.598532+00:00', + materializations: [], +}; + +const mockCommonDimensions = [ + { + name: 'default.date_dim.dateint', + type: 'timestamp', + node_name: 'default.date_dim', + node_display_name: 'Date', + properties: [], + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.dateint', + type: 'timestamp', + node_name: 'default.date_dim', + node_display_name: 'Date', + properties: [], + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.date_dim.day', + type: 'int', + node_name: 'default.date_dim', + node_display_name: 'Date', + properties: [], + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.day', + type: 'int', + node_name: 'default.date_dim', + node_display_name: 'Date', + properties: [], + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.date_dim.month', + type: 'int', + node_name: 'default.date_dim', + node_display_name: 'Date', + properties: [], + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.month', + type: 'int', + node_name: 'default.date_dim', + node_display_name: 'Date', + properties: [], + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.date_dim.year', + type: 'int', + node_name: 'default.date_dim', + node_display_name: 'Date', + properties: [], + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.year', + type: 'int', + node_name: 'default.date_dim', + node_display_name: 'Date', + properties: [], + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, +]; + +describe('CubeBuilderPage', () => { + beforeEach(() => { + mockDjClient.metrics.mockResolvedValue(mockMetrics); + mockDjClient.commonDimensions.mockResolvedValue(mockCommonDimensions); + mockDjClient.createCube.mockResolvedValue({ status: 201, json: {} }); + mockDjClient.namespaces.mockResolvedValue(['default']); + mockDjClient.cube.mockResolvedValue(mockCube); + mockDjClient.node.mockResolvedValue(mockCube); + mockDjClient.listTags.mockResolvedValue([]); + mockDjClient.tagsNode.mockResolvedValue([]); + mockDjClient.patchCube.mockResolvedValue({ status: 201, json: {} }); + + window.scrollTo = jest.fn(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders without crashing', () => { + render( + + + , + ); + expect(screen.getByText('Cube')).toBeInTheDocument(); + }); + + it('renders the Metrics section', () => { + render( + + + , + ); + expect(screen.getByText('Metrics *')).toBeInTheDocument(); + }); + + it('renders the Dimensions section', () => { + render( + + + , + ); + expect(screen.getByText('Dimensions *')).toBeInTheDocument(); + }); + + it('creates a new cube', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(mockDjClient.metrics).toHaveBeenCalled(); + }); + + const selectMetrics = screen.getAllByTestId('select-metrics')[0]; + expect(selectMetrics).toBeDefined(); + expect(selectMetrics).not.toBeNull(); + expect(screen.getAllByText('3 Available Metrics')[0]).toBeInTheDocument(); + + fireEvent.keyDown(selectMetrics.firstChild, { key: 'ArrowDown' }); + for (const metric of mockMetrics) { + await waitFor(() => { + expect(screen.getByText(metric)).toBeInTheDocument(); + fireEvent.click(screen.getByText(metric)); + }); + } + fireEvent.click(screen.getAllByText('Dimensions *')[0]); + + expect(mockDjClient.commonDimensions).toHaveBeenCalled(); + + const selectDimensions = screen.getAllByTestId('select-dimensions')[0]; + expect(selectDimensions).toBeDefined(); + expect(selectDimensions).not.toBeNull(); + expect( + screen.getByText( + 'default.repair_order_details.repair_order_id → default.repair_order.hard_hat_id → default.hard_hat.birth_date', + ), + ).toBeInTheDocument(); + + const selectDimensionsDate = screen.getAllByTestId( + 'dimensions-default.date_dim', + )[0]; + + fireEvent.keyDown(selectDimensionsDate.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('Day')); + fireEvent.click(screen.getByText('Month')); + fireEvent.click(screen.getByText('Year')); + fireEvent.click(screen.getByText('Dateint')); + + // Save + const createCube = screen.getAllByRole('button', { + name: 'CreateCube', + })[0]; + expect(createCube).toBeInTheDocument(); + + await waitFor(() => { + fireEvent.click(createCube); + }); + await waitFor(() => { + expect(mockDjClient.createCube).toHaveBeenCalledWith( + '', + '', + '', + 'published', + [ + 'default.num_repair_orders', + 'default.avg_repair_price', + 'default.total_repair_cost', + ], + [ + 'default.date_dim.day', + 'default.date_dim.month', + 'default.date_dim.year', + 'default.date_dim.dateint', + ], + [], + ); + }); + }); + + const renderEditNode = element => { + return render( + + + + + , + ); + }; + + it('updates an existing cube', async () => { + renderEditNode( + + + , + ); + expect(screen.getAllByText('Edit')[0]).toBeInTheDocument(); + await waitFor(() => { + expect(mockDjClient.cube).toHaveBeenCalled(); + }); + await waitFor(() => { + expect(mockDjClient.metrics).toHaveBeenCalled(); + }); + + const selectMetrics = screen.getAllByTestId('select-metrics')[0]; + expect(selectMetrics).toBeDefined(); + expect(selectMetrics).not.toBeNull(); + expect(screen.getByText('default.num_repair_orders')).toBeInTheDocument(); + + fireEvent.click(screen.getAllByText('Dimensions *')[0]); + + expect(mockDjClient.commonDimensions).toHaveBeenCalled(); + + const selectDimensions = screen.getAllByTestId('select-dimensions')[0]; + expect(selectDimensions).toBeDefined(); + expect(selectDimensions).not.toBeNull(); + expect( + screen.getByText( + 'default.repair_order_details.repair_order_id → default.repair_order.hard_hat_id → default.hard_hat.birth_date', + ), + ).toBeInTheDocument(); + + const selectDimensionsDate = screen.getAllByTestId( + 'dimensions-default.date_dim', + )[0]; + + fireEvent.keyDown(selectDimensionsDate.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('Day')); + fireEvent.click(screen.getByText('Month')); + fireEvent.click(screen.getByText('Year')); + fireEvent.click(screen.getByText('Dateint')); + + // Save + const createCube = screen.getAllByRole('button', { + name: 'CreateCube', + })[0]; + expect(createCube).toBeInTheDocument(); + + await waitFor(() => { + fireEvent.click(createCube); + }); + await waitFor(() => { + expect(mockDjClient.patchCube).toHaveBeenCalledWith( + 'default.repair_orders_cube', + 'Default: Repair Orders Cube', + 'Repairs cube', + 'draft', + ['default.total_repair_cost', 'default.num_repair_orders'], + [ + 'default.date_dim.day', + 'default.date_dim.month', + 'default.date_dim.year', + 'default.date_dim.dateint', + ], + [], + ); + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/CubeBuilderPage/index.jsx b/datajunction-ui/src/app/pages/CubeBuilderPage/index.jsx new file mode 100644 index 000000000..561026cb6 --- /dev/null +++ b/datajunction-ui/src/app/pages/CubeBuilderPage/index.jsx @@ -0,0 +1,267 @@ +import React, { useContext, useEffect, useState } from 'react'; +import NamespaceHeader from '../../components/NamespaceHeader'; +import { DataJunctionAPI } from '../../services/DJService'; +import DJClientContext from '../../providers/djclient'; +import 'react-querybuilder/dist/query-builder.scss'; +import 'styles/styles.scss'; +import { ErrorMessage, Field, Form, Formik } from 'formik'; +import { displayMessageAfterSubmit } from '../../../utils/form'; +import { useParams } from 'react-router-dom'; +import { Action } from '../../components/forms/Action'; +import NodeNameField from '../../components/forms/NodeNameField'; +import { MetricsSelect } from './MetricsSelect'; +import { DimensionsSelect } from './DimensionsSelect'; +import { TagsField } from '../AddEditNodePage/TagsField'; + +export function CubeBuilderPage() { + const djClient = useContext(DJClientContext).DataJunctionAPI; + + let { nodeType, initialNamespace, name } = useParams(); + const action = name !== undefined ? Action.Edit : Action.Add; + const validator = ruleType => !!ruleType.value; + + const initialValues = { + name: action === Action.Edit ? name : '', + namespace: action === Action.Add ? initialNamespace : '', + display_name: '', + description: '', + mode: 'published', + metrics: [], + dimensions: [], + filters: [], + tags: [], + }; + + const handleSubmit = (values, { setSubmitting, setStatus }) => { + if (action === Action.Add) { + setTimeout(() => { + createNode(values, setStatus); + setSubmitting(false); + }, 400); + } else { + setTimeout(() => { + patchNode(values, setStatus); + setSubmitting(false); + }, 400); + } + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); + }; + + const createNode = async (values, setStatus) => { + const { status, json } = await djClient.createCube( + values.name, + values.display_name, + values.description, + values.mode, + values.metrics, + values.dimensions, + values.filters || [], + ); + if (status === 200 || status === 201) { + if (values.tags) { + await djClient.tagsNode(values.name, values.tags); + } + setStatus({ + success: ( + <> + Successfully created {json.type} node{' '} + {json.name}! + + ), + }); + } else { + setStatus({ + failure: `${json.message}`, + }); + } + }; + + const patchNode = async (values, setStatus) => { + const { status, json } = await djClient.patchCube( + values.name, + values.display_name, + values.description, + values.mode, + values.metrics, + values.dimensions, + values.filters || [], + ); + const tagsResponse = await djClient.tagsNode( + values.name, + (values.tags || []).map(tag => tag), + ); + if ((status === 200 || status === 201) && tagsResponse.status === 200) { + setStatus({ + success: ( + <> + Successfully updated {json.type} node{' '} + {json.name}! + + ), + }); + } else { + setStatus({ + failure: `${json.message}`, + }); + } + }; + + const updateFieldsWithNodeData = (data, setFieldValue, setSelectTags) => { + setFieldValue('display_name', data.display_name || '', false); + setFieldValue('description', data.description || '', false); + setFieldValue('mode', data.mode || 'draft', false); + setFieldValue( + 'tags', + data.tags.map(tag => tag.name), + ); + // For react-select fields, we have to explicitly set the entire + // field rather than just the values + setSelectTags( + { + return { value: t.name, label: t.display_name }; + })} + />, + ); + }; + + const staticFieldsInEdit = () => ( + <> +
    + {name} +
    +
    + cube +
    +
    + + + +
    + + ); + + // @ts-ignore + return ( + <> +
    + + + {function Render({ isSubmitting, status, setFieldValue, props }) { + const [node, setNode] = useState([]); + const [selectTags, setSelectTags] = useState(null); + + // Get cube + useEffect(() => { + const fetchData = async () => { + if (name) { + const cube = await djClient.cube(name); + setNode(cube); + updateFieldsWithNodeData(cube, setFieldValue, setSelectTags); + } + }; + fetchData().catch(console.error); + }, [setFieldValue]); + + return ( +
    +
    +
    +

    + {action === Action.Edit ? 'Edit' : 'Create'}{' '} + + Cube + +

    + {displayMessageAfterSubmit(status)} + {action === Action.Add ? ( + + ) : ( + staticFieldsInEdit(node) + )} +
    + + + +
    +
    + +

    Select metrics to include in the cube.

    + + {action === Action.Edit ? ( + + ) : ( + + )} + +
    +
    +
    +
    + +

    + Select dimensions to include in the cube. As metrics are + selected above, the list of available dimensions will be + filtered to those shared by the selected metrics. If the + dimensions list is empty, no shared dimensions were + discovered. +

    + + {action === Action.Edit ? ( + + ) : ( + + )} + +
    +
    + + + + + + +
    + {action === Action.Edit ? selectTags : } + +
    +
    +
    + ); + }} +
    +
    + + ); +} + +CubeBuilderPage.defaultProps = { + djClient: DataJunctionAPI, +}; diff --git a/datajunction-ui/src/app/pages/LoginPage/LoginForm.jsx b/datajunction-ui/src/app/pages/LoginPage/LoginForm.jsx new file mode 100644 index 000000000..31ef632a9 --- /dev/null +++ b/datajunction-ui/src/app/pages/LoginPage/LoginForm.jsx @@ -0,0 +1,124 @@ +import { useState } from 'react'; +import { Formik, Form, Field, ErrorMessage } from 'formik'; +import '../../../styles/login.css'; +import LoadingIcon from '../../icons/LoadingIcon'; +import logo from '../Root/assets/dj-logo.png'; +import GitHubLoginButton from './assets/sign-in-with-github.png'; +import GoogleLoginButton from './assets/sign-in-with-google.png'; +import * as Yup from 'yup'; + +const githubLoginURL = new URL('/github/login/', process.env.REACT_APP_DJ_URL); +const googleLoginURL = new URL('/google/login/', process.env.REACT_APP_DJ_URL); + +const LoginSchema = Yup.object().shape({ + username: Yup.string() + .min(2, 'Must be at least 2 characters') + .required('Username is required'), + password: Yup.string().required('Password is required'), +}); + +export default function LoginForm({ setShowSignup }) { + const [, setError] = useState(''); + + // Add the path that the user was trying to access in order to properly redirect after auth + githubLoginURL.searchParams.append('target', window.location.pathname); + googleLoginURL.searchParams.append('target', window.location.pathname); + + const handleBasicLogin = async ({ username, password }) => { + const data = new FormData(); + data.append('username', username); + data.append('password', password); + await fetch(`${process.env.REACT_APP_DJ_URL}/basic/login/`, { + method: 'POST', + body: data, + credentials: 'include', + }).catch(error => { + setError(error ? JSON.stringify(error) : ''); + }); + window.location.reload(); + }; + + return ( + { + setTimeout(() => { + handleBasicLogin(values); + setSubmitting(false); + }, 400); + }} + > + {({ isSubmitting }) => ( +
    +
    + DJ Logo +

    DataJunction

    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +

    + Don't have an account yet?{' '} + setShowSignup(true)}>Sign Up +

    +
    + +
    +

    Or

    +
    + {process.env.REACT_ENABLE_GITHUB_OAUTH === 'true' ? ( +
    + + Sign in with GitHub + +
    + ) : ( + '' + )} + {process.env.REACT_ENABLE_GOOGLE_OAUTH === 'true' ? ( +
    + + Sign in with Google + +
    + ) : ( + '' + )} +
    + )} +
    + ); +} diff --git a/datajunction-ui/src/app/pages/LoginPage/SignupForm.jsx b/datajunction-ui/src/app/pages/LoginPage/SignupForm.jsx new file mode 100644 index 000000000..7315346c4 --- /dev/null +++ b/datajunction-ui/src/app/pages/LoginPage/SignupForm.jsx @@ -0,0 +1,156 @@ +import { useState } from 'react'; +import { Formik, Form, Field, ErrorMessage } from 'formik'; +import '../../../styles/login.css'; +import logo from '../Root/assets/dj-logo.png'; +import LoadingIcon from '../../icons/LoadingIcon'; +import GitHubLoginButton from './assets/sign-in-with-github.png'; +import GoogleLoginButton from './assets/sign-in-with-google.png'; +import * as Yup from 'yup'; + +const githubLoginURL = new URL('/github/login/', process.env.REACT_APP_DJ_URL); +const googleLoginURL = new URL('/google/login/', process.env.REACT_APP_DJ_URL); + +const SignupSchema = Yup.object().shape({ + email: Yup.string().email('Invalid email').required('Email is required'), + signupUsername: Yup.string() + .min(3, 'Must be at least 2 characters') + .max(20, 'Must be less than 20 characters') + .required('Username is required'), + signupPassword: Yup.string().required('Password is required'), +}); + +export default function SignupForm({ setShowSignup }) { + const [, setError] = useState(''); + + // Add the path that the user was trying to access in order to properly redirect after auth + githubLoginURL.searchParams.append('target', window.location.pathname); + googleLoginURL.searchParams.append('target', window.location.pathname); + + const handleBasicSignup = async ({ + email, + signupUsername, + signupPassword, + }) => { + const data = new FormData(); + data.append('email', email); + data.append('username', signupUsername); + data.append('password', signupPassword); + await fetch(`${process.env.REACT_APP_DJ_URL}/basic/user/`, { + method: 'POST', + body: data, + credentials: 'include', + }).catch(error => { + setError(error ? JSON.stringify(error) : ''); + }); + const loginData = new FormData(); + loginData.append('username', signupUsername); + loginData.append('password', signupPassword); + await fetch(`${process.env.REACT_APP_DJ_URL}/basic/login/`, { + method: 'POST', + body: data, + credentials: 'include', + }).catch(error => { + setError(error ? JSON.stringify(error) : ''); + }); + window.location.reload(); + }; + + return ( + { + setTimeout(() => { + handleBasicSignup(values); + setSubmitting(false); + }, 400); + }} + > + {({ isSubmitting }) => ( +
    +
    + DJ Logo +

    DataJunction

    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +

    + Have an account already?{' '} + setShowSignup(false)}>Login +

    +
    + +
    +

    Or

    +
    + {process.env.REACT_ENABLE_GITHUB_OAUTH === 'true' ? ( +
    + + Sign in with GitHub + +
    + ) : ( + '' + )} + {process.env.REACT_ENABLE_GOOGLE_OAUTH === 'true' ? ( +
    + + Sign in with Google + +
    + ) : ( + '' + )} +
    + )} +
    + ); +} diff --git a/datajunction-ui/src/app/pages/LoginPage/__tests__/index.test.jsx b/datajunction-ui/src/app/pages/LoginPage/__tests__/index.test.jsx new file mode 100644 index 000000000..761e4c96a --- /dev/null +++ b/datajunction-ui/src/app/pages/LoginPage/__tests__/index.test.jsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import { LoginPage } from '../index'; + +describe('LoginPage', () => { + const original = window.location; + + beforeAll(() => { + Object.defineProperty(window, 'location', { + configurable: true, + value: { reload: jest.fn() }, + }); + }); + + afterAll(() => { + Object.defineProperty(window, 'location', { + configurable: true, + value: original, + }); + }); + + beforeEach(() => { + fetch.resetMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('displays error messages when fields are empty and form is submitted', async () => { + const { getByText, queryAllByText } = render(); + fireEvent.click(getByText('Login')); + + await waitFor(() => { + expect(getByText('DataJunction')).toBeInTheDocument(); + expect(getByText('Username is required')).toBeInTheDocument(); + expect(getByText('Password is required')).toBeInTheDocument(); + }); + }); + + it('calls fetch with correct data on login', async () => { + const username = 'testUser'; + const password = 'testPassword'; + + const { getByText, getByPlaceholderText } = render(); + fireEvent.change(getByPlaceholderText('Username'), { + target: { value: username }, + }); + fireEvent.change(getByPlaceholderText('Password'), { + target: { value: password }, + }); + fireEvent.click(getByText('Login')); + + await waitFor(() => { + expect(fetch).toHaveBeenCalledWith( + `${process.env.REACT_APP_DJ_URL}/basic/login/`, + expect.objectContaining({ + method: 'POST', + body: expect.any(FormData), + credentials: 'include', + }), + ); + expect(window.location.reload).toHaveBeenCalled(); + }); + }); + + it('calls fetch with correct data on signup', async () => { + const email = 'testEmail@testEmail.com'; + const username = 'testUser'; + const password = 'testPassword'; + + const { getByText, getByPlaceholderText } = render(); + fireEvent.click(getByText('Sign Up')); + fireEvent.change(getByPlaceholderText('Email'), { + target: { value: email }, + }); + fireEvent.change(getByPlaceholderText('Username'), { + target: { value: username }, + }); + fireEvent.change(getByPlaceholderText('Password'), { + target: { value: password }, + }); + fireEvent.click(getByText('Sign Up')); + + await waitFor(() => { + expect(fetch).toHaveBeenCalledWith( + `${process.env.REACT_APP_DJ_URL}/basic/user/`, + expect.objectContaining({ + method: 'POST', + body: expect.any(FormData), + credentials: 'include', + }), + ); + expect(window.location.reload).toHaveBeenCalled(); + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/LoginPage/assets/sign-in-with-github.png b/datajunction-ui/src/app/pages/LoginPage/assets/sign-in-with-github.png new file mode 100644 index 000000000..d497cc9be Binary files /dev/null and b/datajunction-ui/src/app/pages/LoginPage/assets/sign-in-with-github.png differ diff --git a/datajunction-ui/src/app/pages/LoginPage/assets/sign-in-with-google.png b/datajunction-ui/src/app/pages/LoginPage/assets/sign-in-with-google.png new file mode 100644 index 000000000..bfc343b72 Binary files /dev/null and b/datajunction-ui/src/app/pages/LoginPage/assets/sign-in-with-google.png differ diff --git a/datajunction-ui/src/app/pages/LoginPage/index.jsx b/datajunction-ui/src/app/pages/LoginPage/index.jsx new file mode 100644 index 000000000..72eb6ee6a --- /dev/null +++ b/datajunction-ui/src/app/pages/LoginPage/index.jsx @@ -0,0 +1,17 @@ +import { useState } from 'react'; +import '../../../styles/login.css'; +import SignupForm from './SignupForm'; +import LoginForm from './LoginForm'; + +export function LoginPage() { + const [showSignup, setShowSignup] = useState(false); + return ( +
    + {showSignup ? ( + + ) : ( + + )} +
    + ); +} diff --git a/datajunction-ui/src/app/pages/NamespacePage/AddNamespacePopover.jsx b/datajunction-ui/src/app/pages/NamespacePage/AddNamespacePopover.jsx new file mode 100644 index 000000000..8272f58dd --- /dev/null +++ b/datajunction-ui/src/app/pages/NamespacePage/AddNamespacePopover.jsx @@ -0,0 +1,85 @@ +import { useContext, useState } from 'react'; +import * as React from 'react'; +import DJClientContext from '../../providers/djclient'; +import { ErrorMessage, Field, Form, Formik } from 'formik'; +import AddItemIcon from '../../icons/AddItemIcon'; +import { displayMessageAfterSubmit } from '../../../utils/form'; + +export default function AddNamespacePopover({ namespace }) { + const djClient = useContext(DJClientContext).DataJunctionAPI; + const [popoverAnchor, setPopoverAnchor] = useState(false); + + const addNamespace = async ({ namespace }, { setSubmitting, setStatus }) => { + setSubmitting(false); + const response = await djClient.addNamespace(namespace); + if (response.status === 200 || response.status === 201) { + setStatus({ success: 'Saved' }); + } else { + setStatus({ + failure: `${response.json.message}`, + }); + } + window.location.reload(); + }; + + return ( + <> + +
    + + {function Render({ isSubmitting, status, setFieldValue }) { + return ( +
    + {displayMessageAfterSubmit(status)} + + + + + + +
    + ); + }} +
    +
    + + ); +} diff --git a/datajunction-ui/src/app/pages/NamespacePage/Explorer.jsx b/datajunction-ui/src/app/pages/NamespacePage/Explorer.jsx new file mode 100644 index 000000000..73df648a8 --- /dev/null +++ b/datajunction-ui/src/app/pages/NamespacePage/Explorer.jsx @@ -0,0 +1,61 @@ +import React, { useEffect, useState } from 'react'; +import CollapsedIcon from '../../icons/CollapsedIcon'; +import ExpandedIcon from '../../icons/ExpandedIcon'; + +const Explorer = ({ item = [], current }) => { + const [items, setItems] = useState([]); + const [expand, setExpand] = useState(false); + const [highlight, setHighlight] = useState(false); + + useEffect(() => { + setItems(item); + setHighlight(current); + if (current !== undefined && current?.startsWith(item.path)) { + setExpand(true); + } else setExpand(false); + }, [current, item]); + + const handleClickOnParent = e => { + e.stopPropagation(); + setExpand(prev => { + return !prev; + }); + }; + + return ( + <> +
    + {items.children && items.children.length > 0 ? ( + {!expand ? : } + ) : null} + {items.namespace}{' '} +
    + {items.children + ? items.children.map((item, index) => ( +
    +
    + +
    +
    + )) + : null} + + ); +}; + +export default Explorer; diff --git a/datajunction-ui/src/app/pages/NamespacePage/FieldControl.jsx b/datajunction-ui/src/app/pages/NamespacePage/FieldControl.jsx new file mode 100644 index 000000000..2eee3a955 --- /dev/null +++ b/datajunction-ui/src/app/pages/NamespacePage/FieldControl.jsx @@ -0,0 +1,21 @@ +import { components } from 'react-select'; + +const Control = ({ children, ...props }) => { + const { label, onLabelClick } = props.selectProps; + const style = { + cursor: 'pointer', + padding: '10px 5px 10px 12px', + color: 'rgb(112, 110, 115)', + }; + + return ( + + + {label} + + {children} + + ); +}; + +export default Control; diff --git a/datajunction-ui/src/app/pages/NamespacePage/Loadable.jsx b/datajunction-ui/src/app/pages/NamespacePage/Loadable.jsx new file mode 100644 index 000000000..d4fa9fff2 --- /dev/null +++ b/datajunction-ui/src/app/pages/NamespacePage/Loadable.jsx @@ -0,0 +1,16 @@ +/** + * Asynchronously loads the component for namespaces node-viewing page + */ + +import * as React from 'react'; +import { lazyLoad } from '../../../utils/loadable'; + +export const NamespacePage = props => { + return lazyLoad( + () => import('./index'), + module => module.NamespacePage, + { + fallback:
    , + }, + )(props); +}; diff --git a/datajunction-ui/src/app/pages/NamespacePage/NodeTypeSelect.jsx b/datajunction-ui/src/app/pages/NamespacePage/NodeTypeSelect.jsx new file mode 100644 index 000000000..577756fd1 --- /dev/null +++ b/datajunction-ui/src/app/pages/NamespacePage/NodeTypeSelect.jsx @@ -0,0 +1,30 @@ +import Select from 'react-select'; +import Control from './FieldControl'; + +export default function NodeTypeSelect({ onChange }) { + return ( + + onChange(e)} + options={tags?.map(tag => { + return { + value: tag.name, + label: tag.display_name, + }; + })} + /> + + ); +} diff --git a/datajunction-ui/src/app/pages/NamespacePage/UserSelect.jsx b/datajunction-ui/src/app/pages/NamespacePage/UserSelect.jsx new file mode 100644 index 000000000..008578118 --- /dev/null +++ b/datajunction-ui/src/app/pages/NamespacePage/UserSelect.jsx @@ -0,0 +1,47 @@ +import { useContext, useEffect, useState } from 'react'; +import DJClientContext from '../../providers/djclient'; +import Control from './FieldControl'; + +import Select from 'react-select'; + +export default function UserSelect({ onChange, currentUser }) { + const djClient = useContext(DJClientContext).DataJunctionAPI; + const [retrieved, setRetrieved] = useState(false); + const [users, setUsers] = useState([]); + + useEffect(() => { + const fetchData = async () => { + const users = await djClient.users(); + setUsers(users); + setRetrieved(true); + }; + fetchData().catch(console.error); + }, [djClient]); + + return ( + + {retrieved ? ( + + + + + <> + + + + + +
    +
    + + +
    +
    +
    + + + +
    +
    + + + + ); + }} + + + + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/ClientCodePopover.jsx b/datajunction-ui/src/app/pages/NodePage/ClientCodePopover.jsx new file mode 100644 index 000000000..93bf2168e --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/ClientCodePopover.jsx @@ -0,0 +1,46 @@ +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { useEffect, useRef, useState } from 'react'; +import { nightOwl } from 'react-syntax-highlighter/src/styles/hljs'; +import PythonIcon from '../../icons/PythonIcon'; + +export default function ClientCodePopover({ code }) { + const [codeAnchor, setCodeAnchor] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = event => { + if (ref.current && !ref.current.contains(event.target)) { + setCodeAnchor(false); + } + }; + document.addEventListener('click', handleClickOutside, true); + return () => { + document.removeEventListener('click', handleClickOutside, true); + }; + }, [setCodeAnchor]); + + return ( + <> + + + + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/DimensionFilter.jsx b/datajunction-ui/src/app/pages/NodePage/DimensionFilter.jsx new file mode 100644 index 000000000..d6a479bb4 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/DimensionFilter.jsx @@ -0,0 +1,86 @@ +import { useContext, useEffect, useState } from 'react'; +import * as React from 'react'; +import DJClientContext from '../../providers/djclient'; +import CreatableSelect from 'react-select/creatable'; + +export default function DimensionFilter({ dimension, onChange }) { + const djClient = useContext(DJClientContext).DataJunctionAPI; + const [dimensionValues, setDimensionValues] = useState([]); + + useEffect(() => { + const fetchData = async () => { + const dimensionNode = await djClient.node(dimension.metadata.node_name); + + // Only include the primary keys as filterable dimensions for now, until we figure out how + // to build a manageable UI experience around all dimensional attributes + if (dimensionNode && dimensionNode.type === 'dimension') { + const primaryKey = + dimensionNode.name + + '.' + + dimensionNode.columns + .filter(col => + col.attributes.some( + attr => attr.attribute_type.name === 'primary_key', + ), + ) + .map(col => col.name); + const label = + dimensionNode.name + + '.' + + dimensionNode.columns + .filter(col => + col.attributes.some(attr => attr.attribute_type.name === 'label'), + ) + .map(col => col.name); + + const data = await djClient.nodeData(dimension.metadata.node_name); + /* istanbul ignore if */ + if (dimensionNode && data.results && data.results.length > 0) { + const columnNames = data.results[0].columns.map( + column => column.semantic_entity, + ); + const dimValues = data.results[0].rows.map(row => { + const rowData = { value: '', label: '' }; + row.forEach((value, index) => { + if (columnNames[index] === primaryKey) { + rowData.value = value; + if (rowData.label === '') { + rowData.label = value; + } + } else if (columnNames[index] === label) { + rowData.label = value; + } + }); + return rowData; + }); + setDimensionValues(dimValues); + } + } + }; + fetchData().catch(console.error); + }, [dimension.metadata.node_name, djClient]); + + return ( +
    + {dimension.label.split('[').slice(0)[0]} ( + { + + {dimension.metadata.node_display_name} + + } + ) + { + onChange({ + dimension: dimension.value, + values: event, + }); + }} + /> +
    + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/EditColumnPopover.jsx b/datajunction-ui/src/app/pages/NodePage/EditColumnPopover.jsx new file mode 100644 index 000000000..11de09b84 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/EditColumnPopover.jsx @@ -0,0 +1,116 @@ +import { useContext, useEffect, useRef, useState } from 'react'; +import * as React from 'react'; +import DJClientContext from '../../providers/djclient'; +import { Form, Formik } from 'formik'; +import { FormikSelect } from '../AddEditNodePage/FormikSelect'; +import EditIcon from '../../icons/EditIcon'; +import { displayMessageAfterSubmit, labelize } from '../../../utils/form'; + +export default function EditColumnPopover({ column, node, options, onSubmit }) { + const djClient = useContext(DJClientContext).DataJunctionAPI; + const [popoverAnchor, setPopoverAnchor] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = event => { + if (ref.current && !ref.current.contains(event.target)) { + setPopoverAnchor(false); + } + }; + document.addEventListener('click', handleClickOutside, true); + return () => { + document.removeEventListener('click', handleClickOutside, true); + }; + }, [setPopoverAnchor]); + + const saveAttributes = async ( + { node, column, attributes }, + { setSubmitting, setStatus }, + ) => { + setSubmitting(false); + const response = await djClient.setAttributes(node, column, attributes); + if (response.status === 200 || response.status === 201) { + setStatus({ success: 'Saved!' }); + } else { + setStatus({ + failure: `${response.json.message}`, + }); + } + onSubmit(); + // window.location.reload(); + }; + + return ( + <> + +
    + + {function Render({ isSubmitting, status, setFieldValue }) { + return ( +
    + {displayMessageAfterSubmit(status)} + + { + return { + value: attr.attribute_type.name, + label: labelize(attr.attribute_type.name), + }; + })} + isMulti={true} + /> + + + + +
    + ); + }} +
    +
    + + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/LinkDimensionPopover.jsx b/datajunction-ui/src/app/pages/NodePage/LinkDimensionPopover.jsx new file mode 100644 index 000000000..552e7509b --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/LinkDimensionPopover.jsx @@ -0,0 +1,158 @@ +import { useContext, useEffect, useRef, useState } from 'react'; +import * as React from 'react'; +import DJClientContext from '../../providers/djclient'; +import { Field, Form, Formik } from 'formik'; +import { FormikSelect } from '../AddEditNodePage/FormikSelect'; +import EditIcon from '../../icons/EditIcon'; +import { displayMessageAfterSubmit } from '../../../utils/form'; +import LoadingIcon from '../../icons/LoadingIcon'; + +export default function LinkDimensionPopover({ + column, + referencedDimensionNode, + node, + options, + onSubmit, +}) { + const djClient = useContext(DJClientContext).DataJunctionAPI; + const [popoverAnchor, setPopoverAnchor] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = event => { + if (ref.current && !ref.current.contains(event.target)) { + setPopoverAnchor(false); + } + }; + document.addEventListener('click', handleClickOutside, true); + return () => { + document.removeEventListener('click', handleClickOutside, true); + }; + }, [setPopoverAnchor]); + + const columnDimension = referencedDimensionNode; + + const handleSubmit = async ( + { node, column, dimension }, + { setSubmitting, setStatus }, + ) => { + if (referencedDimensionNode && dimension === 'Remove') { + await unlinkDimension( + node, + column, + referencedDimensionNode, + setStatus, + ).then(_ => setSubmitting(false)); + } else { + await linkDimension(node, column, dimension, setStatus).then(_ => + setSubmitting(false), + ); + } + onSubmit(); + }; + + const linkDimension = async (node, column, dimension, setStatus) => { + const response = await djClient.linkDimension(node, column, dimension); + if (response.status === 200 || response.status === 201) { + setStatus({ success: 'Saved!' }); + } else { + setStatus({ + failure: `${response.json.message}`, + }); + } + }; + + const unlinkDimension = async (node, column, currentDimension, setStatus) => { + const response = await djClient.unlinkDimension( + node, + column, + currentDimension, + ); + if (response.status === 200 || response.status === 201) { + setStatus({ success: 'Removed dimension link!' }); + } else { + setStatus({ + failure: `${response.json.message}`, + }); + } + }; + + return ( + <> + +
    + + {function Render({ isSubmitting, status, setFieldValue }) { + return ( +
    + {displayMessageAfterSubmit(status)} + + + + + + +
    + ); + }} +
    +
    + + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/Loadable.jsx b/datajunction-ui/src/app/pages/NodePage/Loadable.jsx new file mode 100644 index 000000000..1461404fd --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/Loadable.jsx @@ -0,0 +1,16 @@ +/** + * Asynchronously loads the component for the Node page + */ + +import * as React from 'react'; +import { lazyLoad } from '../../../utils/loadable'; + +export const NodePage = props => { + return lazyLoad( + () => import('./index'), + module => module.NodePage, + { + fallback:
    , + }, + )(props); +}; diff --git a/datajunction-ui/src/app/pages/NodePage/MaterializationConfigField.jsx b/datajunction-ui/src/app/pages/NodePage/MaterializationConfigField.jsx new file mode 100644 index 000000000..f506f2410 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/MaterializationConfigField.jsx @@ -0,0 +1,60 @@ +/** + * Materialization configuration field. + */ +import React from 'react'; +import { ErrorMessage, Field, useFormikContext } from 'formik'; +import CodeMirror from '@uiw/react-codemirror'; +import { langs } from '@uiw/codemirror-extensions-langs'; + +export const ConfigField = ({ djClient, value }) => { + const formik = useFormikContext(); + const jsonExt = langs.json(); + + const updateFormik = val => { + formik.setFieldValue('spark_config', JSON.parse(val)); + }; + + return ( +
    +
    + + + + + +
    + { + updateFormik(value); + }} + /> +
    +
    {' '} + <> +
    + ); +}; diff --git a/datajunction-ui/src/app/pages/NodePage/NodeColumnTab.jsx b/datajunction-ui/src/app/pages/NodePage/NodeColumnTab.jsx new file mode 100644 index 000000000..4ebd0e656 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/NodeColumnTab.jsx @@ -0,0 +1,255 @@ +import { useEffect, useState } from 'react'; +import * as React from 'react'; +import EditColumnPopover from './EditColumnPopover'; +import LinkDimensionPopover from './LinkDimensionPopover'; +import { labelize } from '../../../utils/form'; +import PartitionColumnPopover from './PartitionColumnPopover'; +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { foundation } from 'react-syntax-highlighter/src/styles/hljs'; + +export default function NodeColumnTab({ node, djClient }) { + const [attributes, setAttributes] = useState([]); + const [dimensions, setDimensions] = useState([]); + const [columns, setColumns] = useState([]); + const [links, setLinks] = useState([]); + + useEffect(() => { + const fetchData = async () => { + setColumns(await djClient.columns(node)); + }; + fetchData().catch(console.error); + }, [djClient, node]); + + useEffect(() => { + const fetchData = async () => { + const attributes = await djClient.attributes(); + const options = attributes.map(attr => { + return { value: attr.name, label: labelize(attr.name) }; + }); + setAttributes(options); + }; + fetchData().catch(console.error); + }, [djClient]); + + useEffect(() => { + const fetchData = async () => { + const dimensions = await djClient.dimensions(); + const options = dimensions.map(dim => { + return { + value: dim.name, + label: `${dim.name} (${dim.indegree} links)`, + }; + }); + setDimensions(options); + }; + fetchData().catch(console.error); + }, [djClient]); + + const showColumnAttributes = col => { + return col.attributes.map((attr, idx) => ( + + {attr.attribute_type.name.replace(/_/, ' ')} + + )); + }; + + const showColumnPartition = col => { + if (col.partition) { + return ( + <> + + + Type: {col.partition.type_} + +
    + + Format: {col.partition.format} + +
    + + Granularity: {col.partition.granularity} + +
    + + ); + } + return ''; + }; + + const columnList = columns => { + return columns?.map(col => { + const dimensionLinks = (links.length > 0 ? links : node?.dimension_links) + .map(link => [ + link.dimension.name, + Object.entries(link.foreign_keys).filter( + entry => entry[0] === node.name + '.' + col.name, + ), + ]) + .filter(keys => keys[1].length >= 1); + const referencedDimensionNode = + dimensionLinks.length > 0 ? dimensionLinks[0][0] : null; + return ( + + + {col.name} + + + + {col.display_name} + + + + + {col.type} + + + {node.type !== 'cube' ? ( + + {referencedDimensionNode !== null ? ( + + {referencedDimensionNode} + + ) : ( + '' + )} + { + const res = await djClient.node(node.name); + setLinks(res.dimension_links); + }} + /> + + ) : ( + '' + )} + {node.type !== 'cube' ? ( + + {showColumnAttributes(col)} + { + const res = await djClient.node(node.name); + setColumns(res.columns); + }} + /> + + ) : ( + '' + )} + + {showColumnPartition(col)} + { + const res = await djClient.node(node.name); + setColumns(res.columns); + }} + /> + + + ); + }); + }; + + return ( + <> +
    + + + + + + + {node?.type !== 'cube' ? ( + <> + + + + ) : ( + '' + )} + + + + {columnList(columns)} +
    ColumnDisplay NameTypeLinked DimensionAttributesPartition
    +
    +
    +

    Linked Dimensions (Custom Join SQL)

    + + + + + + + + + + + {node?.dimension_links.map(link => { + return ( + + + + + + + ); + })} + +
    Dimension NodeJoin TypeJoin SQLRole
    + + {link.dimension.name} + + {link.join_type.toUpperCase()} + + {link.join_sql} + + {link.role}
    +
    + + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/NodeDependenciesTab.jsx b/datajunction-ui/src/app/pages/NodePage/NodeDependenciesTab.jsx new file mode 100644 index 000000000..d69bd7326 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/NodeDependenciesTab.jsx @@ -0,0 +1,147 @@ +import { useEffect, useState } from 'react'; +import * as React from 'react'; +import { labelize } from '../../../utils/form'; +import LoadingIcon from '../../icons/LoadingIcon'; + +export default function NodeDependenciesTab({ node, djClient }) { + const [nodeDAG, setNodeDAG] = useState({ + upstreams: [], + downstreams: [], + dimensions: [], + }); + + const [retrieved, setRetrieved] = useState(false); + + useEffect(() => { + const fetchData = async () => { + let upstreams = await djClient.upstreams(node.name); + let downstreams = await djClient.downstreams(node.name); + let dimensions = await djClient.nodeDimensions(node.name); + setNodeDAG({ + upstreams: upstreams, + downstreams: downstreams, + dimensions: dimensions, + }); + setRetrieved(true); + }; + fetchData().catch(console.error); + }, [djClient, node]); + + // Builds the block of dimensions selectors, grouped by node name + path + return ( +
    +

    Upstreams

    + {retrieved ? ( + + ) : ( + + + + )} +

    Downstreams

    + {retrieved ? ( + + ) : ( + + + + )} +

    Dimensions

    + {retrieved ? ( + + ) : ( + + + + )} +
    + ); +} + +export function NodeDimensionsList({ rawDimensions }) { + const dimensions = Object.entries( + rawDimensions.reduce((group, dimension) => { + group[dimension.node_name + dimension.path] = + group[dimension.node_name + dimension.path] ?? []; + group[dimension.node_name + dimension.path].push(dimension); + return group; + }, {}), + ); + return ( +
    + {dimensions.map(grouping => { + const dimensionsInGroup = grouping[1]; + const role = dimensionsInGroup[0].path + .map(pathItem => pathItem.split('.').slice(-1)) + .join(' → '); + const fullPath = dimensionsInGroup[0].path.join(' → '); + const groupHeader = ( + + + {dimensionsInGroup[0].node_display_name} + {' '} + with role{' '} + + {role} + {' '} + via {fullPath} + + ); + const dimensionGroupOptions = dimensionsInGroup.map(dim => { + return { + value: dim.name, + label: + labelize(dim.name.split('.').slice(-1)[0]) + + (dim.properties.includes('primary_key') ? ' (PK)' : ''), + }; + }); + return ( +
    + {groupHeader} +
    + {dimensionGroupOptions.map(dimension => { + return ( +
    + {dimension.label.split('[').slice(0)[0]} ⇢{' '} + + {dimension.value} + +
    + ); + })} +
    +
    + ); + })} +
    + ); +} + +export function NodeList({ nodes }) { + return nodes && nodes.length > 0 ? ( +
      + {nodes?.map(node => ( +
    • + + {node.type} + + {node.name} +
    • + ))} +
    + ) : ( + None + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/NodeGraphTab.jsx b/datajunction-ui/src/app/pages/NodePage/NodeGraphTab.jsx new file mode 100644 index 000000000..522ee75d3 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/NodeGraphTab.jsx @@ -0,0 +1,137 @@ +import { useContext } from 'react'; +import { MarkerType } from 'reactflow'; + +import '../../../styles/dag.css'; +import 'reactflow/dist/style.css'; +import DJClientContext from '../../providers/djclient'; +import LayoutFlow from '../../components/djgraph/LayoutFlow'; + +const NodeGraphTab = djNode => { + const djClient = useContext(DJClientContext).DataJunctionAPI; + + const createNode = node => { + const primary_key = node.columns + .filter(col => + col.attributes.some(attr => attr.attribute_type.name === 'primary_key'), + ) + .map(col => col.name); + const dimensionLinkForeignKeys = node.dimension_links + ? node.dimension_links.flatMap(link => + Object.keys(link.foreign_keys).map(key => key.split('.').slice(-1)), + ) + : []; + const column_names = node.columns + .map(col => { + return { + name: col.name, + type: col.type, + dimension: col.dimension !== null ? col.dimension.name : null, + order: primary_key.includes(col.name) + ? -1 + : dimensionLinkForeignKeys.includes(col.name) + ? 0 + : 1, + }; + }) + .sort((a, b) => a.order - b.order); + return { + id: String(node.name), + type: 'DJNode', + data: { + label: + node.table !== null + ? String(node.schema_ + '.' + node.table) + : 'default.' + node.name, + table: node.table, + name: String(node.name), + display_name: String(node.display_name), + type: node.type, + primary_key: primary_key, + column_names: column_names, + is_current: node.name === djNode.djNode.name, + }, + }; + }; + + const dimensionEdges = node => { + return node.dimension_links === undefined + ? [] + : node.dimension_links.flatMap(link => { + return Object.keys(link.foreign_keys).map(fk => { + return { + id: + link.dimension.name + + '->' + + node.name + + '=' + + link.foreign_keys[fk] + + '->' + + fk, + source: link.dimension.name, + sourceHandle: link.foreign_keys[fk], + target: node.name, + targetHandle: fk, + draggable: true, + markerStart: { + type: MarkerType.Arrow, + width: 20, + height: 20, + color: '#b0b9c2', + }, + style: { + strokeWidth: 3, + stroke: '#b0b9c2', + }, + }; + }); + }); + }; + + const parentEdges = node => { + return node.parents + .filter(parent => parent.name) + .map(parent => { + return { + id: node.name + '-' + parent.name, + source: parent.name, + sourceHandle: parent.name, + target: node.name, + targetHandle: node.name, + animated: true, + markerEnd: { + type: MarkerType.Arrow, + }, + style: { + strokeWidth: 3, + stroke: '#b0b9c2', + }, + }; + }); + }; + + const dagFetch = async (getLayoutedElements, setNodes, setEdges) => { + let related_nodes = await djClient.node_dag(djNode.djNode.name); + var djNodes = [djNode.djNode]; + for (const iterable of [related_nodes]) { + for (const item of iterable) { + if (item.type !== 'cube') { + djNodes.push(item); + } + } + } + let edges = []; + djNodes.forEach(node => { + edges = edges.concat(parentEdges(node)); + edges = edges.concat(dimensionEdges(node)); + }); + const nodes = djNodes.map(node => createNode(node)); + + // use dagre to determine the position of the parents (the DJ nodes) + // the positions of the columns are relative to each DJ node + getLayoutedElements(nodes, edges); + setNodes(nodes); + setEdges(edges); + }; + return LayoutFlow(djNode, dagFetch); +}; +export default NodeGraphTab; diff --git a/datajunction-ui/src/app/pages/NodePage/NodeHistory.jsx b/datajunction-ui/src/app/pages/NodePage/NodeHistory.jsx new file mode 100644 index 000000000..a9adf1859 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/NodeHistory.jsx @@ -0,0 +1,216 @@ +import { useEffect, useState } from 'react'; +import * as React from 'react'; +import DiffIcon from '../../icons/DiffIcon'; +import { labelize } from '../../../utils/form'; +import CommitIcon from '../../icons/CommitIcon'; + +export default function NodeHistory({ node, djClient }) { + const [history, setHistory] = useState([]); + + useEffect(() => { + const fetchData = async () => { + if (node) { + const data = await djClient.history('node', node.name); + setHistory(data); + } + }; + fetchData().catch(console.error); + }, [djClient, node]); + + const eventData = event => { + const standard = ( + <> + + Details + + + ); + + if (event.activity_type === 'update' && event.entity_type === 'node') { + return ( + <> + + {event.details.version} + + + Diff + + + ); + } + return ''; + }; + + const eventDescription = event => { + if (event.activity_type === 'create' && event.entity_type === 'node') { + return ( +
    + {event.activity_type}{' '} + {event.entity_type}{' '} + + {event.entity_name} + +
    + ); + } + if ( + ['create', 'update', 'delete'].includes(event.activity_type) && + event.entity_type === 'link' + ) { + return ( +
    + {event.activity_type}{' '} + {event.entity_type} from{' '} + + {event.entity_name} + {' '} + to{' '} + + + {event.details.dimension} + + +
    + ); + } + if (event.activity_type === 'refresh') { + return ( +
    + {event.activity_type}{' '} + {event.entity_type}{' '} + + {event.entity_name} + +
    + ); + } + if (event.activity_type === 'update' && event.entity_type === 'node') { + return ( +
    + {event.activity_type}{' '} + {event.entity_type}{' '} + + {event.entity_name} + +
    + ); + } + if (event.activity_type === 'tag' && event.entity_type === 'node') { + return ( +
    + Add tag{event.details.tags.length > 1 ? 's' : ''}{' '} + {event.details.tags.map(tag => ( + + {tag} + + ))} +
    + ); + } + if (event.activity_type === 'create' && event.entity_type === 'partition') { + return ( +
    + Set {event.details.partition.type_} partition on{' '} + {event.details.column} +
    + ); + } + + if ( + event.activity_type === 'set_attribute' && + event.entity_type === 'column_attribute' + ) { + return ( +
    + Set column attributes on{' '} + + {event.node} + +
    + ); + } + + if (event.entity_type === 'materialization') { + return ( +
    + + {event.activity_type} + {' '} + {event.entity_type}{' '} + + {event.entity_name} + +
    + ); + } + + if ( + event.activity_type === 'status_change' && + event.entity_type === 'node' + ) { + return ( +
    + + {labelize(event.activity_type)} + {' '} + on {event.node} from{' '} + {event.pre.status} to {event.post.status} +
    + ); + } + + if ( + event.activity_type === 'create' && + event.entity_type === 'availability' + ) { + return ( +
    + Materialized table{' '} + + {event.post.catalog}.{event.post.schema_}.{event.post.table} + {' '} + with partitions between {event.post.min_temporal_partition} and{' '} + {event.post.max_temporal_partition} available for{' '} + {event.node}. +
    + ); + } + + if (event.activity_type === 'create' && event.entity_type === 'backfill') { + return ( +
    + Backfill created for materialization{' '} + {event.details.materialization} for partition{' '} + {event.details.partition[0].column_name} from{' '} + {event.details.partition[0].range[0]} to{' '} + {event.details.partition[0].range[1]} +
    + ); + } + }; + + const removeTagNodeEventsWithoutTags = event => + event.activity_type !== 'tag' || + (event.activity_type === 'tag' && + event.entity_type === 'node' && + event.details.tags.length > 0); + + return ( +
      + {history.filter(removeTagNodeEventsWithoutTags).map(event => ( +
    • + {eventDescription(event)} +
      + done by {event.user ? event.user : 'unknown'} on{' '} + {new Date(Date.parse(event.created_at)).toLocaleString()} +
      +
      {eventData(event)}
      +
    • + ))} +
    + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/NodeInfoTab.jsx b/datajunction-ui/src/app/pages/NodePage/NodeInfoTab.jsx new file mode 100644 index 000000000..b6656fd8a --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/NodeInfoTab.jsx @@ -0,0 +1,332 @@ +import { useState, useContext, useEffect } from 'react'; +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { foundation } from 'react-syntax-highlighter/src/styles/hljs'; +import sql from 'react-syntax-highlighter/dist/esm/languages/hljs/sql'; +import NodeStatus from './NodeStatus'; +import ListGroupItem from '../../components/ListGroupItem'; +import ToggleSwitch from '../../components/ToggleSwitch'; +import DJClientContext from '../../providers/djclient'; +import { labelize } from '../../../utils/form'; + +SyntaxHighlighter.registerLanguage('sql', sql); +foundation.hljs['padding'] = '2rem'; + +export default function NodeInfoTab({ node }) { + const [compiledSQL, setCompiledSQL] = useState(''); + const [checked, setChecked] = useState(false); + const nodeTags = node?.tags.map(tag => ( + + )); + const djClient = useContext(DJClientContext).DataJunctionAPI; + + useEffect(() => { + const fetchData = async () => { + if (checked === true) { + const data = await djClient.compiledSql(node.name); + if (data.sql) { + setCompiledSQL(data.sql); + } else { + setCompiledSQL( + '/* Ran into an issue while generating compiled SQL */', + ); + } + } + }; + fetchData().catch(console.error); + }, [node, djClient, checked]); + function toggle(value) { + return !value; + } + const metricsWarning = + node?.type === 'metric' && node?.incompatible_druid_functions.length > 0 ? ( +
    + ⚠{' '} + + The following functions used in the metric definition may not be + compatible with Druid SQL:{' '} + {node?.incompatible_druid_functions.map(func => ( +
  • + ⇢{' '} + + {func} + +
  • + ))} + If you need your metrics to be compatible with Druid, please use{' '} + + these supported functions + + . +
    +
    + ) : ( + '' + ); + + const metricQueryDiv = + node?.type === 'metric' ? ( +
    +
    +
    +
    Upstream Node
    +

    + + {node?.upstream_node} + +

    +
    +
    +
    Aggregate Expression
    + + {node?.expression} + +
    +
    +
    + ) : ( + '' + ); + const queryDiv = node?.query ? ( +
    +
    +
    +
    Query
    + {['metric', 'dimension', 'transform'].indexOf(node?.type) > -1 ? ( + setChecked(toggle)} + toggleName="Show Compiled SQL" + /> + ) : ( + <> + )} + + {checked ? compiledSQL : node?.query} + +
    +
    +
    + ) : ( + <> + ); + + const displayCubeElement = cubeElem => { + return ( + + ); + }; + + const metricMetadataDiv = + node?.type === 'metric' ? ( +
    +
    +
    +
    Direction
    +

    + {node?.metric_metadata?.direction + ? labelize(node?.metric_metadata?.direction?.toLowerCase()) + : 'None'} +

    +
    +
    +
    Unit
    +

    + {node?.metric_metadata?.unit?.name + ? labelize(node?.metric_metadata?.unit?.name?.toLowerCase()) + : 'None'} +

    +
    +
    +
    + ) : ( + '' + ); + + const cubeElementsDiv = node?.cube_elements ? ( +
    +
    +
    +
    Cube Elements
    +
    + {node.cube_elements.map(cubeElem => + cubeElem.type === 'metric' ? displayCubeElement(cubeElem) : '', + )} + {node.cube_elements.map(cubeElem => + cubeElem.type !== 'metric' ? displayCubeElement(cubeElem) : '', + )} +
    +
    +
    +
    + ) : ( + <> + ); + + const primaryKeyOrRequiredDims = ( +
    +
    + {node?.type !== 'metric' ? 'Primary Key' : 'Required Dimensions'} +
    +

    + {node?.type !== 'metric' + ? node?.primary_key?.map(dim => ( + + {dim} + + )) + : node?.required_dimensions?.map(dim => ( + + {dim.name} + + ))} +

    +
    + ); + return ( +
    + {node?.type === 'metric' ? metricsWarning : ''} + +
    +
    +
    +
    Version
    + +

    + + {node?.version} + +

    +
    + {node.type === 'source' ? ( +
    +
    Table
    +

    + {node?.catalog.name}.{node?.schema_}.{node?.table} +

    +
    + ) : ( + <> + )} +
    +
    Status
    +

    + +

    +
    +
    +
    Mode
    +

    + + {node?.mode} + +

    +
    +
    +
    Tags
    +

    + {nodeTags} +

    +
    + {primaryKeyOrRequiredDims} +
    +
    Last Updated
    +

    + {new Date(node?.updated_at).toDateString()} +

    +
    +
    +
    + {metricMetadataDiv} + {node?.type !== 'cube' && node?.type !== 'metric' ? queryDiv : ''} + {node?.type === 'metric' ? metricQueryDiv : ''} + {cubeElementsDiv} +
    + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/NodeLineageTab.jsx b/datajunction-ui/src/app/pages/NodePage/NodeLineageTab.jsx new file mode 100644 index 000000000..f8bb09391 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/NodeLineageTab.jsx @@ -0,0 +1,84 @@ +import { useContext } from 'react'; +import { MarkerType } from 'reactflow'; + +import '../../../styles/dag.css'; +import 'reactflow/dist/style.css'; +import DJClientContext from '../../providers/djclient'; +import LayoutFlow from '../../components/djgraph/LayoutFlow'; + +const createDJNode = node => { + return { + id: node.node_name, + type: 'DJNode', + data: { + label: node.node_name, + name: node.node_name, + type: node.node_type, + table: node.node_type === 'source' ? node.node_name : '', + display_name: + node.node_type === 'source' ? node.node_name : node.display_name, + column_names: [{ name: node.column_name, type: '' }], + primary_key: [], + }, + }; +}; + +const NodeColumnLineage = djNode => { + const djClient = useContext(DJClientContext).DataJunctionAPI; + const dagFetch = async (getLayoutedElements, setNodes, setEdges) => { + let relatedNodes = await djClient.node_lineage(djNode.djNode.name); + let nodesMapping = {}; + let edgesMapping = {}; + let processing = relatedNodes; + while (processing.length > 0) { + let current = processing.pop(); + let node = createDJNode(current); + if (node.id in nodesMapping) { + nodesMapping[node.id].data.column_names = Array.from( + new Set([ + ...nodesMapping[node.id].data.column_names.map(x => x.name), + ...node.data.column_names.map(x => x.name), + ]), + ).map(x => { + return { name: x, type: '' }; + }); + } else { + nodesMapping[node.id] = node; + } + + current.lineage.forEach(lineageColumn => { + const sourceHandle = + lineageColumn.node_name + '.' + lineageColumn.column_name; + const targetHandle = current.node_name + '.' + current.column_name; + edgesMapping[sourceHandle + '->' + targetHandle] = { + id: sourceHandle + '->' + targetHandle, + source: lineageColumn.node_name, + sourceHandle: sourceHandle, + target: current.node_name, + targetHandle: targetHandle, + animated: true, + markerEnd: { + type: MarkerType.Arrow, + }, + style: { + strokeWidth: 3, + stroke: '#b0b9c2', + }, + }; + processing.push(lineageColumn); + }); + } + + // use dagre to determine the position of the parents (the DJ nodes) + // the positions of the columns are relative to each DJ node + const elements = getLayoutedElements( + Object.keys(nodesMapping).map(key => nodesMapping[key]), + Object.keys(edgesMapping).map(key => edgesMapping[key]), + ); + + setNodes(elements.nodes); + setEdges(elements.edges); + }; + return LayoutFlow(djNode, dagFetch); +}; +export default NodeColumnLineage; diff --git a/datajunction-ui/src/app/pages/NodePage/NodeMaterializationTab.jsx b/datajunction-ui/src/app/pages/NodePage/NodeMaterializationTab.jsx new file mode 100644 index 000000000..aab7db45d --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/NodeMaterializationTab.jsx @@ -0,0 +1,330 @@ +import { useEffect, useState } from 'react'; +import TableIcon from '../../icons/TableIcon'; +import AddMaterializationPopover from './AddMaterializationPopover'; +import * as React from 'react'; +import AddBackfillPopover from './AddBackfillPopover'; +import { labelize } from '../../../utils/form'; +import NodeMaterializationDelete from '../../components/NodeMaterializationDelete'; + +const cronstrue = require('cronstrue'); + +export default function NodeMaterializationTab({ node, djClient }) { + const [materializations, setMaterializations] = useState([]); + useEffect(() => { + const fetchData = async () => { + if (node) { + const data = await djClient.materializations(node.name); + setMaterializations(data); + } + }; + fetchData().catch(console.error); + }, [djClient, node]); + + const partitionColumnsMap = node + ? Object.fromEntries( + node?.columns + .filter(col => col.partition !== null) + .map(col => [col.name, col.display_name]), + ) + : {}; + const cron = materialization => { + var parsedCron = ''; + try { + parsedCron = cronstrue.toString(materialization.schedule); + } catch (e) {} + return parsedCron; + }; + + const materializationRows = materializations => { + return materializations.map(materialization => ( + <> +
    +
    +
    + {materialization.job + ?.replace('MaterializationJob', '') + .match(/[A-Z][a-z]+/g) + .join(' ')} +
    +
    + +
    +
    + {materialization.schedule} +
    {cron(materialization)}
    +
    +
    + + {labelize(materialization.strategy)} + +
    +
    +
    +
    +
    +
      +
    • +
      Output Tables
      {' '} + {materialization.output_tables.map(table => ( +
      +
      + {' '} + + {table.split('.')[0] + '.' + table.split('.')[1]} + +
      +
      + {table.split('.')[2]} +
      +
      + ))} +
    • +
    +
    + +
    + +
    + + +
    +
      +
    • +
      Partitions
      {' '} +
        + {node.columns + .filter(col => col.partition !== null) + .map(column => { + return ( +
      • +
        + {column.display_name} + + {column.partition.type_} + +
        +
      • + ); + })} +
      +
    • +
    +
    +
    + + )); + }; + return ( + <> +
    +
    +

    Materializations

    + {node ? : <>} + {materializations.length > 0 ? ( +
    +
    + {materializationRows( + materializations.filter( + materialization => + !( + materialization.name === 'default' && + node.type === 'cube' + ), + ), + )} +
    +
    + ) : ( +
    + No materialization workflows configured for this node. +
    + )} +
    +
    +

    Materialized Datasets

    + {node && node.availability !== null ? ( + + + + + + + + + + + + + + + + + +
    Output DatasetValid ThroughPartitionsLinks
    + { +
    +
    + {' '} + + {node.availability.catalog + + '.' + + node.availability.schema_} + +
    + +
    + } +
    + {new Date(node.availability.valid_through_ts).toISOString()} + + + + {node.availability.min_temporal_partition} + + to + + {node.availability.max_temporal_partition} + + + + {node.availability.links !== null ? ( + Object.entries(node.availability.links).map( + ([key, value]) => ( + + ), + ) + ) : ( + <> + )} +
    + ) : ( +
    + No materialized datasets available for this node. +
    + )} +
    +
    + + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/NodeStatus.jsx b/datajunction-ui/src/app/pages/NodePage/NodeStatus.jsx new file mode 100644 index 000000000..e4387f7fb --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/NodeStatus.jsx @@ -0,0 +1,103 @@ +import { Component, useContext, useEffect, useRef, useState } from 'react'; +import ValidIcon from '../../icons/ValidIcon'; +import InvalidIcon from '../../icons/InvalidIcon'; +import DJClientContext from '../../providers/djclient'; +import { foundation } from 'react-syntax-highlighter/src/styles/hljs'; +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; +import markdown from 'react-syntax-highlighter/dist/cjs/languages/hljs/markdown'; +import * as React from 'react'; +import { AlertMessage } from '../AddEditNodePage/AlertMessage'; +import AlertIcon from '../../icons/AlertIcon'; +import { labelize } from '../../../utils/form'; + +SyntaxHighlighter.registerLanguage('markdown', markdown); + +export default function NodeStatus({ node, revalidate = true }) { + const MAX_ERROR_LENGTH = 200; + const djClient = useContext(DJClientContext).DataJunctionAPI; + const [validation, setValidation] = useState([]); + + const [codeAnchor, setCodeAnchor] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (revalidate) { + const fetchData = async () => { + setValidation(await djClient.revalidate(node.name)); + }; + fetchData().catch(console.error); + } + }, [djClient, node, revalidate]); + + const displayValidation = + revalidate && validation?.errors?.length > 0 ? ( + <> + +
    + {validation?.errors?.map((error, idx) => ( +
    + + {labelize(error.type.toLowerCase())}: + {' '} + {error.message.length > MAX_ERROR_LENGTH + ? error.message.slice(0, MAX_ERROR_LENGTH - 1) + '...' + : error.message} +
    + ))} +
    + + ) : ( + <> + ); + + return ( + <> + {revalidate && validation?.errors?.length > 0 ? ( + displayValidation + ) : validation?.status === 'valid' || + node?.status === 'valid' || + node?.current?.status === 'VALID' ? ( + + + + ) : ( + + + + )} + + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/NodeValidateTab.jsx b/datajunction-ui/src/app/pages/NodePage/NodeValidateTab.jsx new file mode 100644 index 000000000..6434fc9da --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/NodeValidateTab.jsx @@ -0,0 +1,367 @@ +import React, { useEffect, useState } from 'react'; +import Select from 'react-select'; +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { foundation } from 'react-syntax-highlighter/src/styles/hljs'; +import { labelize } from '../../../utils/form'; +import { Form, Formik } from 'formik'; +import DimensionFilter from './DimensionFilter'; +import QueryInfo from '../../components/QueryInfo'; +import LoadingIcon from '../../icons/LoadingIcon'; + +export default function NodeValidateTab({ node, djClient }) { + const [query, setQuery] = useState(''); + const [lookup, setLookup] = useState([]); + const [running, setRunning] = useState(false); + + // These are a list of dimensions that are available for this node + const [dimensions, setDimensions] = useState([]); + + // A separate structure used to store the selected dimensions to filter by and their values + const [selectedFilters, setSelectedFilters] = useState({}); + + // The set of dimensions and filters to pass to the /sql or /data endpoints for the node + const [selection, setSelection] = useState({ + dimensions: [], + filters: [], + }); + + // Any query result info retrieved when a node query is run + const [queryInfo, setQueryInfo] = useState(null); + + const initialValues = {}; + + const [state, setState] = useState({ + selectedTab: 'results', + }); + + const switchTab = tabName => { + setState({ selectedTab: tabName }); + }; + + useEffect(() => { + const fetchData = async () => { + if (node) { + // Find all the dimensions for this node + const dimensions = await djClient.nodeDimensions(node.name); + + // Create a dimensions lookup object + const lookup = {}; + dimensions.forEach(dimension => { + lookup[dimension.name] = dimension; + }); + setLookup(lookup); + + // Group the dimensions by dimension node + const grouped = Object.entries( + dimensions.reduce((group, dimension) => { + group[dimension.node_name + dimension.path] = + group[dimension.node_name + dimension.path] ?? []; + group[dimension.node_name + dimension.path].push(dimension); + return group; + }, {}), + ); + setDimensions(grouped); + + // Build the query for this node based on the user-provided dimensions and filters + const query = await djClient.sql(node.name, selection); + setQuery(query.sql); + } + }; + fetchData().catch(console.error); + }, [djClient, node, selection]); + + const dimensionsList = dimensions.flatMap(grouping => { + const dimensionsInGroup = grouping[1]; + return dimensionsInGroup + .filter(dim => dim.properties.includes('primary_key') === true) + .map(dim => { + return { + value: dim.name, + label: ( + + {labelize(dim.name.split('.').slice(-1)[0])}{' '} + {dim.name} + + ), + }; + }); + }); + + // Run the query and use SSE to stream the status of the query execution results + const runQuery = async (values, setStatus, setSubmitting) => { + setRunning(true); + const sse = await djClient.streamNodeData(node?.name, selection); + sse.onmessage = e => { + const messageData = JSON.parse(JSON.parse(e.data)); + if ( + messageData !== null && + messageData?.state !== 'FINISHED' && + messageData?.state !== 'CANCELED' && + messageData?.state !== 'FAILED' + ) { + setRunning(false); + } + if (messageData.results && messageData.results?.length > 0) { + messageData.numRows = messageData.results?.length + ? messageData.results[0].rows.length + : []; + switchTab('results'); + setRunning(false); + } else { + switchTab('info'); + } + setQueryInfo(messageData); + }; + sse.onerror = () => sse.close(); + }; + + // Handle form submission (runs the query) + const handleSubmit = async (values, { setSubmitting, setStatus }) => { + await runQuery(values, setStatus, setSubmitting); + }; + + // Handle when filter values are updated. This is available for all nodes. + const handleAddFilters = event => { + const updatedFilters = selectedFilters; + if (event.dimension in updatedFilters) { + updatedFilters[event.dimension].operator = event.operator; + updatedFilters[event.dimension].values = event.values; + } else { + updatedFilters[event.dimension] = { + operator: event.operator, + values: event.values, + }; + } + setSelectedFilters(updatedFilters); + const updatedDimensions = selection.dimensions.concat([event.dimension]); + setSelection({ + filters: Object.entries(updatedFilters).map(obj => + obj[1].values + ? `${obj[0]} IN (${obj[1].values + .map(val => + ['int', 'bigint', 'float', 'double', 'long'].includes( + lookup[obj[0]].type, + ) + ? val.value + : "'" + val.value + "'", + ) + .join(', ')})` + : '', + ), + dimensions: updatedDimensions, + }); + }; + + // Handle when one or more dimensions are selected from the dropdown + // Note that this is only available to metrics + const handleAddDimensions = event => { + const updatedDimensions = event.map( + selectedDimension => selectedDimension.value, + ); + setSelection({ + filters: selection.filters, + dimensions: updatedDimensions, + }); + }; + + const filters = dimensions.map(grouping => { + const dimensionsInGroup = grouping[1]; + const dimensionGroupOptions = dimensionsInGroup + .filter(dim => dim.properties.includes('primary_key') === true) + .map(dim => { + return { + value: dim.name, + label: labelize(dim.name.split('.').slice(-1)[0]), + metadata: dim, + }; + }); + return ( + <> +
    + {dimensionGroupOptions.map(dimension => { + return ( + + ); + })} +
    + + ); + }); + + return ( + + {function Render({ isSubmitting, status, setFieldValue }) { + return ( +
    +
    +
    + {node?.type === 'metric' ? ( + <> + + + + + +
    +
    + {values.partition_type === 'temporal' ? ( + <> + + +
    +
    + + + + + + + ) : ( + '' + )} + + + ); + }} + +
    + + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/PartitionValueForm.jsx b/datajunction-ui/src/app/pages/NodePage/PartitionValueForm.jsx new file mode 100644 index 000000000..15ab353eb --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/PartitionValueForm.jsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { Field } from 'formik'; + +export default function PartitionValueForm({ col, materialization }) { + if (col.partition.type_ === 'temporal') { + return ( + <> +
    +
    {col.display_name}
    +
    + From{' '} + {' '} + To + +
    +
    + + ); + } else { + return ( + <> +
    +
    {col.display_name}
    +
    + +
    +
    + + ); + } +} diff --git a/datajunction-ui/src/app/pages/NodePage/RevisionDiff.jsx b/datajunction-ui/src/app/pages/NodePage/RevisionDiff.jsx new file mode 100644 index 000000000..e207e500b --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/RevisionDiff.jsx @@ -0,0 +1,209 @@ +import { useContext, useEffect, useState } from 'react'; +import * as React from 'react'; +import { diffLines, formatLines } from 'unidiff'; +import { parseDiff, Diff, Hunk } from 'react-diff-view'; + +import { useParams } from 'react-router-dom'; +import DJClientContext from '../../providers/djclient'; +import NamespaceHeader from '../../components/NamespaceHeader'; +import { labelize } from '../../../utils/form'; +import DiffIcon from '../../icons/DiffIcon'; +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { foundation } from 'react-syntax-highlighter/src/styles/hljs'; +import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; + +SyntaxHighlighter.registerLanguage('sql', sql); +foundation.hljs['padding'] = '2rem'; + +export default function RevisionDiff() { + const djClient = useContext(DJClientContext).DataJunctionAPI; + const [revisions, setRevisions] = useState([]); + + const { name, revision } = useParams(); + + useEffect(() => { + const fetchData = async () => { + const revisions = await djClient.revisions(name); + setRevisions(revisions); + }; + fetchData().catch(console.error); + }, [djClient, name]); + + const thisRevision = revisions + .map((rev, idx) => [idx, rev]) + .filter((rev, idx) => { + return rev[1].version === revision; + }); + const prevRevision = revisions.filter( + (rev, idx) => idx + 1 === thisRevision[0][0], + ); + + const EMPTY_HUNKS = []; + + const revisionDiff = (older, newer) => { + const diffObj = {}; + const fields = [ + 'display_name', + 'version', + 'query', + 'mode', + 'status', + 'description', + 'columns', + // 'catalog', + 'schema', + 'table', + 'updated_at', + ]; + if (older) { + for (const key of fields) { + if (older[key] && (key !== 'columns' || older.type === 'cube')) { + diffObj[key] = {}; + diffObj[key].diffText = formatLines( + diffLines( + older + ? key === 'columns' + ? older[key].map(col => col.name).join('\n') + : older[key].toString() + : '', + newer + ? key === 'columns' + ? newer[key].map(col => col.name).join('\n') + : newer[key].toString() + : '', + ), + { + context: 5000, + }, + ); + const [diff] = parseDiff(diffObj[key].diffText, { + nearbySequences: 'zip', + }); + diffObj[key].diff = diff; + } + } + } + return diffObj; + }; + + const diffObjects = revisionDiff( + prevRevision[0], + thisRevision[0] ? thisRevision[0][1] : thisRevision[0], + ); + + return ( +
    + +
    +
    +

    + + + {prevRevision[0]?.name} + {' '} + + {prevRevision[0]?.type} + + +

    +
    + + {prevRevision[0]?.version} + + + + {thisRevision[0] ? thisRevision[0][1].version : ''} + {' '} +
    + {Object.keys(diffObjects).map(field => { + return ( +
    +

    + {labelize(field)}{' '} + + {diffObjects[field]?.diff.hunks.length > 0 + ? '' + : 'no change'} + +

    + {diffObjects[field]?.diff.hunks.length > 0 ? ( + + {hunks => + hunks.map(hunk => ) + } + + ) : ( +
    + {prevRevision[0] ? ( + field === 'query' ? ( + <> + + {prevRevision[0].query} + + + ) : field === 'columns' ? ( +
    + {prevRevision[0][field].map(col => ( + <> + {col.name} +
    + + ))} +
    + ) : ( + prevRevision[0][field].toString() + ) + ) : ( + '' + )} +
    + )} +
    + ); + })} +
    +
    +
    + ); +} diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx new file mode 100644 index 000000000..9dcc85a49 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import DJClientContext from '../../../providers/djclient'; +import AddBackfillPopover from '../AddBackfillPopover'; +import { mocks } from '../../../../mocks/mockNodes'; + +const mockDjClient = { + DataJunctionAPI: { + runBackfill: jest.fn(), + }, +}; + +let reloadMock = jest.fn(); + +beforeEach(() => { + delete window.location; + window.location = { reload: reloadMock }; +}); + +afterEach(() => { + reloadMock.mockClear(); +}); + +describe('', () => { + it('renders correctly and handles form submission', async () => { + // Mock onSubmit function + const onSubmitMock = jest.fn(); + + mockDjClient.DataJunctionAPI.runBackfill.mockReturnValue({ + status: 201, + json: { message: '' }, + }); + + // Render the component + const { getByLabelText, getByText } = render( + + + , + ); + + // Open the popover + fireEvent.click(getByLabelText('AddBackfill')); + + fireEvent.click(getByText('Save')); + + // Expect setAttributes to be called + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.runBackfill).toHaveBeenCalled(); + expect(getByText('Saved!')).toBeInTheDocument(); + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx new file mode 100644 index 000000000..9285ebdd2 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { render, fireEvent, waitFor, screen } from '@testing-library/react'; +import DJClientContext from '../../../providers/djclient'; +import AddMaterializationPopover from '../AddMaterializationPopover'; +import { mocks } from '../../../../mocks/mockNodes'; + +const mockDjClient = { + DataJunctionAPI: { + materialize: jest.fn(), + materializationInfo: jest.fn(), + }, +}; + +describe('', () => { + it('renders correctly and handles form submission', async () => { + // Mock onSubmit function + const onSubmitMock = jest.fn(); + mockDjClient.DataJunctionAPI.materialize.mockReturnValue({ + status: 201, + json: { + message: 'Saved!', + }, + }); + mockDjClient.DataJunctionAPI.materializationInfo.mockReturnValue({ + status: 200, + json: { + job_types: [ + { + name: 'spark_sql', + label: 'Spark SQL', + description: 'Spark SQL materialization job', + allowed_node_types: ['transform', 'dimension', 'cube'], + job_class: 'SparkSqlMaterializationJob', + }, + { + name: 'druid_metrics_cube', + label: 'Druid Cube', + description: + 'Used to materialize a cube to Druid for low-latency access to a set of metrics and dimensions. While the logical cube definition is at the level of metrics and dimensions, a materialized Druid cube will reference measures and dimensions, with rollup configured on the measures where appropriate.', + allowed_node_types: ['cube'], + job_class: 'DruidCubeMaterializationJob', + }, + ], + strategies: [ + { + name: 'full', + label: 'Full', + }, + { + name: 'snapshot', + label: 'Snapshot', + }, + { + name: 'incremental_time', + label: 'Incremental Time', + }, + { + name: 'view', + label: 'View', + }, + ], + }, + }); + + // Render the component + const { getByText } = render( + + + , + ); + + // Open the popover + fireEvent.click(getByText('+ Add Materialization')); + + // Save the materialization + fireEvent.click(getByText('Save')); + + // Expect setAttributes to be called + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.materialize).toHaveBeenCalled(); + expect(getByText('Saved!')).toBeInTheDocument(); + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx new file mode 100644 index 000000000..5368a4d71 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import ClientCodePopover from '../ClientCodePopover'; +import userEvent from '@testing-library/user-event'; + +describe('', () => { + const defaultProps = { + code: "print('Hello, World!')", + }; + + it('toggles the code popover visibility when the button is clicked', async () => { + render(); + + const button = screen.getByRole('button', 'code-button'); + + // Initially, the popover should be hidden + expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle( + 'display: none', + ); + + // Clicking the button should display the popover + fireEvent.click(button); + expect(screen.getByRole('dialog', { hidden: true })).not.toHaveStyle( + 'display: none', + ); + + // Clicking the button again should hide the popover + fireEvent.click(button); + expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle( + 'display: none', + ); + + // Trigger onClose by pressing + userEvent.keyboard('{Escape}'); + // fireEvent.click(screen.getByTestId('body').firstChild()); + await waitFor(() => { + expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle( + 'display: none', + ); + }); + }); + + it('renders the provided code within the SyntaxHighlighter', () => { + render(); + expect(screen.getByRole('dialog', { hidden: true })).toHaveTextContent( + defaultProps.code, + ); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx new file mode 100644 index 000000000..4a14ea31a --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import DJClientContext from '../../../providers/djclient'; +import DimensionFilter from '../DimensionFilter'; + +// Mock DJClientContext +const mockDJClient = { + DataJunctionAPI: { + node: jest.fn(), + nodeData: jest.fn(), + }, +}; + +const mockDimension = { + label: 'Dimension Label [Test]', + value: 'dimension_value', + metadata: { + node_name: 'test_node', + node_display_name: 'Test Node', + }, +}; + +const getByTextStartsWith = (container, text) => { + return Array.from(container.querySelectorAll('*')).find(element => { + return element.textContent.trim().startsWith(text); + }); +}; + +describe('DimensionFilter', () => { + it('fetches dimension data and renders correctly', async () => { + // Mock node response + const mockNodeResponse = { + type: 'dimension', + name: 'test_node', + columns: [ + { + name: 'id', + attributes: [{ attribute_type: { name: 'primary_key' } }], + }, + { name: 'name', attributes: [{ attribute_type: { name: 'label' } }] }, + ], + }; + mockDJClient.DataJunctionAPI.node.mockResolvedValue(mockNodeResponse); + + // Mock node data response + const mockNodeDataResponse = { + results: [ + { + columns: [{ semantic_entity: 'id' }, { semantic_entity: 'name' }], + rows: [ + [1, 'Value 1'], + [2, 'Value 2'], + ], + }, + ], + }; + mockDJClient.DataJunctionAPI.nodeData.mockResolvedValue( + mockNodeDataResponse, + ); + + const { container } = render( + + + , + ); + + // Check if the dimension label and node display name are rendered + expect( + getByTextStartsWith(container, 'Dimension Label'), + ).toBeInTheDocument(); + expect(screen.getByText('Test Node')).toBeInTheDocument(); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx new file mode 100644 index 000000000..f649d1784 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { render, fireEvent, waitFor, screen } from '@testing-library/react'; +import EditColumnPopover from '../EditColumnPopover'; +import DJClientContext from '../../../providers/djclient'; + +const mockDjClient = { + DataJunctionAPI: { + setAttributes: jest.fn(), + }, +}; + +describe('', () => { + it('renders correctly and handles form submission', async () => { + // Mock necessary data + const column = { + name: 'column1', + dimension: { name: 'dimension1' }, + attributes: [ + { attribute_type: { name: 'primary_key', namespace: 'system' } }, + ], + }; + const node = { name: 'default.node1' }; + const options = [ + { value: 'dimension', label: 'Dimension' }, + { value: 'primary_key', label: 'Primary Key' }, + ]; + + // Mock onSubmit function + const onSubmitMock = jest.fn(); + + mockDjClient.DataJunctionAPI.setAttributes.mockReturnValue({ + status: 201, + json: [ + { + name: 'id', + type: 'int', + attributes: [ + { attribute_type: { name: 'primary_key', namespace: 'system' } }, + ], + dimension: null, + }, + ], + }); + + // Render the component + const { getByLabelText, getByText, getByTestId } = render( + + + , + ); + + // Open the popover + fireEvent.click(getByLabelText('EditColumn')); + + // Click on one attribute in the select + const editAttributes = getByTestId('edit-attributes'); + fireEvent.keyDown(editAttributes.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('Dimension')); + fireEvent.click(getByText('Save')); + getByText('Save').click(); + + // Expect setAttributes to be called + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.setAttributes).toHaveBeenCalled(); + expect(getByText('Saved!')).toBeInTheDocument(); + }); + + // Click on two attributes in the select + fireEvent.keyDown(editAttributes.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('Dimension')); + fireEvent.click(screen.getByText('Primary Key')); + fireEvent.click(getByText('Save')); + getByText('Save').click(); + + // Expect setAttributes to be called + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.setAttributes).toHaveBeenCalledWith( + 'default.node1', + 'column1', + ['primary_key', 'dimension'], + ); + expect(getByText('Saved!')).toBeInTheDocument(); + }); + + // Close the popover + fireEvent.click(getByLabelText('EditColumn')); + }); + + it('handles failed form submission', async () => { + // Mock necessary data + const column = { + name: 'column1', + dimension: { name: 'dimension1' }, + attributes: [ + { attribute_type: { name: 'primary_key', namespace: 'system' } }, + ], + }; + const node = { name: 'default.node1' }; + const options = [ + { value: 'dimension', label: 'Dimension' }, + { value: 'primary_key', label: 'Primary Key' }, + ]; + + // Mock onSubmit function + const onSubmitMock = jest.fn(); + + mockDjClient.DataJunctionAPI.setAttributes.mockReturnValue({ + status: 500, + json: { message: 'bad request' }, + }); + + // Render the component + const { getByLabelText, getByText, getByTestId } = render( + + + , + ); + + // Open the popover + fireEvent.click(getByLabelText('EditColumn')); + + // Click on one attribute in the select + const editAttributes = getByTestId('edit-attributes'); + fireEvent.keyDown(editAttributes.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('Dimension')); + fireEvent.click(getByText('Save')); + getByText('Save').click(); + + // Expect setAttributes to be called and the failure message to show up + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.setAttributes).toHaveBeenCalled(); + expect(getByText('bad request')).toBeInTheDocument(); + }); + + // Close the popover + fireEvent.click(getByLabelText('EditColumn')); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx new file mode 100644 index 000000000..a6f2425a6 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { render, fireEvent, waitFor, screen } from '@testing-library/react'; +import LinkDimensionPopover from '../LinkDimensionPopover'; +import DJClientContext from '../../../providers/djclient'; + +const mockDjClient = { + DataJunctionAPI: { + linkDimension: jest.fn(), + unlinkDimension: jest.fn(), + }, +}; + +describe('', () => { + it('renders correctly and handles form submission', async () => { + // Mock necessary data + const column = { + name: 'column1', + dimension: { name: 'default.dimension1' }, + }; + const node = { name: 'default.node1' }; + const options = [ + { value: 'Remove', label: '[Remove dimension link]' }, + { value: 'default.dimension1', label: 'Dimension 1' }, + { value: 'default.dimension2', label: 'Dimension 2' }, + ]; + + // Mock onSubmit function + const onSubmitMock = jest.fn(); + + mockDjClient.DataJunctionAPI.linkDimension.mockReturnValue({ + status: 200, + json: { message: 'Success' }, + }); + + mockDjClient.DataJunctionAPI.unlinkDimension.mockReturnValue({ + status: 200, + json: { message: 'Success' }, + }); + + // Render the component + const { getByLabelText, getByText, getByTestId } = render( + + + , + ); + + // Open the popover + fireEvent.click(getByLabelText('LinkDimension')); + + // Click on a dimension and save + const linkDimension = getByTestId('link-dimension'); + fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('Dimension 1')); + fireEvent.click(getByText('Save')); + + // Expect linkDimension to be called + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.linkDimension).toHaveBeenCalledWith( + 'default.node1', + 'column1', + 'default.dimension1', + ); + expect(getByText('Saved!')).toBeInTheDocument(); + }); + + // Click on the 'Remove' option and save + fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('[Remove dimension link]')); + fireEvent.click(getByText('Save')); + + // Expect unlinkDimension to be called + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.unlinkDimension).toHaveBeenCalledWith( + 'default.node1', + 'column1', + 'default.dimension1', + ); + expect(getByText('Removed dimension link!')).toBeInTheDocument(); + }); + }); + + it('handles failed form submission', async () => { + // Mock necessary data + const column = { + name: 'column1', + dimension: { name: 'default.dimension1' }, + }; + const node = { name: 'default.node1' }; + const options = [ + { value: 'default.dimension1', label: 'Dimension 1' }, + { value: 'default.dimension2', label: 'Dimension 2' }, + ]; + + // Mock onSubmit function + const onSubmitMock = jest.fn(); + + mockDjClient.DataJunctionAPI.linkDimension.mockReturnValue({ + status: 500, + json: { message: 'Failed due to nonexistent dimension' }, + }); + + mockDjClient.DataJunctionAPI.unlinkDimension.mockReturnValue({ + status: 500, + json: { message: 'Failed due to no dimension link' }, + }); + + // Render the component + const { getByLabelText, getByText, getByTestId } = render( + + + , + ); + + // Open the popover + fireEvent.click(getByLabelText('LinkDimension')); + + // Click on a dimension and save + const linkDimension = getByTestId('link-dimension'); + fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('Dimension 1')); + fireEvent.click(getByText('Save')); + + // Expect linkDimension to be called + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.linkDimension).toHaveBeenCalledWith( + 'default.node1', + 'column1', + 'default.dimension1', + ); + expect( + getByText('Failed due to nonexistent dimension'), + ).toBeInTheDocument(); + }); + + // Click on the 'Remove' option and save + fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('[Remove Dimension]')); + fireEvent.click(getByText('Save')); + + // Expect unlinkDimension to be called + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.unlinkDimension).toHaveBeenCalledWith( + 'default.node1', + 'column1', + 'default.dimension1', + ); + expect(getByText('Failed due to no dimension link')).toBeInTheDocument(); + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx new file mode 100644 index 000000000..95be139ea --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx @@ -0,0 +1,166 @@ +import React from 'react'; +import { render, waitFor, screen } from '@testing-library/react'; +import NodeColumnTab from '../NodeColumnTab'; + +describe('', () => { + const mockDjClient = { + node: jest.fn(), + columns: jest.fn(), + attributes: jest.fn(), + dimensions: jest.fn(), + }; + + const mockNodeColumns = [ + { + name: 'repair_order_id', + display_name: 'Repair Order Id', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'municipality_id', + display_name: 'Municipality Id', + type: 'string', + attributes: [], + dimension: null, + }, + { + name: 'hard_hat_id', + display_name: 'Hard Hat Id', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'order_date', + display_name: 'Order Date', + type: 'date', + attributes: [], + dimension: null, + }, + { + name: 'required_date', + display_name: 'Required Date', + type: 'date', + attributes: [], + dimension: null, + }, + { + name: 'dispatched_date', + display_name: 'Dispatched Date', + type: 'date', + attributes: [], + dimension: null, + }, + { + name: 'dispatcher_id', + display_name: 'Dispatcher Id', + type: 'int', + attributes: [], + dimension: null, + }, + ]; + + const mockNode = { + node_revision_id: 1, + node_id: 1, + type: 'source', + name: 'default.repair_orders', + display_name: 'Default: Repair Orders', + version: 'v1.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + schema_: 'roads', + table: 'repair_orders', + description: 'Repair orders', + query: null, + availability: null, + columns: mockNodeColumns, + updated_at: '2023-08-21T16:48:52.880498+00:00', + materializations: [], + parents: [], + dimension_links: [ + { + dimension: { + name: 'default.contractor', + }, + join_type: 'left', + join_sql: + 'default.contractor.contractor_id = default.repair_orders.contractor_id', + join_cardinality: 'one_to_one', + role: 'contractor', + foreign_keys: { + 'default.repair_orders.contractor_id': + 'default.contractor.contractor_id', + }, + }, + ], + }; + + const mockAttributes = [ + { + uniqueness_scope: [], + namespace: 'system', + name: 'primary_key', + description: + 'Points to a column which is part of the primary key of the node', + allowed_node_types: ['source', 'transform', 'dimension'], + id: 1, + }, + { + uniqueness_scope: [], + namespace: 'system', + name: 'dimension', + description: 'Points to a dimension attribute column', + allowed_node_types: ['source', 'transform'], + id: 2, + }, + ]; + + const mockDimensions = ['default.contractor', 'default.hard_hat']; + + beforeEach(() => { + // Reset the mocks before each test + mockDjClient.node.mockReset(); + mockDjClient.columns.mockReset(); + mockDjClient.attributes.mockReset(); + mockDjClient.dimensions.mockReset(); + }); + + it('renders node columns and dimension links', async () => { + mockDjClient.node.mockReturnValue(mockNode); + mockDjClient.columns.mockReturnValue(mockNodeColumns); + mockDjClient.attributes.mockReturnValue(mockAttributes); + mockDjClient.dimensions.mockReturnValue(mockDimensions); + + render(); + + await waitFor(() => { + // Displays the columns + for (const column of mockNode.columns) { + expect(screen.getByText(column.name)).toBeInTheDocument(); + expect(screen.getByText(column.display_name)).toBeInTheDocument(); + } + + // Displays the dimension links + for (const dimensionLink of mockNode.dimension_links) { + const link = screen + .getByText(dimensionLink.dimension.name) + .closest('a'); + expect(link).toHaveAttribute( + 'href', + `/nodes/${dimensionLink.dimension.name}`, + ); + } + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx new file mode 100644 index 000000000..278e4f95f --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx @@ -0,0 +1,151 @@ +import React from 'react'; +import { render, waitFor, screen } from '@testing-library/react'; +import NodeDependenciesTab from '../NodeDependenciesTab'; + +describe('', () => { + const mockDjClient = { + node: jest.fn(), + nodeDimensions: jest.fn(), + upstreams: jest.fn(), + downstreams: jest.fn(), + }; + + const mockNode = { + node_revision_id: 1, + node_id: 1, + type: 'source', + name: 'default.repair_orders', + display_name: 'Default: Repair Orders', + version: 'v1.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + schema_: 'roads', + table: 'repair_orders', + description: 'Repair orders', + query: null, + availability: null, + columns: [ + { + name: 'repair_order_id', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'municipality_id', + type: 'string', + attributes: [], + dimension: null, + }, + { + name: 'hard_hat_id', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'order_date', + type: 'date', + attributes: [], + dimension: null, + }, + { + name: 'required_date', + type: 'date', + attributes: [], + dimension: null, + }, + { + name: 'dispatched_date', + type: 'date', + attributes: [], + dimension: null, + }, + { + name: 'dispatcher_id', + type: 'int', + attributes: [], + dimension: null, + }, + ], + updated_at: '2023-08-21T16:48:52.880498+00:00', + materializations: [], + parents: [], + dimension_links: [ + { + dimension: { + name: 'default.contractor', + }, + join_type: 'left', + join_sql: + 'default.contractor.contractor_id = default.repair_orders.contractor_id', + join_cardinality: 'one_to_one', + role: 'contractor', + }, + ], + }; + + const mockDimensions = [ + { + properties: [], + name: 'default.dispatcher.company_name', + node_display_name: 'Default: Dispatcher', + node_name: 'default.dispatcher', + path: ['default.repair_orders_fact.dispatcher_id'], + type: 'string', + }, + { + properties: ['primary_key'], + name: 'default.dispatcher.dispatcher_id', + node_display_name: 'Default: Dispatcher', + node_name: 'default.dispatcher', + path: ['default.repair_orders_fact.dispatcher_id'], + type: 'int', + }, + { + properties: [], + name: 'default.hard_hat.city', + node_display_name: 'Default: Hard Hat', + node_name: 'default.hard_hat', + path: ['default.repair_orders_fact.hard_hat_id'], + type: 'string', + }, + { + properties: ['primary_key'], + name: 'default.hard_hat.hard_hat_id', + node_display_name: 'Default: Hard Hat', + node_name: 'default.hard_hat', + path: ['default.repair_orders_fact.hard_hat_id'], + type: 'int', + }, + ]; + + beforeEach(() => { + // Reset the mocks before each test + mockDjClient.nodeDimensions.mockReset(); + mockDjClient.upstreams.mockReset(); + mockDjClient.downstreams.mockReset(); + }); + + it('renders nodes with dimensions', async () => { + mockDjClient.nodeDimensions.mockReturnValue(mockDimensions); + mockDjClient.upstreams.mockReturnValue([mockNode]); + mockDjClient.downstreams.mockReturnValue([mockNode]); + render(); + await waitFor(() => { + for (const dimension of mockDimensions) { + const link = screen.getByText(dimension.node_display_name).closest('a'); + expect(link).toHaveAttribute('href', `/nodes/${dimension.node_name}`); + expect(screen.getByText(dimension.name)).toBeInTheDocument(); + } + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx new file mode 100644 index 000000000..819f2e9c4 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx @@ -0,0 +1,595 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import NodeGraphTab from '../NodeGraphTab'; +import DJClientContext from '../../../providers/djclient'; + +describe('', () => { + const domTestingLib = require('@testing-library/dom'); + const { queryHelpers } = domTestingLib; + + const queryByAttribute = attribute => + queryHelpers.queryAllByAttribute.bind(null, attribute); + + const mockNodeDAG = [ + { + namespace: 'default', + node_revision_id: 2, + node_id: 2, + type: 'source', + name: 'default.repair_order_details', + display_name: 'Default: Repair Order Details', + version: 'v1.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + schema_: 'roads', + table: 'repair_order_details', + description: 'Details on repair orders', + query: null, + availability: null, + columns: [ + { + name: 'repair_order_id', + type: 'int', + attributes: [], + dimension: { + name: 'default.repair_order', + }, + }, + { + name: 'repair_type_id', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'price', + type: 'float', + attributes: [], + dimension: null, + }, + { + name: 'quantity', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'discount', + type: 'float', + attributes: [], + dimension: null, + }, + ], + updated_at: '2023-08-21T16:48:52.981201+00:00', + materializations: [], + parents: [], + created_at: '2023-08-21T16:48:52.970554+00:00', + tags: [], + dimension_links: [], + }, + { + namespace: 'default', + node_revision_id: 14, + node_id: 14, + type: 'dimension', + name: 'default.date_dim', + display_name: 'Default: Date Dim', + version: 'v1.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + schema_: null, + table: null, + description: 'Date dimension', + query: + '\n SELECT\n dateint,\n month,\n year,\n day\n FROM default.date\n ', + availability: null, + dimension_links: [], + columns: [ + { + name: 'dateint', + type: 'timestamp', + attributes: [ + { + attribute_type: { + namespace: 'system', + name: 'primary_key', + }, + }, + ], + dimension: null, + }, + { + name: 'month', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'year', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'day', + type: 'int', + attributes: [], + dimension: null, + }, + ], + updated_at: '2023-08-21T16:48:54.726980+00:00', + materializations: [], + parents: [ + { + name: 'default.date', + }, + ], + created_at: '2023-08-21T16:48:54.726871+00:00', + tags: [], + }, + { + namespace: 'default', + node_revision_id: 18, + node_id: 18, + type: 'dimension', + name: 'default.hard_hat', + display_name: 'Default: Hard Hat', + version: 'v1.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + schema_: null, + table: null, + description: 'Hard hat dimension', + query: + '\n SELECT\n hard_hat_id,\n last_name,\n first_name,\n title,\n birth_date,\n hire_date,\n address,\n city,\n state,\n postal_code,\n country,\n manager,\n contractor_id\n FROM default.hard_hats\n ', + availability: null, + dimension_links: [], + columns: [ + { + name: 'hard_hat_id', + type: 'int', + attributes: [ + { + attribute_type: { + namespace: 'system', + name: 'primary_key', + }, + }, + ], + dimension: null, + }, + { + name: 'last_name', + type: 'string', + attributes: [], + dimension: null, + }, + { + name: 'first_name', + type: 'string', + attributes: [], + dimension: null, + }, + { + name: 'title', + type: 'string', + attributes: [], + dimension: null, + }, + { + name: 'birth_date', + type: 'date', + attributes: [], + dimension: { + name: 'default.date_dim', + }, + }, + { + name: 'hire_date', + type: 'date', + attributes: [], + dimension: { + name: 'default.date_dim', + }, + }, + { + name: 'address', + type: 'string', + attributes: [], + dimension: null, + }, + { + name: 'city', + type: 'string', + attributes: [], + dimension: null, + }, + { + name: 'state', + type: 'string', + attributes: [], + dimension: { + name: 'default.us_state', + }, + }, + { + name: 'postal_code', + type: 'string', + attributes: [], + dimension: null, + }, + { + name: 'country', + type: 'string', + attributes: [], + dimension: null, + }, + { + name: 'manager', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'contractor_id', + type: 'int', + attributes: [], + dimension: null, + }, + ], + updated_at: '2023-08-21T16:48:55.594603+00:00', + materializations: [], + parents: [ + { + name: 'default.hard_hats', + }, + ], + created_at: '2023-08-21T16:48:55.594537+00:00', + tags: [], + }, + { + namespace: 'default', + node_revision_id: 24, + node_id: 24, + type: 'metric', + name: 'default.avg_repair_price', + display_name: 'Default: Avg Repair Price', + version: 'v1.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + schema_: null, + table: null, + description: 'Average repair price', + query: + 'SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n', + availability: null, + dimension_links: [], + columns: [ + { + name: 'default_DOT_avg_repair_price', + type: 'double', + attributes: [], + dimension: null, + }, + ], + updated_at: '2023-08-21T16:48:56.932231+00:00', + materializations: [], + parents: [ + { + name: 'default.repair_order_details', + }, + ], + created_at: '2023-08-21T16:48:56.932162+00:00', + tags: [], + }, + ]; + + function getByAttribute(container, id, attribute, ...rest) { + const result = queryByAttribute(attribute)(container, id, ...rest); + return result[0]; + } + + const mockDJClient = () => { + return { + DataJunctionAPI: { + metric: jest.fn(), + node_dag: jest.fn(name => { + return mockNodeDAG; + }), + }, + }; + }; + + const defaultProps = { + djNode: { + namespace: 'default', + node_revision_id: 24, + node_id: 24, + type: 'metric', + name: 'default.avg_repair_price', + display_name: 'Default: Avg Repair Price', + version: 'v1.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + schema_: null, + table: null, + description: 'Average repair price', + query: + 'SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n', + availability: null, + columns: [ + { + name: 'default_DOT_avg_repair_price', + type: 'double', + attributes: [], + dimension: null, + }, + ], + updated_at: '2023-08-21T16:48:56.932231+00:00', + materializations: [], + parents: [ + { + name: 'default.repair_order_details', + }, + ], + created_at: '2023-08-21T16:48:56.932162+00:00', + tags: [], + primary_key: [], + createNodeClientCode: + 'dj = DJBuilder(DJ_URL)\n\navg_repair_price = dj.create_metric(\n description="Average repair price",\n display_name="Default: Avg Repair Price",\n name="default.avg_repair_price",\n primary_key=[],\n query="""SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n"""\n)', + dimensions: [ + { + name: 'default.date_dim.dateint', + type: 'timestamp', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.dateint', + type: 'timestamp', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.date_dim.day', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.day', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.date_dim.month', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.month', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.date_dim.year', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.year', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.hard_hat.address', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.birth_date', + type: 'date', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.city', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.contractor_id', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.country', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.first_name', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.hard_hat_id', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.hire_date', + type: 'date', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.last_name', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.manager', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.postal_code', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.state', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.title', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + ], + }, + }; + + it('renders and calls node_dag with the correct node name', async () => { + const djClient = mockDJClient(); + djClient.DataJunctionAPI.metric = name => defaultProps.djNode; + // const layoutFlowMock = jest.spyOn(LayoutFlow); + const { container } = render( + + + , + ); + + await waitFor(() => { + expect(djClient.DataJunctionAPI.node_dag).toHaveBeenCalledWith( + defaultProps.djNode.name, + ); + + // The origin node should be displayed + expect(screen.getByText('Default: Avg Repair Price')).toBeInTheDocument(); + + // Every node in the DAG should be displayed on the screen + mockNodeDAG.forEach(node => + expect( + getByAttribute(container, node.name, 'data-id'), + ).toBeInTheDocument(), + ); + + const metricNode = getByAttribute( + container, + 'default.avg_repair_price', + 'data-id', + ); + expect(screen.getByText('▶ Show dimensions')).toBeInTheDocument(); + expect(screen.getByText('▶ More columns')).toBeInTheDocument(); + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx new file mode 100644 index 000000000..e3d6310ae --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import NodeColumnLineage from '../NodeLineageTab'; +import DJClientContext from '../../../providers/djclient'; +import { mocks } from '../../../../mocks/mockNodes'; + +describe('', () => { + const domTestingLib = require('@testing-library/dom'); + const { queryHelpers } = domTestingLib; + + const queryByAttribute = attribute => + queryHelpers.queryAllByAttribute.bind(null, attribute); + + function getByAttribute(container, id, attribute, ...rest) { + const result = queryByAttribute(attribute)(container, id, ...rest); + return result[0]; + } + + const mockDJClient = () => { + return { + DataJunctionAPI: { + node: jest.fn(), + metric: jest.fn(), + node_lineage: jest.fn(name => { + return mocks.mockNodeLineage; + }), + }, + }; + }; + + const defaultProps = { + djNode: mocks.mockMetricNodeJson, + }; + + it('renders and calls node_lineage with the correct node name', async () => { + const djClient = mockDJClient(); + djClient.DataJunctionAPI.node = name => mocks.mockMetricNode; + djClient.DataJunctionAPI.getMetric = name => mocks.mockMetricNodeJson; + const { container } = render( + + + , + ); + + await waitFor(() => { + expect(djClient.DataJunctionAPI.node_lineage).toHaveBeenCalledWith( + mocks.mockMetricNode.name, + ); + + // The origin node should be displayed + expect(screen.getByText('Default: Avg Repair Price')).toBeInTheDocument(); + + expect( + getByAttribute(container, 'default.avg_repair_price', 'data-id'), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx new file mode 100644 index 000000000..7b73b3509 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { render, waitFor, screen } from '@testing-library/react'; +import NodeMaterializationTab from '../NodeMaterializationTab'; + +describe('', () => { + const mockDjClient = { + node: jest.fn(), + materializations: jest.fn(), + }; + + const mockMaterializations = [ + { + name: 'mat_one', + config: {}, + schedule: '@daily', + job: 'SparkSqlMaterializationJob', + backfills: [ + { + spec: [ + { + column_name: 'date', + values: ['20200101'], + range: ['20201010'], + }, + ], + urls: ['https://example.com/'], + }, + ], + strategy: 'full', + output_tables: ['table1'], + urls: ['https://example.com/'], + }, + ]; + + const mockNode = { + node_revision_id: 1, + node_id: 1, + type: 'source', + name: 'default.repair_orders', + display_name: 'Default: Repair Orders', + version: 'v1.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + schema_: 'roads', + table: 'repair_orders', + description: 'Repair orders', + query: null, + availability: { + catalog: 'default', + categorical_partitions: [], + max_temporal_partition: ['2023', '01', '25'], + min_temporal_partition: ['2022', '01', '01'], + partitions: [], + schema_: 'foo', + table: 'bar', + temporal_partitions: [], + valid_through_ts: 1729667463, + url: 'https://www.table.com', + links: { dashboard: 'https://www.foobar.com/dashboard' }, + }, + columns: [ + { + name: 'repair_order_id', + type: 'int', + attributes: [], + dimension: null, + partition: { + type_: 'temporal', + format: 'YYYYMMDD', + granularity: 'day', + }, + }, + { + name: 'municipality_id', + type: 'string', + attributes: [], + dimension: null, + partition: null, + }, + { + name: 'hard_hat_id', + type: 'int', + attributes: [], + dimension: null, + partition: null, + }, + ], + updated_at: '2023-08-21T16:48:52.880498+00:00', + materializations: [ + { + name: 'mat1', + config: {}, + schedule: 'string', + job: 'string', + backfills: [ + { + spec: [ + { + column_name: 'string', + values: ['string'], + range: ['string'], + }, + ], + urls: ['string'], + }, + ], + strategy: 'string', + output_tables: ['string'], + urls: ['https://example.com/'], + }, + ], + parents: [], + dimension_links: [ + { + dimension: { + name: 'default.contractor', + }, + join_type: 'left', + join_sql: + 'default.contractor.contractor_id = default.repair_orders.contractor_id', + join_cardinality: 'one_to_one', + role: 'contractor', + }, + ], + }; + + beforeEach(() => { + mockDjClient.materializations.mockReset(); + }); + + it('renders NodeMaterializationTab tab correctly', async () => { + mockDjClient.materializations.mockReturnValue(mockMaterializations); + + render(); + await waitFor(() => { + const link = screen.getByText('dashboard').closest('a'); + expect(link).toHaveAttribute('href', `https://www.foobar.com/dashboard`); + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/NodePage.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/NodePage.test.jsx new file mode 100644 index 000000000..7b1206137 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/NodePage.test.jsx @@ -0,0 +1,863 @@ +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { mocks } from '../../../../mocks/mockNodes'; +import DJClientContext from '../../../providers/djclient'; +import { NodePage } from '../Loadable'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import userEvent from '@testing-library/user-event'; + +describe('', () => { + const domTestingLib = require('@testing-library/dom'); + const { queryHelpers } = domTestingLib; + + const queryByAttribute = attribute => + queryHelpers.queryAllByAttribute.bind(null, attribute); + + function getByAttribute(container, id, attribute, ...rest) { + const result = queryByAttribute(attribute)(container, id, ...rest); + return result[0]; + } + + const mockDJClient = () => { + return { + DataJunctionAPI: { + node: jest.fn(), + metric: jest.fn(), + getMetric: jest.fn(), + revalidate: jest.fn().mockReturnValue({ status: 'valid' }), + node_dag: jest.fn().mockReturnValue(mocks.mockNodeDAG), + clientCode: jest.fn().mockReturnValue('dj_client = DJClient()'), + columns: jest.fn(), + history: jest.fn(), + revisions: jest.fn(), + materializations: jest.fn(), + materializationInfo: jest.fn(), + sql: jest.fn(), + cube: jest.fn(), + compiledSql: jest.fn(), + node_lineage: jest.fn(), + nodesWithDimension: jest.fn(), + attributes: jest.fn(), + dimensions: jest.fn(), + setPartition: jest.fn(), + engines: jest.fn(), + streamNodeData: jest.fn(), + nodeDimensions: jest.fn(), + }, + }; + }; + + const defaultProps = { + name: 'default.avg_repair_price', + djNode: { + namespace: 'default', + node_revision_id: 24, + node_id: 24, + type: 'metric', + name: 'default.avg_repair_price', + display_name: 'Default: Avg Repair Price', + version: 'v1.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + schema_: null, + table: null, + description: 'Average repair price', + query: + 'SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n', + availability: null, + dimension_links: [], + columns: [ + { + name: 'default_DOT_avg_repair_price', + type: 'double', + display_name: 'Default DOT avg repair price', + attributes: [], + dimension: null, + }, + ], + updated_at: '2023-08-21T16:48:56.932231+00:00', + materializations: [], + parents: [ + { + name: 'default.repair_order_details', + }, + ], + created_at: '2023-08-21T16:48:56.932162+00:00', + tags: [{ name: 'purpose', display_name: 'Purpose' }], + primary_key: [], + incompatible_druid_functions: ['IF'], + createNodeClientCode: + 'dj = DJBuilder(DJ_URL)\n\navg_repair_price = dj.create_metric(\n description="Average repair price",\n display_name="Default: Avg Repair Price",\n name="default.avg_repair_price",\n primary_key=[],\n query="""SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n"""\n)', + dimensions: [ + { + name: 'default.date_dim.dateint', + type: 'timestamp', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.dateint', + type: 'timestamp', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.date_dim.day', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.day', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.date_dim.month', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.month', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.date_dim.year', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.year', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.hard_hat.address', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.birth_date', + type: 'date', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.city', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.contractor_id', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.country', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.first_name', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.hard_hat_id', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.hire_date', + type: 'date', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.last_name', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.manager', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.postal_code', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.state', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + { + name: 'default.hard_hat.title', + type: 'string', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + ], + }, + ], + }, + }; + + it('renders the NodeInfo tab correctly for a metric node', async () => { + const djClient = mockDJClient(); + djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); + djClient.DataJunctionAPI.getMetric.mockReturnValue( + mocks.mockMetricNodeJson, + ); + const element = ( + + + + ); + const { container } = render( + + + + + , + ); + + await waitFor(() => { + expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith( + 'default.num_repair_orders', + ); + + expect( + screen.getByRole('dialog', { name: 'NodeName' }), + ).toHaveTextContent('default.num_repair_orders'); + + expect(screen.getByRole('button', { name: 'Info' })).toBeInTheDocument(); + expect( + screen.getByRole('dialog', { name: 'Description' }), + ).toHaveTextContent('Number of repair orders'); + + expect(screen.getByRole('dialog', { name: 'Version' })).toHaveTextContent( + 'v1.0', + ); + + expect( + screen.getByRole('dialog', { name: 'NodeStatus' }), + ).toBeInTheDocument(); + + expect(screen.getByRole('dialog', { name: 'Tags' })).toHaveTextContent( + 'Purpose', + ); + + expect( + screen.getByRole('dialog', { name: 'RequiredDimensions' }), + ).toHaveTextContent(''); + + expect( + screen.getByRole('dialog', { name: 'DisplayName' }), + ).toHaveTextContent('Default: Num Repair Orders'); + + expect( + screen.getByRole('dialog', { name: 'NodeType' }), + ).toHaveTextContent('metric'); + + expect( + container.getElementsByClassName('language-sql'), + ).toMatchSnapshot(); + }); + }, 60000); + + it('renders the NodeInfo tab correctly for cube nodes', async () => { + const djClient = mockDJClient(); + djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockCubeNode); + djClient.DataJunctionAPI.cube.mockReturnValue(mocks.mockCubesCube); + const element = ( + + + + ); + const { container } = render( + + + + + , + ); + + await waitFor(() => { + expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith( + 'default.repair_orders_cube', + ); + userEvent.click(screen.getByRole('button', { name: 'Info' })); + + expect( + screen.getByRole('dialog', { name: 'NodeName' }), + ).toHaveTextContent('default.repair_orders_cube'); + + expect(screen.getByRole('button', { name: 'Info' })).toBeInTheDocument(); + expect( + screen.getByRole('dialog', { name: 'Description' }), + ).toHaveTextContent('Repair Orders'); + + expect(screen.getByRole('dialog', { name: 'Version' })).toHaveTextContent( + 'v1.0', + ); + + expect( + screen.getByRole('dialog', { name: 'PrimaryKey' }), + ).toHaveTextContent(''); + + expect( + screen.getByRole('dialog', { name: 'DisplayName' }), + ).toHaveTextContent('Default: Repair Orders Cube'); + + expect( + screen.getByRole('dialog', { name: 'NodeType' }), + ).toHaveTextContent('cube'); + + expect( + screen.getByRole('dialog', { name: 'NodeType' }), + ).toHaveTextContent('cube'); + + expect(screen.getByText('Cube Elements')).toBeInTheDocument(); + screen + .getAllByRole('cell', { name: 'CubeElement' }) + .map(cube => cube.hasAttribute('a')); + }); + }, 60000); + + it('renders the NodeColumns tab correctly', async () => { + const djClient = mockDJClient(); + djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); + djClient.DataJunctionAPI.getMetric.mockReturnValue( + mocks.mockMetricNodeJson, + ); + djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); + djClient.DataJunctionAPI.attributes.mockReturnValue(mocks.attributes); + djClient.DataJunctionAPI.dimensions.mockReturnValue(mocks.dimensions); + djClient.DataJunctionAPI.engines.mockReturnValue([]); + djClient.DataJunctionAPI.setPartition.mockReturnValue({ + status: 200, + json: { message: '' }, + }); + + const element = ( + + + + ); + render( + + + + + , + ); + await waitFor(() => { + expect(djClient.DataJunctionAPI.columns).toHaveBeenCalledWith( + mocks.mockMetricNode, + ); + expect( + screen.getByRole('columnheader', { name: 'ColumnName' }), + ).toHaveTextContent('default_DOT_avg_repair_price'); + expect( + screen.getByRole('columnheader', { name: 'ColumnDisplayName' }), + ).toHaveTextContent('Default DOT avg repair price'); + expect( + screen.getByRole('columnheader', { name: 'ColumnType' }), + ).toHaveTextContent('double'); + + // check that the edit column popover can be clicked + const editColumnPopover = screen.getByRole('button', { + name: 'EditColumn', + }); + expect(editColumnPopover).toBeInTheDocument(); + fireEvent.click(editColumnPopover); + expect( + screen.getByRole('button', { name: 'SaveEditColumn' }), + ).toBeInTheDocument(); + + // check that the link dimension popover can be clicked + const linkDimensionPopover = screen.getByRole('button', { + name: 'LinkDimension', + }); + expect(linkDimensionPopover).toBeInTheDocument(); + fireEvent.click(linkDimensionPopover); + expect( + screen.getByRole('button', { name: 'SaveLinkDimension' }), + ).toBeInTheDocument(); + + // check that the set column partition popover can be clicked + const partitionColumnPopover = screen.getByRole('button', { + name: 'PartitionColumn', + }); + expect(partitionColumnPopover).toBeInTheDocument(); + fireEvent.click(partitionColumnPopover); + const savePartition = screen.getByRole('button', { + name: 'SaveEditColumn', + }); + expect(savePartition).toBeInTheDocument(); + fireEvent.click(savePartition); + expect(screen.getByText('Saved!')); + }); + }, 60000); + // check compiled SQL on nodeInfo page + + it('renders the NodeHistory tab correctly', async () => { + const djClient = mockDJClient(); + djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); + djClient.DataJunctionAPI.getMetric.mockReturnValue( + mocks.mockMetricNodeJson, + ); + djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); + djClient.DataJunctionAPI.history.mockReturnValue(mocks.metricNodeHistory); + djClient.DataJunctionAPI.revisions.mockReturnValue( + mocks.metricNodeRevisions, + ); + + const element = ( + + + + ); + const { container } = render( + + + + + , + ); + await waitFor(async () => { + fireEvent.click(screen.getByRole('button', { name: 'History' })); + expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith( + mocks.mockMetricNode.name, + ); + expect(djClient.DataJunctionAPI.history).toHaveBeenCalledWith( + 'node', + mocks.mockMetricNode.name, + ); + expect( + screen.getByRole('list', { name: 'Activity' }), + ).toBeInTheDocument(); + screen + .queryAllByRole('cell', { + name: 'HistoryAttribute', + }) + .forEach(cell => expect(cell).toHaveTextContent(/Set col1 as /)); + + screen + .queryAllByRole('cell', { + name: 'HistoryCreateLink', + }) + .forEach(cell => + expect(cell).toHaveTextContent( + 'Linked col1 todefault.hard_hat viahard_hat_id', + ), + ); + + screen + .queryAllByRole('cell', { + name: 'HistoryCreateMaterialization', + }) + .forEach(cell => + expect(cell).toHaveTextContent( + 'Initialized materialization some_random_materialization', + ), + ); + + screen + .queryAllByRole('cell', { + name: 'HistoryNodeStatusChange', + }) + .forEach(cell => + expect(cell).toHaveTextContent( + 'Status changed from valid to invalid Caused by a change in upstream default.repair_order_details', + ), + ); + }); + }); + + it('renders compiled sql correctly', async () => { + const djClient = mockDJClient(); + djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockTransformNode); + djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); + djClient.DataJunctionAPI.compiledSql.mockReturnValue('select 1'); + + const element = ( + + + + ); + render( + + + + + , + ); + await waitFor(() => { + fireEvent.click(screen.getByRole('checkbox', { name: 'ToggleSwitch' })); + expect(djClient.DataJunctionAPI.compiledSql).toHaveBeenCalledWith( + mocks.mockTransformNode.name, + ); + }); + }); + + it('renders an empty NodeMaterialization tab correctly', async () => { + const djClient = mockDJClient(); + djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); + djClient.DataJunctionAPI.getMetric.mockReturnValue( + mocks.mockMetricNodeJson, + ); + djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); + djClient.DataJunctionAPI.materializations.mockReturnValue([]); + + const element = ( + + + + ); + render( + + + + + , + ); + await waitFor(() => { + fireEvent.click(screen.getByRole('button', { name: 'Materializations' })); + expect(djClient.DataJunctionAPI.materializations).toHaveBeenCalledWith( + mocks.mockMetricNode.name, + ); + screen.getByText( + 'No materialization workflows configured for this node.', + ); + screen.getByText('No materialized datasets available for this node.'); + }); + }); + + it('renders the NodeMaterialization tab with materializations correctly', async () => { + const djClient = mockDJClient(); + djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockTransformNode); + djClient.DataJunctionAPI.getMetric.mockReturnValue( + mocks.mockMetricNodeJson, + ); + djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); + djClient.DataJunctionAPI.materializations.mockReturnValue( + mocks.nodeMaterializations, + ); + + djClient.DataJunctionAPI.materializationInfo.mockReturnValue( + mocks.materializationInfo, + ); + + const element = ( + + + + ); + render( + + + + + , + ); + await waitFor( + () => { + fireEvent.click( + screen.getByRole('button', { name: 'Materializations' }), + ); + expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith( + mocks.mockTransformNode.name, + ); + expect(djClient.DataJunctionAPI.materializations).toHaveBeenCalledWith( + mocks.mockTransformNode.name, + ); + + expect( + screen.getByRole('table', { name: 'Materializations' }), + ).toMatchSnapshot(); + }, + { timeout: 3000 }, + ); + }, 60000); + + it('renders the NodeValidate tab', async () => { + const djClient = mockDJClient(); + window.scrollTo = jest.fn(); + djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); + djClient.DataJunctionAPI.nodeDimensions.mockReturnValue([]); + djClient.DataJunctionAPI.getMetric.mockReturnValue( + mocks.mockMetricNodeJson, + ); + djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); + djClient.DataJunctionAPI.sql.mockReturnValue({ + sql: 'SELECT * FROM testNode', + }); + const streamNodeData = { + onmessage: jest.fn(), + onerror: jest.fn(), + close: jest.fn(), + }; + djClient.DataJunctionAPI.streamNodeData.mockResolvedValue(streamNodeData); + djClient.DataJunctionAPI.streamNodeData.mockResolvedValueOnce({ + state: 'FINISHED', + results: [ + { + columns: [{ name: 'column1' }, { name: 'column2' }], + rows: [ + [1, 'value1'], + [2, 'value2'], + ], + }, + ], + }); + + const element = ( + + + + ); + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.getByText('Group By')).toBeInTheDocument(); + expect(screen.getByText('Add Filters')).toBeInTheDocument(); + expect(screen.getByText('Generated Query')).toBeInTheDocument(); + expect(screen.getByText('Results')).toBeInTheDocument(); + }); + // Click on the 'Validate' tab + fireEvent.click(screen.getByRole('button', { name: '► Validate' })); + + await waitFor(() => { + expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith( + mocks.mockMetricNode.name, + ); + expect(djClient.DataJunctionAPI.sql).toHaveBeenCalledWith( + mocks.mockMetricNode.name, + { dimensions: [], filters: [] }, + ); + expect(djClient.DataJunctionAPI.nodeDimensions).toHaveBeenCalledWith( + mocks.mockMetricNode.name, + ); + }); + + // Click on 'Run' to run the node query + const runButton = screen.getByText('► Run'); + fireEvent.click(runButton); + + await waitFor(() => { + expect(djClient.DataJunctionAPI.streamNodeData).toHaveBeenCalledWith( + mocks.mockMetricNode.name, + { dimensions: [], filters: [] }, + ); + expect(streamNodeData.onmessage).toBeDefined(); + expect(streamNodeData.onerror).toBeDefined(); + }); + + const infoTab = screen.getByRole('button', { name: 'QueryInfo' }); + const resultsTab = screen.getByText('Results'); + + // Initially, the Results tab should be active + expect(resultsTab).toHaveClass('active'); + expect(infoTab).not.toHaveClass('active'); + + // Click on the Info tab first + fireEvent.click(infoTab); + + await waitFor(() => { + // Now, the Info tab should be active + expect(infoTab).toHaveClass('active'); + expect(resultsTab).not.toHaveClass('active'); + }); + + // Click on the Results tab + fireEvent.click(resultsTab); + + await waitFor(() => { + // Now, the Results tab should be active again + expect(resultsTab).toHaveClass('active'); + expect(infoTab).not.toHaveClass('active'); + }); + }); + + it('renders a NodeColumnLineage tab correctly', async () => { + const djClient = mockDJClient(); + djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); + djClient.DataJunctionAPI.getMetric.mockReturnValue( + mocks.mockMetricNodeJson, + ); + djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); + djClient.DataJunctionAPI.node_lineage.mockReturnValue( + mocks.mockNodeLineage, + ); + + const element = ( + + + + ); + render( + + + + + , + ); + await waitFor(() => { + fireEvent.click(screen.getByRole('button', { name: 'Lineage' })); + expect(djClient.DataJunctionAPI.node_lineage).toHaveBeenCalledWith( + mocks.mockMetricNode.name, + ); + }); + }); + + it('renders a NodeGraph tab correctly', async () => { + const djClient = mockDJClient(); + djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode); + djClient.DataJunctionAPI.getMetric.mockReturnValue( + mocks.mockMetricNodeJson, + ); + djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns); + + const element = ( + + + + ); + render( + + + + + , + ); + await waitFor(() => { + fireEvent.click(screen.getByRole('button', { name: 'Graph' })); + expect(djClient.DataJunctionAPI.node_dag).toHaveBeenCalledWith( + mocks.mockMetricNode.name, + ); + }); + }); + + it('renders Linked Nodes tab correctly', async () => { + const djClient = mockDJClient(); + djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockDimensionNode); + djClient.DataJunctionAPI.nodesWithDimension.mockReturnValue( + mocks.mockLinkedNodes, + ); + + const element = ( + + + + ); + render( + + + + + , + ); + await waitFor(() => { + fireEvent.click(screen.getByRole('button', { name: 'Linked Nodes' })); + expect(djClient.DataJunctionAPI.nodesWithDimension).toHaveBeenCalledWith( + mocks.mockDimensionNode.name, + ); + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx new file mode 100644 index 000000000..8793e89cf --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx @@ -0,0 +1,175 @@ +import React from 'react'; +import { render, waitFor, screen } from '@testing-library/react'; +import NodesWithDimension from '../NodesWithDimension'; + +describe('', () => { + const mockDjClient = { + nodesWithDimension: jest.fn(), + }; + + const mockNodesWithDimension = [ + { + node_revision_id: 2, + node_id: 2, + type: 'source', + name: 'default.repair_order_details', + display_name: 'Default: Repair Order Details', + version: 'v1.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + schema_: 'roads', + table: 'repair_order_details', + description: 'Details on repair orders', + query: null, + availability: null, + columns: [ + { + name: 'repair_order_id', + type: 'int', + attributes: [], + dimension: { + name: 'default.repair_order', + }, + }, + { + name: 'repair_type_id', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'price', + type: 'float', + attributes: [], + dimension: null, + }, + { + name: 'quantity', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'discount', + type: 'float', + attributes: [], + dimension: null, + }, + ], + updated_at: '2023-08-21T16:48:52.981201+00:00', + materializations: [], + parents: [], + }, + { + node_revision_id: 1, + node_id: 1, + type: 'source', + name: 'default.repair_orders', + display_name: 'Default: Repair Orders', + version: 'v1.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + schema_: 'roads', + table: 'repair_orders', + description: 'Repair orders', + query: null, + availability: null, + columns: [ + { + name: 'repair_order_id', + type: 'int', + attributes: [], + dimension: { + name: 'default.repair_order', + }, + }, + { + name: 'municipality_id', + type: 'string', + attributes: [], + dimension: null, + }, + { + name: 'hard_hat_id', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'order_date', + type: 'date', + attributes: [], + dimension: null, + }, + { + name: 'required_date', + type: 'date', + attributes: [], + dimension: null, + }, + { + name: 'dispatched_date', + type: 'date', + attributes: [], + dimension: null, + }, + { + name: 'dispatcher_id', + type: 'int', + attributes: [], + dimension: null, + }, + ], + updated_at: '2023-08-21T16:48:52.880498+00:00', + materializations: [], + parents: [], + }, + ]; + + const defaultProps = { + node: { + name: 'TestNode', + }, + djClient: mockDjClient, + }; + + beforeEach(() => { + // Reset the mocks before each test + mockDjClient.nodesWithDimension.mockReset(); + }); + + it('renders nodes with dimensions', async () => { + mockDjClient.nodesWithDimension.mockReturnValue(mockNodesWithDimension); + render(); + await waitFor(() => { + // calls nodesWithDimension with the correct node name + expect(mockDjClient.nodesWithDimension).toHaveBeenCalledWith( + defaultProps.node.name, + ); + for (const node of mockNodesWithDimension) { + // renders nodes based on nodesWithDimension data + expect(screen.getByText(node.display_name)).toBeInTheDocument(); + + // renders links to the correct URLs + const link = screen.getByText(node.display_name).closest('a'); + expect(link).toHaveAttribute('href', `/nodes/${node.name}`); + } + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx new file mode 100644 index 000000000..67dff0b30 --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx @@ -0,0 +1,164 @@ +import React from 'react'; +import { render, waitFor, screen } from '@testing-library/react'; +import RevisionDiff from '../RevisionDiff'; +import DJClientContext from '../../../providers/djclient'; +import { NodePage } from '../Loadable'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; + +describe('', () => { + const mockDjClient = { + DataJunctionAPI: { + revisions: jest.fn(), + }, + }; + + const mockNodesWithDimension = [ + { + node_revision_id: 1, + node_id: 1, + type: 'dimension', + name: 'default.repair_order', + display_name: 'Repair Orders', + version: 'v1.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + description: 'Repair order dimension', + query: + 'SELECT repair_order_id, municipality_id, hard_hat_id, order_date, ' + + 'dispatcher_id FROM default.repair_orders', + availability: null, + columns: [ + { + name: 'repair_order_id', + type: 'int', + attributes: [], + dimension: { + name: 'default.repair_order', + }, + }, + { + name: 'municipality_id', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'hard_hat_id', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'order_date', + type: 'date', + attributes: [], + dimension: null, + }, + { + name: 'dispatcher_id', + type: 'int', + attributes: [], + dimension: null, + }, + ], + updated_at: '2023-08-21T16:48:52.981201+00:00', + materializations: [], + parents: [], + }, + { + node_revision_id: 2, + node_id: 2, + type: 'dimension', + name: 'default.repair_order', + display_name: 'Repair Orders', + version: 'v2.0', + status: 'valid', + mode: 'published', + catalog: { + id: 1, + uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488', + created_at: '2023-08-21T16:48:51.146121+00:00', + updated_at: '2023-08-21T16:48:51.146122+00:00', + extra_params: {}, + name: 'warehouse', + }, + description: 'Repair order dimension', + query: + 'SELECT repair_order_id, municipality_id, hard_hat_id, ' + + 'dispatcher_id FROM default.repair_orders', + availability: null, + columns: [ + { + name: 'repair_order_id', + type: 'int', + attributes: [], + dimension: { + name: 'default.repair_order', + }, + }, + { + name: 'municipality_id', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'hard_hat_id', + type: 'int', + attributes: [], + dimension: null, + }, + { + name: 'dispatcher_id', + type: 'int', + attributes: [], + dimension: null, + }, + ], + updated_at: '2023-08-21T16:48:52.981201+00:00', + materializations: [], + parents: [], + }, + ]; + + beforeEach(() => { + // Reset the mocks before each test + mockDjClient.DataJunctionAPI.revisions.mockReset(); + }); + + it('renders revision diff', async () => { + mockDjClient.DataJunctionAPI.revisions.mockReturnValue( + mockNodesWithDimension, + ); + const element = ( + + + + ); + const { container } = render( + + + + + , + ); + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.revisions).toHaveBeenCalledWith( + 'default.repair_orders_cube', + ); + + const diffViews = screen.getAllByRole('gridcell', 'DiffView'); + diffViews.map(diffView => expect(diffView).toBeInTheDocument()); + }); + }); +}); diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap b/datajunction-ui/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap new file mode 100644 index 000000000..92bbc02fd --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap @@ -0,0 +1,319 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders the NodeInfo tab correctly for a metric node 1`] = ` +HTMLCollection [ + + + count + + + (repair_order_id) + + , +] +`; + +exports[` renders the NodeMaterialization tab with materializations correctly 1`] = ` +
    +
    +

    + Materializations +

    + + +
    +

    + Materialized Datasets +

    +
    + No materialized datasets available for this node. +
    +
    +
    +`; diff --git a/datajunction-ui/src/app/pages/NodePage/index.jsx b/datajunction-ui/src/app/pages/NodePage/index.jsx new file mode 100644 index 000000000..87b656a7e --- /dev/null +++ b/datajunction-ui/src/app/pages/NodePage/index.jsx @@ -0,0 +1,225 @@ +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import { useContext, useEffect, useState } from 'react'; +import Tab from '../../components/Tab'; +import NamespaceHeader from '../../components/NamespaceHeader'; +import NodeInfoTab from './NodeInfoTab'; +import NodeColumnTab from './NodeColumnTab'; +import NodeGraphTab from './NodeGraphTab'; +import NodeHistory from './NodeHistory'; +import NotebookDownload from './NotebookDownload'; +import DJClientContext from '../../providers/djclient'; +import NodeValidateTab from './NodeValidateTab'; +import NodeMaterializationTab from './NodeMaterializationTab'; +import ClientCodePopover from './ClientCodePopover'; +import NodesWithDimension from './NodesWithDimension'; +import NodeColumnLineage from './NodeLineageTab'; +import EditIcon from '../../icons/EditIcon'; +import AlertIcon from '../../icons/AlertIcon'; +import NodeDependenciesTab from './NodeDependenciesTab'; +import { useNavigate } from 'react-router-dom'; + +export function NodePage() { + const djClient = useContext(DJClientContext).DataJunctionAPI; + const navigate = useNavigate(); + + const { name, tab } = useParams(); + + const [state, setState] = useState({ + selectedTab: tab || 'info', + }); + + const [node, setNode] = useState(); + + const onClickTab = id => () => { + navigate(`/nodes/${name}/${id}`); + setState({ selectedTab: id }); + }; + + const buildTabs = tab => { + return tab.display ? ( + + ) : null; + }; + + useEffect(() => { + const fetchData = async () => { + const data = await djClient.node(name); + data.createNodeClientCode = await djClient.clientCode(name); + if (data.type === 'metric') { + const metric = await djClient.getMetric(name); + data.metric_metadata = metric.current.metricMetadata; + data.required_dimensions = metric.current.requiredDimensions; + data.upstream_node = metric.current.parents[0].name; + data.expression = metric.current.metricMetadata.expression; + data.incompatible_druid_functions = + metric.current.metricMetadata.incompatibleDruidFunctions; + } + if (data.type === 'cube') { + const cube = await djClient.cube(name); + data.cube_elements = cube.cube_elements; + } + setNode(data); + }; + fetchData().catch(console.error); + }, [djClient, name]); + + const tabsList = node => { + return [ + { + id: 'info', + name: 'Info', + display: true, + }, + { + id: 'columns', + name: 'Columns', + display: true, + }, + { + id: 'graph', + name: 'Graph', + display: true, + }, + { + id: 'history', + name: 'History', + display: true, + }, + { + id: 'validate', + name: '► Validate', + display: node?.type !== 'source', + }, + { + id: 'materializations', + name: 'Materializations', + display: node?.type !== 'source', + }, + { + id: 'linked', + name: 'Linked Nodes', + display: node?.type === 'dimension', + }, + { + id: 'lineage', + name: 'Lineage', + display: node?.type === 'metric', + }, + { + id: 'dependencies', + name: 'Dependencies', + display: node?.type !== 'cube', + }, + ]; + }; + let tabToDisplay = null; + + switch (state.selectedTab) { + case 'info': + tabToDisplay = + node && node.message === undefined ? : ''; + break; + case 'columns': + tabToDisplay = ; + break; + case 'graph': + tabToDisplay = ; + break; + case 'history': + tabToDisplay = ; + break; + case 'validate': + tabToDisplay = ; + break; + case 'materializations': + tabToDisplay = ; + break; + case 'linked': + tabToDisplay = ; + break; + case 'lineage': + tabToDisplay = ; + break; + case 'dependencies': + tabToDisplay = ; + break; + default: /* istanbul ignore next */ + tabToDisplay = ; + } + // @ts-ignore + return ( +
    + +
    + {node?.message === undefined ? ( +
    +

    + + {node?.display_name}{' '} + + {node?.type} + + +

    + + + + +
    + + {node?.name} + + + {node?.version} + + {node?.type === 'cube' ? : <>} +
    +
    + {tabsList(node).map(buildTabs)} +
    + {tabToDisplay} +
    + ) : node?.message !== undefined ? ( +
    + + Node `{name}` does not exist! +
    + ) : ( + '' + )} +
    +
    + ); +} diff --git a/datajunction-ui/src/app/pages/NotFoundPage/Loadable.tsx b/datajunction-ui/src/app/pages/NotFoundPage/Loadable.tsx new file mode 100644 index 000000000..74b007c1a --- /dev/null +++ b/datajunction-ui/src/app/pages/NotFoundPage/Loadable.tsx @@ -0,0 +1,14 @@ +/** + * Asynchronously loads the component for NotFoundPage + */ + +import * as React from 'react'; +import { lazyLoad } from '../../../utils/loadable'; + +export const NotFoundPage = lazyLoad( + () => import('./index'), + module => module.NotFoundPage, + { + fallback: <>, + }, +); diff --git a/datajunction-ui/src/app/pages/NotFoundPage/__tests__/index.test.jsx b/datajunction-ui/src/app/pages/NotFoundPage/__tests__/index.test.jsx new file mode 100644 index 000000000..a7925ea86 --- /dev/null +++ b/datajunction-ui/src/app/pages/NotFoundPage/__tests__/index.test.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { NotFoundPage } from '../index'; +import { HelmetProvider } from 'react-helmet-async'; + +describe('', () => { + it('displays the correct 404 message ', () => { + const { getByText } = render( + + + , + ); + + expect(getByText('Page not found.')).toBeInTheDocument(); + }); +}); diff --git a/datajunction-ui/src/app/pages/NotFoundPage/index.tsx b/datajunction-ui/src/app/pages/NotFoundPage/index.tsx new file mode 100644 index 000000000..658a82df7 --- /dev/null +++ b/datajunction-ui/src/app/pages/NotFoundPage/index.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Helmet } from 'react-helmet-async'; + +export function NotFoundPage() { + return ( + <> + + 404 Page Not Found + + +
    + +

    Page not found.

    +
    + + ); +} diff --git a/datajunction-ui/src/app/pages/RegisterTablePage/Loadable.jsx b/datajunction-ui/src/app/pages/RegisterTablePage/Loadable.jsx new file mode 100644 index 000000000..4f9041491 --- /dev/null +++ b/datajunction-ui/src/app/pages/RegisterTablePage/Loadable.jsx @@ -0,0 +1,16 @@ +/** + * Asynchronously loads the component for the Node page + */ + +import * as React from 'react'; +import { lazyLoad } from '../../../utils/loadable'; + +export const RegisterTablePage = () => { + return lazyLoad( + () => import('./index'), + module => module.RegisterTablePage, + { + fallback:
    , + }, + )(); +}; diff --git a/datajunction-ui/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx b/datajunction-ui/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx new file mode 100644 index 000000000..a6dfe57ba --- /dev/null +++ b/datajunction-ui/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import fetchMock from 'jest-fetch-mock'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../../setupTests'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import DJClientContext from '../../../providers/djclient'; +import { RegisterTablePage } from '../index'; + +describe('', () => { + const initializeMockDJClient = () => { + return { + DataJunctionAPI: { + catalogs: jest.fn(), + registerTable: jest.fn(), + }, + }; + }; + + const mockDjClient = initializeMockDJClient(); + + beforeEach(() => { + fetchMock.resetMocks(); + jest.clearAllMocks(); + window.scrollTo = jest.fn(); + + mockDjClient.DataJunctionAPI.catalogs.mockReturnValue([ + { + name: 'warehouse', + engines: [ + { + name: 'duckdb', + version: '0.7.1', + uri: null, + dialect: null, + }, + ], + }, + ]); + }); + + const renderRegisterTable = element => { + return render( + + + + + , + ); + }; + + const testElement = djClient => { + return ( + + + + ); + }; + + it('registers a table correctly', async () => { + mockDjClient.DataJunctionAPI.registerTable.mockReturnValue({ + status: 201, + json: { name: 'source.warehouse.schema.some_table' }, + }); + + const element = testElement(mockDjClient); + const { container, getByTestId } = renderRegisterTable(element); + + const catalog = getByTestId('choose-catalog'); + await waitFor(async () => { + fireEvent.keyDown(catalog.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('warehouse')); + }); + + await userEvent.type(screen.getByLabelText('Schema'), 'schema'); + await userEvent.type(screen.getByLabelText('Table'), 'some_table'); + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(mockDjClient.DataJunctionAPI.registerTable).toBeCalled(); + expect(mockDjClient.DataJunctionAPI.registerTable).toBeCalledWith( + 'warehouse', + 'schema', + 'some_table', + ); + }); + expect(container.getElementsByClassName('message')).toMatchSnapshot(); + }, 60000); + + it('fails to register a table', async () => { + mockDjClient.DataJunctionAPI.registerTable.mockReturnValue({ + status: 500, + json: { message: 'table not found' }, + }); + + const element = testElement(mockDjClient); + const { getByTestId } = renderRegisterTable(element); + + const catalog = getByTestId('choose-catalog'); + await waitFor(async () => { + fireEvent.keyDown(catalog.firstChild, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('warehouse')); + }); + + await userEvent.type(screen.getByLabelText('Schema'), 'schema'); + await userEvent.type(screen.getByLabelText('Table'), 'some_table'); + await userEvent.click(screen.getByRole('button')); + expect(screen.getByText('table not found')).toBeInTheDocument(); + }, 60000); +}); diff --git a/datajunction-ui/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap b/datajunction-ui/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap new file mode 100644 index 000000000..e206274fb --- /dev/null +++ b/datajunction-ui/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` registers a table correctly 1`] = ` +HTMLCollection [ +
    + + + + Successfully registered source node + + + source.warehouse.schema.some_table + + , which references table + warehouse + . + schema + . + some_table + . +
    , +] +`; diff --git a/datajunction-ui/src/app/pages/RegisterTablePage/index.jsx b/datajunction-ui/src/app/pages/RegisterTablePage/index.jsx new file mode 100644 index 000000000..0915d1cda --- /dev/null +++ b/datajunction-ui/src/app/pages/RegisterTablePage/index.jsx @@ -0,0 +1,142 @@ +/** + * Node add + edit page for transforms, metrics, and dimensions. The creation and edit flow for these + * node types is largely the same, with minor differences handled server-side. For the `query` + * field, this page will render a CodeMirror SQL editor with autocompletion and syntax highlighting. + */ +import { ErrorMessage, Field, Form, Formik } from 'formik'; + +import NamespaceHeader from '../../components/NamespaceHeader'; +import React, { useContext, useEffect, useState } from 'react'; +import DJClientContext from '../../providers/djclient'; +import 'styles/node-creation.scss'; +import { FormikSelect } from '../AddEditNodePage/FormikSelect'; +import { displayMessageAfterSubmit } from '../../../utils/form'; + +export function RegisterTablePage() { + const djClient = useContext(DJClientContext).DataJunctionAPI; + const [catalogs, setCatalogs] = useState([]); + + useEffect(() => { + const fetchData = async () => { + const catalogs = await djClient.catalogs(); + setCatalogs( + catalogs.map(catalog => { + return { value: catalog.name, label: catalog.name }; + }), + ); + }; + fetchData().catch(console.error); + }, [djClient, djClient.namespaces]); + + const initialValues = { + catalog: '', + schema: '', + table: '', + }; + + const validator = values => { + const errors = {}; + if (!values.table) { + errors.table = 'Required'; + } + if (!values.schema) { + errors.schema = 'Required'; + } + return errors; + }; + + const handleSubmit = async (values, { setSubmitting, setStatus }) => { + const { status, json } = await djClient.registerTable( + values.catalog, + values.schema, + values.table, + ); + if (status === 200 || status === 201) { + setStatus({ + success: ( + <> + Successfully registered source node{' '} + {json.name}, which references + table {values.catalog}.{values.schema}.{values.table}. + + ), + }); + } else { + setStatus({ + failure: `${json.message}`, + }); + } + setSubmitting(false); + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); + }; + + return ( +
    + +
    +
    +

    + Register{' '} + + Source + +

    +
    + + {function Render({ isSubmitting, status }) { + return ( +
    + {displayMessageAfterSubmit(status)} + { + <> +
    + + + + + +
    +
    + + + +
    +
    + + + +
    + + + } +
    + ); + }} +
    +
    +
    +
    +
    + ); +} diff --git a/datajunction-ui/src/app/pages/Root/Loadable.tsx b/datajunction-ui/src/app/pages/Root/Loadable.tsx new file mode 100644 index 000000000..37d5a2d86 --- /dev/null +++ b/datajunction-ui/src/app/pages/Root/Loadable.tsx @@ -0,0 +1,14 @@ +/** + * Asynchronously loads the component for the root page + */ + +import * as React from 'react'; +import { lazyLoad } from '../../../utils/loadable'; + +export const Root = lazyLoad( + () => import('./index'), + module => module.Root, + { + fallback: <>, + }, +); diff --git a/datajunction-ui/src/app/pages/Root/__tests__/index.test.jsx b/datajunction-ui/src/app/pages/Root/__tests__/index.test.jsx new file mode 100644 index 000000000..730f40911 --- /dev/null +++ b/datajunction-ui/src/app/pages/Root/__tests__/index.test.jsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { Root } from '../index'; +import DJClientContext from '../../../providers/djclient'; +import { HelmetProvider } from 'react-helmet-async'; + +describe('', () => { + const mockDjClient = { + logout: jest.fn(), + nodeDetails: jest.fn(), + listTags: jest.fn(), + }; + + it('renders with the correct title and navigation', async () => { + render( + + + + + , + ); + + waitFor(() => { + expect(document.title).toEqual('DataJunction'); + const metaDescription = document.querySelector( + "meta[name='description']", + ); + expect(metaDescription).toBeInTheDocument(); + expect(metaDescription.content).toBe( + 'DataJunction Metrics Platform Webapp', + ); + + expect(screen.getByText(/^DataJunction$/)).toBeInTheDocument(); + expect(screen.getByText('Explore').closest('a')).toHaveAttribute( + 'href', + '/', + ); + expect(screen.getByText('SQL').closest('a')).toHaveAttribute( + 'href', + '/sql', + ); + expect(screen.getByText('Open-Source').closest('a')).toHaveAttribute( + 'href', + 'https://www.datajunction.io', + ); + }); + }); + + it('renders Logout button unless REACT_DISABLE_AUTH is true', () => { + process.env.REACT_DISABLE_AUTH = 'false'; + render( + + + + + , + ); + expect(screen.getByText('Logout')).toBeInTheDocument(); + }); + + it('calls logout and reloads window on logout button click', () => { + process.env.REACT_DISABLE_AUTH = 'false'; + const originalLocation = window.location; + delete window.location; + window.location = { ...originalLocation, reload: jest.fn() }; + + render( + + + + + , + ); + + screen.getByText('Logout').click(); + expect(mockDjClient.logout).toHaveBeenCalled(); + window.location = originalLocation; + }); +}); diff --git a/datajunction-ui/src/app/pages/Root/assets/dj-logo.png b/datajunction-ui/src/app/pages/Root/assets/dj-logo.png new file mode 100644 index 000000000..00b5b1945 Binary files /dev/null and b/datajunction-ui/src/app/pages/Root/assets/dj-logo.png differ diff --git a/datajunction-ui/src/app/pages/Root/index.tsx b/datajunction-ui/src/app/pages/Root/index.tsx new file mode 100644 index 000000000..b9a30f683 --- /dev/null +++ b/datajunction-ui/src/app/pages/Root/index.tsx @@ -0,0 +1,120 @@ +import { useContext } from 'react'; +import { Outlet } from 'react-router-dom'; +import DJLogo from '../../icons/DJLogo'; +import { Helmet } from 'react-helmet-async'; +import DJClientContext from '../../providers/djclient'; +import Search from '../../components/Search'; + +// Define the type for the docs sites +type DocsSites = { + [key: string]: string; +}; + +// Default docs sites if REACT_APP_DOCS_SITES is not defined +const defaultDocsSites: DocsSites = { + 'Open-Source': 'https://www.datajunction.io/', +}; + +// Parse the JSON map from the environment variable or use the default +const docsSites: DocsSites = process.env.REACT_APP_DOCS_SITES + ? (JSON.parse(process.env.REACT_APP_DOCS_SITES as string) as DocsSites) + : defaultDocsSites; + +export function Root() { + const djClient = useContext(DJClientContext).DataJunctionAPI; + + const handleLogout = async () => { + await djClient.logout(); + window.location.reload(); + }; + + return ( + <> + + DataJunction + + +
    +
    + + +
    +
    + + + Explore + + + + + SQL + + + + +
    + +
      + {Object.entries(docsSites).map(([key, value]) => ( +
    • + + {key} + +
    • + ))} +
    +
    +
    +
    +
    +
    +
    + {process.env.REACT_DISABLE_AUTH === 'true' ? ( + '' + ) : ( + + + + Logout + + + + )} +
    + + + ); +} diff --git a/datajunction-ui/src/app/pages/SQLBuilderPage/Loadable.jsx b/datajunction-ui/src/app/pages/SQLBuilderPage/Loadable.jsx new file mode 100644 index 000000000..74c0a0609 --- /dev/null +++ b/datajunction-ui/src/app/pages/SQLBuilderPage/Loadable.jsx @@ -0,0 +1,16 @@ +/** + * Asynchronously loads the component for the Node page + */ + +import * as React from 'react'; +import { lazyLoad } from '../../../utils/loadable'; + +export const SQLBuilderPage = props => { + return lazyLoad( + () => import('./index'), + module => module.SQLBuilderPage, + { + fallback:
    , + }, + )(props); +}; diff --git a/datajunction-ui/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx b/datajunction-ui/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx new file mode 100644 index 000000000..d9290d216 --- /dev/null +++ b/datajunction-ui/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx @@ -0,0 +1,173 @@ +import { + render, + screen, + fireEvent, + waitFor, + waitForElement, + act, +} from '@testing-library/react'; +import DJClientContext from '../../../providers/djclient'; +import { SQLBuilderPage } from '../index'; + +const mockDjClient = { + metrics: jest.fn(), + commonDimensions: jest.fn(), + sqls: jest.fn(), + data: jest.fn(), +}; + +const mockMetrics = [ + 'default.num_repair_orders', + 'default.avg_repair_price', + 'default.total_repair_cost', +]; + +const mockCommonDimensions = [ + { + name: 'default.date_dim.dateint', + type: 'timestamp', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.dateint', + type: 'timestamp', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.date_dim.day', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.day', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.date_dim.month', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.month', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, + { + name: 'default.date_dim.year', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.birth_date', + ], + }, + { + name: 'default.date_dim.year', + type: 'int', + path: [ + 'default.repair_order_details.repair_order_id', + 'default.repair_order.hard_hat_id', + 'default.hard_hat.hire_date', + ], + }, +]; + +describe('SQLBuilderPage', () => { + beforeEach(() => { + mockDjClient.metrics.mockResolvedValue(mockMetrics); + mockDjClient.commonDimensions.mockResolvedValue(mockCommonDimensions); + mockDjClient.sqls.mockResolvedValue({ sql: 'SELECT ...' }); + mockDjClient.data.mockResolvedValue({}); + + render( + + + , + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders without crashing', () => { + expect(screen.getByText('Using the SQL Builder')).toBeInTheDocument(); + }); + + it('renders the Metrics section', () => { + expect(screen.getByText('Metrics')).toBeInTheDocument(); + }); + + it('renders the Group By section', () => { + expect(screen.getByText('Group By')).toBeInTheDocument(); + }); + + it('renders the Filter By section', () => { + expect(screen.getByText('Filter By')).toBeInTheDocument(); + }); + + it('fetches metrics on mount', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(mockDjClient.metrics).toHaveBeenCalled(); + }); + + const selectMetrics = screen.getAllByTestId('select-metrics')[0]; + expect(selectMetrics).toBeDefined(); + expect(selectMetrics).not.toBeNull(); + expect(screen.getAllByText('3 Available Metrics')[0]).toBeInTheDocument(); + + fireEvent.keyDown(selectMetrics.firstChild, { key: 'ArrowDown' }); + for (const metric of mockMetrics) { + await waitFor(() => { + expect(screen.getByText(metric)).toBeInTheDocument(); + fireEvent.click(screen.getByText(metric)); + }); + } + fireEvent.click(screen.getAllByText('Group By')[0]); + + expect(mockDjClient.commonDimensions).toHaveBeenCalled(); + + const selectDimensions = screen.getAllByTestId('select-dimensions')[0]; + expect(selectDimensions).toBeDefined(); + expect(selectDimensions).not.toBeNull(); + expect(screen.getAllByText('8 Shared Dimensions')[0]).toBeInTheDocument(); + fireEvent.keyDown(selectDimensions.firstChild, { key: 'ArrowDown' }); + + for (const dim of mockCommonDimensions) { + expect(screen.getAllByText(dim.name)[0]).toBeInTheDocument(); + fireEvent.click(screen.getAllByText(dim.name)[0]); + } + expect(mockDjClient.sqls).toHaveBeenCalled(); + }); +}); diff --git a/datajunction-ui/src/app/pages/SQLBuilderPage/index.jsx b/datajunction-ui/src/app/pages/SQLBuilderPage/index.jsx new file mode 100644 index 000000000..d412c0b52 --- /dev/null +++ b/datajunction-ui/src/app/pages/SQLBuilderPage/index.jsx @@ -0,0 +1,390 @@ +import { useContext, useEffect, useState } from 'react'; +import NamespaceHeader from '../../components/NamespaceHeader'; +import { DataJunctionAPI } from '../../services/DJService'; +import DJClientContext from '../../providers/djclient'; +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { foundation } from 'react-syntax-highlighter/src/styles/hljs'; +import Select from 'react-select'; +import QueryInfo from '../../components/QueryInfo'; +import 'react-querybuilder/dist/query-builder.scss'; +import QueryBuilder, { formatQuery } from 'react-querybuilder'; +import 'styles/styles.scss'; + +export function SQLBuilderPage() { + const DEFAULT_NUM_ROWS = 100; + const djClient = useContext(DJClientContext).DataJunctionAPI; + const validator = ruleType => !!ruleType.value; + const [stagedMetrics, setStagedMetrics] = useState([]); + const [metrics, setMetrics] = useState([]); + const [commonDimensionsList, setCommonDimensionsList] = useState([]); + const [selectedDimensions, setSelectedDimensions] = useState([]); + const [stagedDimensions, setStagedDimensions] = useState([]); + const [selectedMetrics, setSelectedMetrics] = useState([]); + const [query, setQuery] = useState(''); + const [fields, setFields] = useState([]); + const [filters, setFilters] = useState({ combinator: 'and', rules: [] }); + const [queryInfo, setQueryInfo] = useState({}); + const [data, setData] = useState(null); + const [loadingData, setLoadingData] = useState(false); + const [viewData, setViewData] = useState(false); + const [showNumRows, setShowNumRows] = useState(DEFAULT_NUM_ROWS); + const [displayedRows, setDisplayedRows] = useState(<>); + const numRowsOptions = [ + { + value: 10, + label: '10 Rows', + isFixed: true, + }, + { + value: 100, + label: '100 Rows', + isFixed: true, + }, + { + value: 1000, + label: '1,000 Rows', + isFixed: true, + }, + ]; + const toggleViewData = () => setViewData(current => !current); + + // Get data for the current selection of metrics and dimensions + const getData = () => { + setLoadingData(true); + setQueryInfo({}); + const fetchData = async () => { + if (process.env.REACT_USE_SSE) { + const sse = await djClient.stream( + selectedMetrics, + selectedDimensions, + formatQuery(filters, { format: 'sql', parseNumbers: true }), + ); + sse.onmessage = e => { + const messageData = JSON.parse(JSON.parse(e.data)); + setQueryInfo(messageData); + if (messageData.results) { + setLoadingData(false); + setData(messageData.results); + messageData.numRows = messageData.results?.length + ? messageData.results[0].rows.length + : []; + setViewData(true); + setShowNumRows(DEFAULT_NUM_ROWS); + } + }; + sse.onerror = () => sse.close(); + } else { + const response = await djClient.data( + selectedMetrics, + selectedDimensions, + ); + setQueryInfo(response); + if (response.results) { + setLoadingData(false); + setData(response.results); + response.numRows = response.results?.length + ? response.results[0].rows.length + : []; + setViewData(true); + setShowNumRows(DEFAULT_NUM_ROWS); + } + } + }; + fetchData().catch(console.error); + }; + + const resetView = () => { + setQuery(''); + setData(null); + setViewData(false); + setQueryInfo({}); + }; + + // Get metrics + useEffect(() => { + const fetchData = async () => { + const metrics = await djClient.metrics(); + setMetrics(metrics.map(m => ({ value: m, label: m }))); + }; + fetchData().catch(console.error); + }, [djClient, djClient.metrics]); + + const attributeToFormInput = dimension => { + const attribute = { + name: dimension.name, + label: `${dimension.name} (via ${dimension.path.join(' ▶ ')})`, + placeholder: `from ${dimension.path}`, + defaultOperator: '=', + validator, + }; + if (dimension.type === 'bool') { + attribute.valueEditorType = 'checkbox'; + } + if (dimension.type === 'timestamp') { + attribute.inputType = 'datetime-local'; + attribute.defaultOperator = 'between'; + } + return [dimension.name, attribute]; + }; + + // Get common dimensions + useEffect(() => { + const fetchData = async () => { + if (selectedMetrics.length) { + const commonDimensions = await djClient.commonDimensions( + selectedMetrics, + ); + setCommonDimensionsList( + commonDimensions.map(d => ({ + value: d.name, + label: d.name, + path: d.path.join(' ▶ '), + })), + ); + const uniqueFields = Object.fromEntries( + new Map( + commonDimensions.map(dimension => attributeToFormInput(dimension)), + ), + ); + setFields(Object.keys(uniqueFields).map(f => uniqueFields[f])); + } else { + setCommonDimensionsList([]); + setFields([]); + } + }; + fetchData().catch(console.error); + }, [selectedMetrics, djClient]); + + // Get SQL + useEffect(() => { + const fetchData = async () => { + if ( + selectedMetrics.length > 0 && + (selectedDimensions.length > 0 || filters.rules.length > 0) + ) { + const result = await djClient.sqls( + selectedMetrics, + selectedDimensions, + formatQuery(filters, { format: 'sql', parseNumbers: true }), + ); + setQuery(result.sql); + } else { + resetView(); + } + }; + fetchData().catch(console.error); + }, [djClient, filters, selectedDimensions, selectedMetrics]); + + // Set number of rows to display + useEffect(() => { + if (data) { + setDisplayedRows( + data[0]?.rows.slice(0, showNumRows).map((rowData, index) => ( + + {rowData.map(rowValue => ( + {rowValue} + ))} + + )), + ); + } + }, [showNumRows, data]); + const formatOptionLabel = ({ value, label, path }) => ( +
    +
    {label}
    + {path} +
    + ); + + // @ts-ignore + return ( + <> +
    + +
    +
    +

    Metrics

    + + + 'No shared dimensions found. Try selecting different metrics.' + } + placeholder={`${commonDimensionsList.length} Shared Dimensions`} + isMulti + isClearable + closeMenuOnSelect={false} + onChange={e => { + resetView(); + setStagedDimensions(e.map(d => d.value)); + setSelectedDimensions(stagedDimensions); + }} + onMenuClose={() => { + setSelectedDimensions(stagedDimensions); + }} + /> + +

    Filter By

    + setFilters(q)} + /> +
    +
    + {!viewData && !query ? ( +
    +
    Using the SQL Builder
    +

    + The sql builder allows you to group multiple metrics along + with their shared dimensions. Using your selections, + DataJunction will generate the corresponding SQL. +

    +
      +
    1. + Select Metrics: Start by selecting one or more + metrics from the metrics dropdown. +
    2. +
    3. + Select Dimensions: Next, select the dimension + attributes you would like to include. As you select + additional metrics, the list of available dimensions will be + filtered to those shared by the selected metrics. If the + dimensions list is empty, no shared dimensions were + discovered. +
    4. +
    5. + View the generated SQL Query: As you make your + selections, the SQL required to retrieve the set of metrics + and dimensions will be generated below. +
    6. +
    7. + Run the Query: If query running is enabled by your + server, you can also run the generated SQL query to view a + sample of 100 records. +
    8. +
    +
    + ) : ( + <> + )} + {query ? ( + <> + {loadingData ? ( + + {'Running Query'} + + ) : ( + + {'Run Query'} + + )} + {data ? ( + viewData ? ( + <> + + {'View Query'} + + + +
    + + {{ end -}} + +
    + + + {{ if .Site.Params.options.darkMode -}} +
    + + {{ end -}} + + {{ if eq .Site.Params.options.multilingualMode true -}} +
    + + {{ end -}} + + {{ if eq .Site.Params.options.docsVersioning true -}} +
    + + {{ end -}} +
    +
    + + + +{{ if eq .Site.Params.options.navbarSticky true }} +
    +{{ end -}} + +{{ if eq .Section "docs" -}} +
    + +
    + +{{ else if ne .CurrentSection .FirstSection -}} + + +
    + +
    +{{ end -}} diff --git a/docs/layouts/shortcodes/excalidraw.html b/docs/layouts/shortcodes/excalidraw.html new file mode 100644 index 000000000..519051ef0 --- /dev/null +++ b/docs/layouts/shortcodes/excalidraw.html @@ -0,0 +1,32 @@ +
    +
    +
    + \ No newline at end of file diff --git a/docs/layouts/shortcodes/tab.html b/docs/layouts/shortcodes/tab.html new file mode 100644 index 000000000..e4c247c85 --- /dev/null +++ b/docs/layouts/shortcodes/tab.html @@ -0,0 +1,12 @@ +{{ if .Parent }} + {{ $name := .Get 0 }} + {{ $group := printf "tabs-%s" (.Parent.Get 0) }} + + {{ if not (.Parent.Scratch.Get $group) }} + {{ .Parent.Scratch.Set $group slice }} + {{ end }} + + {{ .Parent.Scratch.Add $group (dict "Name" $name "Content" .Inner) }} +{{ else }} + {{ errorf "%q: 'tab' shortcode must be inside 'tabs' shortcode" .Page.Path }} +{{ end}} \ No newline at end of file diff --git a/docs/layouts/shortcodes/tabs.html b/docs/layouts/shortcodes/tabs.html new file mode 100644 index 000000000..01243aba5 --- /dev/null +++ b/docs/layouts/shortcodes/tabs.html @@ -0,0 +1,15 @@ +{{ if .Inner }}{{ end }} +{{ $id := .Get 0 }} +{{ $group := printf "tabs-%s" $id }} + +
    +{{- range $index, $tab := .Scratch.Get $group -}} + + +
    + {{- .Content | $.Page.RenderString -}} +
    +{{- end -}} +
    \ No newline at end of file diff --git a/docs/license.rst b/docs/license.rst deleted file mode 100644 index 3989c5130..000000000 --- a/docs/license.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. _license: - -======= -License -======= - -.. include:: ../LICENSE.txt diff --git a/docs/netlify.toml b/docs/netlify.toml new file mode 100644 index 000000000..cd8864de3 --- /dev/null +++ b/docs/netlify.toml @@ -0,0 +1,41 @@ +[build] + publish = "public" + functions = "functions" + +[build.environment] + NODE_VERSION = "16.16.0" + NPM_VERSION = "8.11.0" + +[context.production] + command = "npm run build" + +[context.deploy-preview] + command = "npm run build -- -b $DEPLOY_URL" + +[context.branch-deploy] + command = "npm run build -- -b $DEPLOY_URL" + +[context.next] + command = "npm run build" + +[context.next.environment] + HUGO_ENV = "next" + +[[plugins]] + package = "netlify-plugin-submit-sitemap" + + [plugins.inputs] + baseUrl = "https://datajunction.io/" + sitemapPath = "/sitemap.xml" + ignorePeriod = 0 + providers = [ + "google", + ] + +[dev] + framework = "#custom" + command = "npm run start" + targetPort = 1313 + port = 8888 + publish = "public" + autoLaunch = false diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 000000000..feaadf6d2 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,8445 @@ +{ + "name": "@hyas/doks", + "version": "0.5.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@hyas/doks", + "version": "0.5.0", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "mermaid": "^10.5.0" + }, + "devDependencies": { + "@babel/cli": "^7.18", + "@babel/core": "^7.18", + "@babel/preset-env": "^7.18", + "@fullhuman/postcss-purgecss": "^4.1", + "@hyas/images": "^0.2.2", + "auto-changelog": "^2.4", + "autoprefixer": "^10.4", + "bootstrap": "^5.2.0-beta1", + "clipboard": "^2.0", + "eslint": "^8.19", + "exec-bin": "^1.0.0", + "flexsearch": "^0.7.21", + "highlight.js": "^11.5", + "hugo-installer": ">=4.0.1", + "instant.page": "^5.1", + "katex": "^0.16", + "lazysizes": "^5.3", + "markdownlint-cli2": "^0.4.0", + "netlify-plugin-submit-sitemap": "^0.4.0", + "node-fetch": "^3.2", + "postcss": "^8.4", + "postcss-cli": "^10.0", + "purgecss-whitelister": "^2.4", + "shx": "^0.3.4", + "stylelint": "^14.9", + "stylelint-config-standard-scss": "^4.0" + }, + "engines": { + "node": ">=16.16.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/cli": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.22.10.tgz", + "integrity": "sha512-rM9ZMmaII630zGvtMtQ3P4GyHs28CHLYE9apLG7L8TgaSqcfoIGrlLSLsh4Q8kDTdZQQEXZm1M0nQtOvU/2heg==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.2.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0" + }, + "bin": { + "babel": "bin/babel.js", + "babel-external-helpers": "bin/babel-external-helpers.js" + }, + "engines": { + "node": ">=6.9.0" + }, + "optionalDependencies": { + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", + "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.10", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.10.tgz", + "integrity": "sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.10", + "@babel/generator": "^7.22.10", + "@babel/helper-compilation-targets": "^7.22.10", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.10", + "@babel/parser": "^7.22.10", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.10", + "@babel/types": "^7.22.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", + "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.10", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz", + "integrity": "sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", + "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz", + "integrity": "sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz", + "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", + "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz", + "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", + "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz", + "integrity": "sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.10.tgz", + "integrity": "sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.10", + "@babel/types": "^7.22.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", + "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.10.tgz", + "integrity": "sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", + "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.10.tgz", + "integrity": "sha512-eueE8lvKVzq5wIObKK/7dvoeKJ+xc6TvRn6aysIjS6pSCeLy7S/eVi7pEQknZqyqvzaNKdDtem8nUNTBgDVR2g==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz", + "integrity": "sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", + "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz", + "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz", + "integrity": "sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", + "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", + "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", + "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", + "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", + "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", + "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", + "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", + "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", + "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", + "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", + "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.10.tgz", + "integrity": "sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", + "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", + "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", + "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", + "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.10.tgz", + "integrity": "sha512-riHpLb1drNkpLlocmSyEg4oYJIQFeXAK/d7rI6mbD0XsvoTOOweXDmQPG/ErxsEhWk3rl3Q/3F6RFQlVFS8m0A==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.10", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.10", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.10", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.5", + "@babel/plugin-transform-classes": "^7.22.6", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.10", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.5", + "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", + "@babel/plugin-transform-numeric-separator": "^7.22.5", + "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.10", + "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "@babel/types": "^7.22.10", + "babel-plugin-polyfill-corejs2": "^0.4.5", + "babel-plugin-polyfill-corejs3": "^0.8.3", + "babel-plugin-polyfill-regenerator": "^0.5.2", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", + "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.10.tgz", + "integrity": "sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.10", + "@babel/generator": "^7.22.10", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.10", + "@babel/types": "^7.22.10", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", + "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==" + }, + "node_modules/@csstools/selector-specificity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz", + "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz", + "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fullhuman/postcss-purgecss": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-4.1.3.tgz", + "integrity": "sha512-jqcsyfvq09VOsMXxJMPLRF6Fhg/NNltzWKnC9qtzva+QKTxerCO4esG6je7hbnmkpZtaDyPTwMBj9bzfWorsrw==", + "dev": true, + "dependencies": { + "purgecss": "^4.1.3" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@hyas/images": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@hyas/images/-/images-0.2.3.tgz", + "integrity": "sha512-PoGQ8DC3erHHS9sAlsrrGOlfqV3RgmIFYhSWcigB/7tE6tUNj4j57T4MjmlEyNcawlVsHvCaoYqPaElQn24aaQ==", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "dev": true, + "optional": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/d3-scale": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz", + "integrity": "sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==" + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==" + }, + "node_modules/@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "dev": true + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mdast": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz", + "integrity": "sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, + "node_modules/@types/node": { + "version": "20.4.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.8.tgz", + "integrity": "sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg==", + "dev": true + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz", + "integrity": "sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==" + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", + "integrity": "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==", + "dev": true, + "dependencies": { + "clean-stack": "^4.0.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/auto-changelog": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.4.0.tgz", + "integrity": "sha512-vh17hko1c0ItsEcw6m7qPRf3m45u+XK5QyCrrBFViElZ8jnKrPC1roSznrd1fIB/0vR/zawdECCRJtTuqIXaJw==", + "dev": true, + "dependencies": { + "commander": "^7.2.0", + "handlebars": "^4.7.7", + "node-fetch": "^2.6.1", + "parse-github-url": "^1.0.2", + "semver": "^7.3.5" + }, + "bin": { + "auto-changelog": "src/index.js" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/auto-changelog/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/auto-changelog/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/auto-changelog/node_modules/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/auto-changelog/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/auto-changelog/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", + "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", + "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.2", + "core-js-compat": "^3.31.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", + "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bootstrap": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.1.tgz", + "integrity": "sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true + }, + "node_modules/cacheable-lookup": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.1.0.tgz", + "integrity": "sha512-KJ/Dmo1lDDhmW2XDPMo+9oiy/CeqosPguPCrgcVzKyZrL6pM1gU2GmPY/xo6OQPTUaA/c0kwHuywB4E6nmT9ww==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001519", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz", + "integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/clean-stack": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", + "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clean-stack/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "dev": true, + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/core-js-compat": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.0.tgz", + "integrity": "sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.9" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-functions-list": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.0.tgz", + "integrity": "sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg==", + "dev": true, + "engines": { + "node": ">=12.22" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cytoscape": { + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.28.1.tgz", + "integrity": "sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==", + "dependencies": { + "heap": "^0.2.6", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/d3": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", + "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/dayjs": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", + "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/del": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-7.0.0.tgz", + "integrity": "sha512-tQbV/4u5WVB8HMJr08pgw0b6nG4RGt/tj+7Numvq+zqcvUFeMaIWWOUFltiU+6go8BSO2/ogsB4EasDaj0y68Q==", + "dev": true, + "dependencies": { + "globby": "^13.1.2", + "graceful-fs": "^4.2.10", + "is-glob": "^4.0.3", + "is-path-cwd": "^3.0.0", + "is-path-inside": "^4.0.0", + "p-map": "^5.5.0", + "rimraf": "^3.0.2", + "slash": "^4.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del/node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "dev": true + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A==" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.486", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.486.tgz", + "integrity": "sha512-9zn9/2lbMGY/mFhoUymD9ODYdLY3zjUW/IW9ihU/sJVeIlD70m2aAb86S35aRGF+iwqLuQP25epruayZjKNjBw==", + "dev": true + }, + "node_modules/elkjs": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.2.tgz", + "integrity": "sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", + "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.1", + "@eslint/js": "^8.46.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.2", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", + "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/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==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exec-bin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/exec-bin/-/exec-bin-1.0.0.tgz", + "integrity": "sha512-p8f8h8b6op2nR7U5rsd+zACUMfsfB+jW8HNIBD2njOQ/gF2WvBfQRo/OU6Q6f/b34WLAyePZcwMJyrDdEjB/fw==", + "dev": true, + "bin": { + "exec-bin": "bin/exec-bin.js" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/flexsearch": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.7.31.tgz", + "integrity": "sha512-XGozTsMPYkm+6b5QL3Z9wQcJjNYxp0CYn3U1gO7dwD6PAqU1SVWZxI9CCg3z+ml3YfqdPnrBehaBrnH2AGKbNA==", + "dev": true + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "dev": true, + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-all": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/glob-all/-/glob-all-3.3.1.tgz", + "integrity": "sha512-Y+ESjdI7ZgMwfzanHZYQ87C59jOO0i+Hd+QYtVt9PhLi6d8wlOpzQnfBxWUlaTuAoR3TkybLqqbIoWveU4Ji7Q==", + "dev": true, + "dependencies": { + "glob": "^7.2.3", + "yargs": "^15.3.1" + }, + "bin": { + "glob-all": "bin/glob-all" + } + }, + "node_modules/glob-all/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/glob-all/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/glob-all/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/glob-all/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==", + "dev": true + }, + "node_modules/glob-all/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/glob-all/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/glob-all/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-all/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/glob-all/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/glob-all/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/glob-all/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/glob-all/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true + }, + "node_modules/gonzales-pe": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", + "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "gonzales": "bin/gonzales.js" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "dev": true, + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/got": { + "version": "12.4.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.4.1.tgz", + "integrity": "sha512-Sz1ojLt4zGNkcftIyJKnulZT/yEDvifhUjccHA8QzOuTgPs/+njXYNMFE3jR4/2OODQSSbH8SdnoLCkbh41ieA==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "@types/cacheable-request": "^6.0.2", + "cacheable-lookup": "^6.0.4", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.0", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==" + }, + "node_modules/highlight.js": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", + "integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/hpagent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.0.0.tgz", + "integrity": "sha512-SCleE2Uc1bM752ymxg8QXYGW0TWtAV4ZW3TqH1aOnyi6T6YW2xadCcclm5qeVjvMvfQ2RKNtZxO7uVb9CTPt1A==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http2-wrapper": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", + "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/hugo-installer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/hugo-installer/-/hugo-installer-4.0.1.tgz", + "integrity": "sha512-pkp1RO7+ekQ0vw1aqgBMK+dD2dqioIWVbwWKsJsKLOpzfFc78gK68Cweoi/g+CftoiMFO7cyGx/2MgkHCMqaLQ==", + "dev": true, + "dependencies": { + "decompress": "4.2.x", + "del": "7.0.x", + "got": "12.4.x", + "hpagent": "1.0.x", + "object-path": "0.11.x", + "semver": "7.3.x", + "yargs": "17.5.x" + }, + "bin": { + "hugo-installer": "bin/hugo-installer.js" + } + }, + "node_modules/hugo-installer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hugo-installer/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hugo-installer/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/instant.page": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/instant.page/-/instant.page-5.2.0.tgz", + "integrity": "sha512-DUSwWyoHFOQnmEwJtg9vzDx8Ef8uNNvTxTmHjd0vN9/XEIb5EQkm/itpZMypoH3dJLJvtkrD97WOCKuMqDdMHQ==", + "dev": true + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/invariant": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha512-FUiAFCOgp7bBzHfa/fK+Uc/vqywvdN9Wg3CiTprLcE630mrhxjDS5MlBkHzeI6+bC/6bq9VX/hxBt05fPAT5WA==", + "dev": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz", + "integrity": "sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/katex": { + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz", + "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/khroma": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", + "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.26.0.tgz", + "integrity": "sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg==", + "dev": true + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==" + }, + "node_modules/lazysizes": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/lazysizes/-/lazysizes-5.3.2.tgz", + "integrity": "sha512-22UzWP+Vedi/sMeOr8O7FWimRVtiNJV2HCa+V8+peZOw6QbswN9k58VUhd7i6iK5bw5QkYrF01LJbeJe0PV8jg==", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdownlint": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.25.1.tgz", + "integrity": "sha512-AG7UkLzNa1fxiOv5B+owPsPhtM4D6DoODhsJgiaNg1xowXovrYgOnLqAgOOFQpWOlHFVQUzjMY5ypNNTeov92g==", + "dev": true, + "dependencies": { + "markdown-it": "12.3.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/markdownlint-cli2": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.4.0.tgz", + "integrity": "sha512-EcwP5tAbyzzL3ACI0L16LqbNctmh8wNX56T+aVvIxWyTAkwbYNx2V7IheRkXS3mE7R/pnaApZ/RSXcXuzRVPjg==", + "dev": true, + "dependencies": { + "globby": "12.1.0", + "markdownlint": "0.25.1", + "markdownlint-cli2-formatter-default": "0.0.3", + "markdownlint-rule-helpers": "0.16.0", + "micromatch": "4.0.4", + "strip-json-comments": "4.0.0", + "yaml": "1.10.2" + }, + "bin": { + "markdownlint-cli2": "markdownlint-cli2.js", + "markdownlint-cli2-config": "markdownlint-cli2-config.js", + "markdownlint-cli2-fix": "markdownlint-cli2-fix.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/markdownlint-cli2-formatter-default": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.3.tgz", + "integrity": "sha512-QEAJitT5eqX1SNboOD+SO/LNBpu4P4je8JlR02ug2cLQAqmIhh8IJnSK7AcaHBHhNADqdGydnPpQOpsNcEEqCw==", + "dev": true, + "peerDependencies": { + "markdownlint-cli2": ">=0.0.4" + } + }, + "node_modules/markdownlint-cli2/node_modules/globby": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-12.1.0.tgz", + "integrity": "sha512-YULDaNwsoUZkRy9TWSY/M7Obh0abamTKoKzTfOI3uU+hfpX2FZqOq8LFDxsjYheF1RH7ITdArgbQnsNBFgcdBA==", + "dev": true, + "dependencies": { + "array-union": "^3.0.1", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.7", + "ignore": "^5.1.9", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdownlint-cli2/node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/markdownlint-cli2/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdownlint-cli2/node_modules/strip-json-comments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-4.0.0.tgz", + "integrity": "sha512-LzWcbfMbAsEDTRmhjWIioe8GcDRl0fa35YMXFoJKDdiD/quGFmjJjdgPjFJJNwCMaLyQqFIDqCdHD2V4HfLgYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdownlint-rule-helpers": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.16.0.tgz", + "integrity": "sha512-oEacRUVeTJ5D5hW1UYd2qExYI0oELdYK72k1TKGvIeYJIbqQWAz476NAc7LNixSySUhcNl++d02DvX0ccDk9/w==", + "dev": true + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.0.tgz", + "integrity": "sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==", + "dependencies": { + "@braintree/sanitize-url": "^6.0.1", + "@types/d3-scale": "^4.0.3", + "@types/d3-scale-chromatic": "^3.0.0", + "cytoscape": "^3.28.1", + "cytoscape-cose-bilkent": "^4.1.0", + "d3": "^7.4.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.10", + "dayjs": "^1.11.7", + "dompurify": "^3.0.5", + "elkjs": "^0.9.0", + "katex": "^0.16.9", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "mdast-util-from-markdown": "^1.3.0", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.3", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/netlify-plugin-submit-sitemap": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/netlify-plugin-submit-sitemap/-/netlify-plugin-submit-sitemap-0.4.0.tgz", + "integrity": "sha512-5ntDtSKZRHaCDrDXh4sH4V7lNEEsoi01lsmSUuqJ/ikPHf0XEErjsKba8TsM3iaZRYEHI9bQse3BWgguwuwIIQ==", + "dev": true, + "dependencies": { + "node-fetch": "^3.2.3" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==" + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-path": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz", + "integrity": "sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==", + "dev": true, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "dev": true, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-5.5.0.tgz", + "integrity": "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==", + "dev": true, + "dependencies": { + "aggregate-error": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-github-url": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", + "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==", + "dev": true, + "bin": { + "parse-github-url": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.4.27", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", + "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-cli": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-10.1.0.tgz", + "integrity": "sha512-Zu7PLORkE9YwNdvOeOVKPmWghprOtjFQU3srMUGbdz3pHJiFh7yZ4geiZFMkjMfB0mtTFR3h8RemR62rPkbOPA==", + "dev": true, + "dependencies": { + "chokidar": "^3.3.0", + "dependency-graph": "^0.11.0", + "fs-extra": "^11.0.0", + "get-stdin": "^9.0.0", + "globby": "^13.0.0", + "picocolors": "^1.0.0", + "postcss-load-config": "^4.0.0", + "postcss-reporter": "^7.0.0", + "pretty-hrtime": "^1.0.3", + "read-cache": "^1.0.0", + "slash": "^5.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "postcss": "index.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-cli/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true + }, + "node_modules/postcss-reporter": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.0.5.tgz", + "integrity": "sha512-glWg7VZBilooZGOFPhN9msJ3FQs19Hie7l5a/eE6WglzYqVeH3ong3ShFcp9kDWJT1g2Y/wd59cocf9XxBtkWA==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "thenby": "^1.3.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", + "integrity": "sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==", + "dev": true + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz", + "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + } + ], + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.19" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/purgecss": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.1.3.tgz", + "integrity": "sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==", + "dev": true, + "dependencies": { + "commander": "^8.0.0", + "glob": "^7.1.7", + "postcss": "^8.3.5", + "postcss-selector-parser": "^6.0.6" + }, + "bin": { + "purgecss": "bin/purgecss.js" + } + }, + "node_modules/purgecss-whitelister": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/purgecss-whitelister/-/purgecss-whitelister-2.4.0.tgz", + "integrity": "sha512-O0jBUDtY9dU9tUT0vA1FvwFdkKDerxzteYaBV49JCbm+QJLFKMlIsf5Kp5cdbLatHQNjJtV8VB8eXtISoZL2Dg==", + "dev": true, + "dependencies": { + "glob-all": "^3.1.0", + "gonzales-pe": "^4.2.4", + "scss-parser": "1.0.3" + } + }, + "node_modules/purgecss/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redent/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "dev": true, + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/scss-parser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/scss-parser/-/scss-parser-1.0.3.tgz", + "integrity": "sha512-XQKCfOJERmhn1yoNRUyxv9wgkf4DIv29Jk0m4FiZforeiCmGxrby8K3not7tQ8GK1yvtd9N0OnNimNetJ8V+zQ==", + "dev": true, + "dependencies": { + "invariant": "2.2.2", + "lodash": "^4.17.4" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/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 + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", + "dev": true + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/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==", + "dev": true + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "dependencies": { + "is-natural-number": "^4.0.1" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", + "dev": true + }, + "node_modules/stylelint": { + "version": "14.16.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.16.1.tgz", + "integrity": "sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.2", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^7.1.0", + "css-functions-list": "^3.1.0", + "debug": "^4.3.4", + "fast-glob": "^3.2.12", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^6.0.1", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.2.0", + "ignore": "^5.2.1", + "import-lazy": "^4.0.0", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.26.0", + "mathml-tag-names": "^2.1.3", + "meow": "^9.0.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.19", + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "style-search": "^0.1.0", + "supports-hyperlinks": "^2.3.0", + "svg-tags": "^1.0.0", + "table": "^6.8.1", + "v8-compile-cache": "^2.3.0", + "write-file-atomic": "^4.0.2" + }, + "bin": { + "stylelint": "bin/stylelint.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz", + "integrity": "sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==", + "dev": true, + "peerDependencies": { + "stylelint": "^14.4.0" + } + }, + "node_modules/stylelint-config-recommended-scss": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-6.0.0.tgz", + "integrity": "sha512-6QOe2/OzXV2AP5FE12A7+qtKdZik7Saf42SMMl84ksVBBPpTdrV+9HaCbPYiRMiwELY9hXCVdH4wlJ+YJb5eig==", + "dev": true, + "dependencies": { + "postcss-scss": "^4.0.2", + "stylelint-config-recommended": "^7.0.0", + "stylelint-scss": "^4.0.0" + }, + "peerDependencies": { + "stylelint": "^14.4.0" + } + }, + "node_modules/stylelint-config-standard": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-25.0.0.tgz", + "integrity": "sha512-21HnP3VSpaT1wFjFvv9VjvOGDtAviv47uTp3uFmzcN+3Lt+RYRv6oAplLaV51Kf792JSxJ6svCJh/G18E9VnCA==", + "dev": true, + "dependencies": { + "stylelint-config-recommended": "^7.0.0" + }, + "peerDependencies": { + "stylelint": "^14.4.0" + } + }, + "node_modules/stylelint-config-standard-scss": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-standard-scss/-/stylelint-config-standard-scss-4.0.0.tgz", + "integrity": "sha512-xizu8PTEyB6zYXBiVg6VtvUYn9m57x+6ZtaOdaxsfpbe5eagLPGNlbYnKfm/CfN69ArUpnwR6LjgsTHzlGbtXQ==", + "dev": true, + "dependencies": { + "stylelint-config-recommended-scss": "^6.0.0", + "stylelint-config-standard": "^25.0.0" + }, + "peerDependencies": { + "stylelint": "^14.4.0" + } + }, + "node_modules/stylelint-scss": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-4.7.0.tgz", + "integrity": "sha512-TSUgIeS0H3jqDZnby1UO1Qv3poi1N8wUYIJY6D1tuUq2MN3lwp/rITVo0wD+1SWTmRm0tNmGO0b7nKInnqF6Hg==", + "dev": true, + "dependencies": { + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "stylelint": "^14.5.1 || ^15.0.0" + } + }, + "node_modules/stylelint/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true + }, + "node_modules/stylelint/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylis": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz", + "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/table": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenby": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz", + "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "dev": true + }, + "node_modules/to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/web-worker": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", + "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/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==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..b56591f18 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,74 @@ +{ + "name": "@hyas/doks", + "description": "Doks theme", + "version": "0.5.0", + "engines": { + "node": ">=16.16.0" + }, + "browserslist": [ + "defaults" + ], + "repository": "https://github.com/h-enk/doks", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "scripts": { + "init": "shx rm -rf .git && git init -b main", + "create": "exec-bin node_modules/.bin/hugo/hugo new", + "prestart": "npm run clean", + "start": "exec-bin node_modules/.bin/hugo/hugo server --bind=0.0.0.0 --disableFastRender", + "prebuild": "npm run clean", + "build": "exec-bin node_modules/.bin/hugo/hugo --gc --minify", + "build:preview": "npm run build -D -F", + "clean": "shx rm -rf public resources", + "clean:install": "shx rm -rf package-lock.json node_modules ", + "lint": "npm run -s lint:scripts && npm run -s lint:styles && npm run -s lint:markdown", + "lint:scripts": "eslint assets/js config functions", + "lint:styles": "stylelint \"assets/scss/**/*.{css,sass,scss,sss,less}\"", + "lint:markdown": "markdownlint-cli2 \"*.md\" \"content/**/*.md\"", + "lint:markdown-fix": "markdownlint-cli2-fix \"*.md\" \"content/**/*.md\"", + "server": "exec-bin node_modules/.bin/hugo/hugo server", + "test": "npm run -s lint", + "env": "env", + "precheck": "npm version", + "check": "exec-bin node_modules/.bin/hugo/hugo version", + "copy:katex-fonts": "shx cp ./node_modules/katex/dist/fonts/* ./static/fonts/", + "postinstall": "hugo-installer --version otherDependencies.hugo --extended --destination node_modules/.bin/hugo", + "version": "auto-changelog -p && git add CHANGELOG.md" + }, + "devDependencies": { + "@babel/cli": "^7.18", + "@babel/core": "^7.18", + "@babel/preset-env": "^7.18", + "@fullhuman/postcss-purgecss": "^4.1", + "@hyas/images": "^0.2.2", + "auto-changelog": "^2.4", + "autoprefixer": "^10.4", + "bootstrap": "^5.2.0-beta1", + "clipboard": "^2.0", + "eslint": "^8.19", + "exec-bin": "^1.0.0", + "flexsearch": "^0.7.21", + "highlight.js": "^11.5", + "hugo-installer": ">=4.0.1", + "instant.page": "^5.1", + "katex": "^0.16", + "lazysizes": "^5.3", + "markdownlint-cli2": "^0.4.0", + "netlify-plugin-submit-sitemap": "^0.4.0", + "node-fetch": "^3.2", + "postcss": "^8.4", + "postcss-cli": "^10.0", + "purgecss-whitelister": "^2.4", + "shx": "^0.3.4", + "stylelint": "^14.9", + "stylelint-config-standard-scss": "^4.0" + }, + "otherDependencies": { + "hugo": "0.107.0" + }, + "dependencies": { + "mermaid": "^10.5.0" + } +} diff --git a/docs/readme.rst b/docs/readme.rst deleted file mode 100644 index 81995ef41..000000000 --- a/docs/readme.rst +++ /dev/null @@ -1,2 +0,0 @@ -.. _readme: -.. include:: ../README.rst diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 2ddf98aff..000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Requirements file for ReadTheDocs, check .readthedocs.yml. -# To build the module reference correctly, make sure every external package -# under `install_requires` in `setup.cfg` is also listed here! -sphinx>=3.2.1 -# sphinx_rtd_theme diff --git a/docs/static/css/fruity.css b/docs/static/css/fruity.css new file mode 100644 index 000000000..3afda5a53 --- /dev/null +++ b/docs/static/css/fruity.css @@ -0,0 +1,467 @@ +@media (prefers-color-scheme: dark) { + pre { + line-height: 125%; + } + + td.linenos .normal { + color: inherit; + background-color: transparent; + padding-left: 5px; + padding-right: 5px; + } + + span.linenos { + color: inherit; + background-color: transparent; + padding-left: 5px; + padding-right: 5px; + } + + td.linenos .special { + color: #000000; + background-color: #ffffc0; + padding-left: 5px; + padding-right: 5px; + } + + span.linenos.special { + color: #000000; + background-color: #ffffc0; + padding-left: 5px; + padding-right: 5px; + } + + .code .hll { + background-color: #333333 + } + + .code { + background: #111111; + color: #000103 + } + + .code .comment { + color: #008800; + font-style: italic; + background-color: #0f140f + } + + /* Comment */ + .code .error { + color: #ffffff + } + + /* Error */ + .code .escape { + color: #ffffff + } + + /* Escape */ + .code .generic { + color: #ffffff + } + + /* Generic */ + .code .keyword { + color: #fb660a; + font-weight: bold + } + + /* Keyword */ + .code .literal { + color: #ffffff + } + + /* Literal */ + .code .name { + color: #ffffff + } + + /* Name */ + .code .operator { + color: #ffffff + } + + /* Operator */ + .code .other { + color: #ffffff + } + + /* Other */ + .code .punctuation { + color: #ffffff + } + + /* Punctuation */ + .code .comment.hashbang { + color: #008800; + font-style: italic; + background-color: #0f140f + } + + /* Comment.Hashbang */ + .code .comment.multiline { + color: #008800; + font-style: italic; + background-color: #0f140f + } + + /* Comment.Multiline */ + .code .comment.preproc { + color: #ff0007; + font-weight: bold; + font-style: italic; + background-color: #0f140f + } + + /* Comment.Preproc */ + .code .comment.preprocfile { + color: #008800; + font-style: italic; + background-color: #0f140f + } + + /* Comment.PreprocFile */ + .code .comment.single { + color: #008800; + font-style: italic; + background-color: #0f140f + } + + /* Comment.Single */ + .code .comment.special { + color: #008800; + font-style: italic; + background-color: #0f140f + } + + /* Comment.Special */ + .code .generic.deleted { + color: #ffffff + } + + /* Generic.Deleted */ + .code .generic.emph { + color: #ffffff + } + + /* Generic.Emph */ + .code .generic.error { + color: #ffffff + } + + /* Generic.Error */ + .code .generic.heading { + color: #ffffff; + font-weight: bold + } + + /* Generic.Heading */ + .code .generic.inserted { + color: #ffffff + } + + /* Generic.Inserted */ + .code .generic.output { + color: #444444; + background-color: #222222 + } + + /* Generic.Output */ + .code .generic.prompt { + color: #ffffff + } + + /* Generic.Prompt */ + .code .generic.string { + color: #ffffff + } + + /* Generic.Strong */ + .code .generic.subheading { + color: #ffffff; + font-weight: bold + } + + /* Generic.Subheading */ + .code .generic.traceback { + color: #ffffff + } + + /* Generic.Traceback */ + .code .keyword.constant { + color: #fb660a; + font-weight: bold + } + + /* Keyword.Constant */ + .code .keyword.declaration { + color: #fb660a; + font-weight: bold + } + + /* Keyword.Declaration */ + .code .keyword.namespace { + color: #fb660a; + font-weight: bold + } + + /* Keyword.Namespace */ + .code .keyword.pseudo { + color: #fb660a + } + + /* Keyword.Pseudo */ + .code .keyword.reserved { + color: #fb660a; + font-weight: bold + } + + /* Keyword.Reserved */ + .code .keyword.type { + color: #cdcaa9; + font-weight: bold + } + + /* Keyword.Type */ + .code .literal.date { + color: #ffffff + } + + /* Literal.Date */ + .code .literal.number { + color: #0086f7; + font-weight: bold + } + + /* Literal.Number */ + .code .literal.string { + color: #0086d2 + } + + /* Literal.String */ + .code .name.attribute { + color: #ff0086; + font-weight: bold + } + + /* Name.Attribute */ + .code .name.builtin { + color: #ffffff + } + + /* Name.Builtin */ + .code .name.class { + color: #ffffff + } + + /* Name.Class */ + .code .name.constant { + color: #0086d2 + } + + /* Name.Constant */ + .code .name.decorator { + color: #ffffff + } + + /* Name.Decorator */ + .code .name.entity { + color: #ffffff + } + + /* Name.Entity */ + .code .name.exception { + color: #ffffff + } + + /* Name.Exception */ + .code .name.function { + color: #ff0086; + font-weight: bold + } + + /* Name.Function */ + .code .name.label { + color: #ffffff + } + + /* Name.Label */ + .code .name.namespace { + color: #ffffff + } + + /* Name.Namespace */ + .code .name.other { + color: #ffffff + } + + /* Name.Other */ + .code .name.property { + color: #ffffff + } + + /* Name.Property */ + .code .name.tag { + color: #fb660a; + font-weight: bold + } + + /* Name.Tag */ + .code .name.variable { + color: #fb660a + } + + /* Name.Variable */ + .code .operator.word { + color: #ffffff + } + + /* Operator.Word */ + .code .punctuation.marker { + color: #ffffff + } + + /* Punctuation.Marker */ + .code .text.whitespace { + color: #888888 + } + + /* Text.Whitespace */ + .code .literal.number.bin { + color: #0086f7; + font-weight: bold + } + + /* Literal.Number.Bin */ + .code .literal.number.float { + color: #0086f7; + font-weight: bold + } + + /* Literal.Number.Float */ + .code .literal.number.hex { + color: #0086f7; + font-weight: bold + } + + /* Literal.Number.Hex */ + .code .literal.number.integer { + color: #0086f7; + font-weight: bold + } + + /* Literal.Number.Integer */ + .code .literal.number.oct { + color: #0086f7; + font-weight: bold + } + + /* Literal.Number.Oct */ + .code .literal.string.affix { + color: #0086d2 + } + + /* Literal.String.Affix */ + .code .literal.string.backtick { + color: #0086d2 + } + + /* Literal.String.Backtick */ + .code .literal.string.char { + color: #0086d2 + } + + /* Literal.String.Char */ + .code .literal.string.delimiter { + color: #0086d2 + } + + /* Literal.String.Delimiter */ + .code .literal.string.doc { + color: #0086d2 + } + + /* Literal.String.Doc */ + .code .literal.string.double { + color: #0086d2 + } + + /* Literal.String.Double */ + .code .literal.string.escape { + color: #0086d2 + } + + /* Literal.String.Escape */ + .code .literal.string.heredoc { + color: #0086d2 + } + + /* Literal.String.Heredoc */ + .code .literal.string.interpol { + color: #0086d2 + } + + /* Literal.String.Interpol */ + .code .literal.string.other { + color: #0086d2 + } + + /* Literal.String.Other */ + .code .literal.string.regex { + color: #0086d2 + } + + /* Literal.String.Regex */ + .code .literal.string.single { + color: #0086d2 + } + + /* Literal.String.Single */ + .code .literal.string.symbol { + color: #0086d2 + } + + /* Literal.String.Symbol */ + .code .name.builtin.pseudo { + color: #ffffff + } + + /* Name.Builtin.Pseudo */ + .code .name.function.magic { + color: #ff0086; + font-weight: bold + } + + /* Name.Function.Magic */ + .code .name.variable.class { + color: #fb660a + } + + /* Name.Variable.Class */ + .code .name.variable.global { + color: #fb660a + } + + /* Name.Variable.Global */ + .code .name.variable.instance { + color: #fb660a + } + + /* Name.Variable.Instance */ + .code .name.variable.magic { + color: #fb660a + } + + /* Name.Variable.Magic */ + .code .literal.number.integer.long { + color: #0086f7; + font-weight: bold + } + + /* Literal.Number.Integer.Long */ +} + +@media (prefers-color-scheme: light) { + +} \ No newline at end of file diff --git a/docs/static/excalidraw-drawings/connecting_a_dimension.excalidraw b/docs/static/excalidraw-drawings/connecting_a_dimension.excalidraw new file mode 100644 index 000000000..75a5c2235 --- /dev/null +++ b/docs/static/excalidraw-drawings/connecting_a_dimension.excalidraw @@ -0,0 +1,1231 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "type": "rectangle", + "version": 1182, + "versionNonce": 1395564783, + "isDeleted": false, + "id": "5EOFz6wOaPIc4OfcA41g3", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 39.36897700195311, + "y": 39.85265009765624, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 284.375, + "height": 402.58203125, + "seed": 1655961705, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1681681793165, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 513, + "versionNonce": 1541367041, + "isDeleted": false, + "id": "CHONEkmAysjDgfSdhbzWW", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 12.012806347656237, + "strokeColor": "#e67700", + "backgroundColor": "transparent", + "width": 105.09991455078125, + "height": 25, + "seed": 1347093321, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "hard_hats", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "hard_hats", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 585, + "versionNonce": 1609967503, + "isDeleted": false, + "id": "aoumU95vXKKM1vGYmyL32", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 42.80397221304088, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 128.90625, + "height": 24, + "seed": 1350414889, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "hard_hat_id", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "hard_hat_id", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 619, + "versionNonce": 1603083489, + "isDeleted": false, + "id": "NUBf02TumRi-okLqa0fsZ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 74.41545057842552, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 105.46875, + "height": 24, + "seed": 1480033545, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "last_name", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "last_name", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 644, + "versionNonce": 17325487, + "isDeleted": false, + "id": "5WpHt97VgF8yzGzzcKZPH", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 105.61677269381016, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 117.1875, + "height": 24, + "seed": 854850537, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "first_name", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "first_name", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 660, + "versionNonce": 947443905, + "isDeleted": false, + "id": "figU0-sXtu_s4e6QKv9yp", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 136.8180948091948, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 58.59375, + "height": 24, + "seed": 817796809, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "title", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "title", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 676, + "versionNonce": 1252970447, + "isDeleted": false, + "id": "VrC5cTWmnYlHgP_VLbwVt", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 168.01941692457945, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 117.1875, + "height": 24, + "seed": 338875817, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "birth_date", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "birth_date", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 691, + "versionNonce": 406984865, + "isDeleted": false, + "id": "xvL0v-01-J1DmCfd2wy1d", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 199.2207390399641, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 105.46875, + "height": 24, + "seed": 1081882761, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "hire_date", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "hire_date", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 700, + "versionNonce": 386006511, + "isDeleted": false, + "id": "-z-WYGIMIWr4QoBTUh6Ch", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 230.42206115534873, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 82.03125, + "height": 24, + "seed": 659963753, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "address", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "address", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 706, + "versionNonce": 1352162433, + "isDeleted": false, + "id": "NxEAyil4e96080WXuIkm6", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.61940102022606, + "y": 261.62338327073337, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 46.875, + "height": 24, + "seed": 1997302345, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "city", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "city", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 720, + "versionNonce": 1762289679, + "isDeleted": false, + "id": "tYCzYlEvqUxMze1ud-xh3", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 292.824705386118, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 58.59375, + "height": 24, + "seed": 142227753, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "state", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "state", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 738, + "versionNonce": 1362830433, + "isDeleted": false, + "id": "KTW7o4FV5_QPQrdPeZdK3", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 355.2273496168873, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 82.03125, + "height": 24, + "seed": 218835977, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "country", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "country", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 762, + "versionNonce": 1321242159, + "isDeleted": false, + "id": "rcCAxXe-uR77QO6EocRwY", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 324.02602750150265, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 128.90625, + "height": 24, + "seed": 2092614377, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "postal_code", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "postal_code", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 818, + "versionNonce": 1710878785, + "isDeleted": false, + "id": "sjgnOdzyXiiJssRj48G-W", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 386.42867173227194, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 82.03125, + "height": 24, + "seed": 1142193609, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "manager", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "manager", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 840, + "versionNonce": 51179599, + "isDeleted": false, + "id": "VbdcyRIZpcB1me3bCfPhU", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 70.20924477022606, + "y": 417.6299938476566, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 152.34375, + "height": 24, + "seed": 842259625, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "contractor_id", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "contractor_id", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 688, + "versionNonce": 884228129, + "isDeleted": false, + "id": "NHGdIn7HHbBRyH5mAG9Do", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.58424477022606, + "y": 43.21412846304088, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 35.15625, + "height": 24, + "seed": 650403721, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "int", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "int", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 726, + "versionNonce": 1338510959, + "isDeleted": false, + "id": "MHgMMltX8WI6ZO9WO0C0v", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.58424477022606, + "y": 74.41545057842552, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 70.3125, + "height": 24, + "seed": 99945, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "string", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "string", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 752, + "versionNonce": 2069468161, + "isDeleted": false, + "id": "hYfHTXt-u7dqEgcjlaqUK", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.17799477022606, + "y": 105.61677269381016, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 70.3125, + "height": 24, + "seed": 1219940681, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "string", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "string", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 767, + "versionNonce": 800745615, + "isDeleted": false, + "id": "g7XrtLk4CkPFrRi4uI4LV", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.58424477022606, + "y": 136.8180948091948, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 70.3125, + "height": 24, + "seed": 641712169, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "string", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "string", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 781, + "versionNonce": 233401313, + "isDeleted": false, + "id": "VJvs6yylKMzcKQzmIg9Xt", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.58424477022606, + "y": 168.01941692457945, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 46.875, + "height": 24, + "seed": 109564681, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "date", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "date", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 796, + "versionNonce": 815562415, + "isDeleted": false, + "id": "vvH_2yOfukbBEt7JZ84ea", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.58424477022606, + "y": 199.2207390399641, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 46.875, + "height": 24, + "seed": 235571689, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "date", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "date", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 807, + "versionNonce": 343099329, + "isDeleted": false, + "id": "Gml1-tjyrcJ0lOz8wyMMU", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.58424477022606, + "y": 230.42206115534873, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 70.3125, + "height": 24, + "seed": 1171481801, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "string", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "string", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 813, + "versionNonce": 353816783, + "isDeleted": false, + "id": "6tzjLTtPYKTPtjVR_l7CA", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.99440102022606, + "y": 261.62338327073337, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 70.3125, + "height": 24, + "seed": 159245225, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "string", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "string", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 827, + "versionNonce": 152479649, + "isDeleted": false, + "id": "5ssyQQj0wq-L6Etni5GGo", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.58424477022606, + "y": 292.824705386118, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 70.3125, + "height": 24, + "seed": 892700297, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "string", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "string", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 845, + "versionNonce": 918675183, + "isDeleted": false, + "id": "s-BbxSOZ_92aSuf9mj2EP", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.58424477022606, + "y": 355.2273496168873, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 70.3125, + "height": 24, + "seed": 330681705, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "string", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "string", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 869, + "versionNonce": 1979911041, + "isDeleted": false, + "id": "rJ78lDcCKw0hhk_Oq0p6a", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.58424477022606, + "y": 324.02602750150265, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 70.3125, + "height": 24, + "seed": 206190665, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "string", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "string", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 922, + "versionNonce": 673589519, + "isDeleted": false, + "id": "Q18YvErtBHwZxjo_Fm9xP", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.58424477022606, + "y": 386.42867173227194, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 35.15625, + "height": 24, + "seed": 281954089, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "int", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "int", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 944, + "versionNonce": 1937240929, + "isDeleted": false, + "id": "PmSFvl4bWAinktwCd1_XR", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232.58424477022606, + "y": 417.6299938476566, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 35.15625, + "height": 24, + "seed": 1269660169, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "int", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "int", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "rectangle", + "version": 1440, + "versionNonce": 381431599, + "isDeleted": false, + "id": "6ts5qJqdXNKcQ6ywCn9OS", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 394.7635082519531, + "y": 39.44249384765624, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 284.375, + "height": 65.42578125, + "seed": 1363527655, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 702, + "versionNonce": 1707401025, + "isDeleted": false, + "id": "rgGjugXjxf38n2vr3yUqS", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 425.60377602022606, + "y": 11.602650097656237, + "strokeColor": "#e67700", + "backgroundColor": "transparent", + "width": 168.49986267089844, + "height": 25, + "seed": 738481415, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "hard_hat_state", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "hard_hat_state", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 759, + "versionNonce": 1827434831, + "isDeleted": false, + "id": "mKf2_zWbAE1iuj_jWH8vr", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 425.60377602022606, + "y": 42.80397221304088, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 128.90625, + "height": 24, + "seed": 986423145, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "hard_hat_id", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "hard_hat_id", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 816, + "versionNonce": 2068401953, + "isDeleted": false, + "id": "HD2nM14z2-iYebS1lCOru", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 425.60377602022606, + "y": 74.00529432842552, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 93.75, + "height": 24, + "seed": 886426185, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "state_id", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "state_id", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 863, + "versionNonce": 873426799, + "isDeleted": false, + "id": "Nq6L7o-YAUvSUs1MnaujR", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 587.9787760202261, + "y": 42.80397221304088, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 35.15625, + "height": 24, + "seed": 1073530823, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "int", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "int", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 904, + "versionNonce": 1786036993, + "isDeleted": false, + "id": "leX1Igt-4_IJK7LSC0Rt8", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 587.9787760202261, + "y": 74.00529432842552, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 35.15625, + "height": 24, + "seed": 1236537449, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "int", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "int", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "line", + "version": 517, + "versionNonce": 913156495, + "isDeleted": false, + "id": "PHVWVaoPeYgF4Nuc5tjeX", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 324.6385082519531, + "y": 60.50108759765624, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 70.4609375, + "height": 0, + "seed": 1975704809, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1681681680453, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 70.4609375, + 0 + ] + ] + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/docs/static/excalidraw-drawings/datajunction-illustration.excalidraw b/docs/static/excalidraw-drawings/datajunction-illustration.excalidraw new file mode 100644 index 000000000..d361188bc --- /dev/null +++ b/docs/static/excalidraw-drawings/datajunction-illustration.excalidraw @@ -0,0 +1,2793 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "type": "rectangle", + "version": 2969, + "versionNonce": 1091961559, + "isDeleted": false, + "id": "bEcBsiGmXBgXf0y-zKhyk", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 607.207906131677, + "y": 333.1695201479989, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 133.01146480929427, + "height": 139.4194124355071, + "seed": 2089093433, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "KaghMApuAzEgcaSXTTNKr", + "type": "arrow" + } + ], + "updated": 1682708696230, + "link": null, + "locked": false + }, + { + "type": "arrow", + "version": 7909, + "versionNonce": 387813367, + "isDeleted": false, + "id": "KaghMApuAzEgcaSXTTNKr", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 741.2766282909126, + "y": 397.148300198847, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 55.85295879878345, + "height": 0.671195217366801, + "seed": 90274329, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696230, + "link": null, + "locked": false, + "startBinding": { + "elementId": "bEcBsiGmXBgXf0y-zKhyk", + "gap": 1.0572573499414486, + "focus": -0.09277349717962566 + }, + "endBinding": { + "elementId": "c4lsCJzP-72Lt08TucYoK", + "gap": 1.0852566603039135, + "focus": 0.03193551625567926 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 55.85295879878345, + 0.671195217366801 + ] + ] + }, + { + "type": "rectangle", + "version": 973, + "versionNonce": 452360471, + "isDeleted": false, + "id": "c4lsCJzP-72Lt08TucYoK", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 798.21484375, + "y": 287.64453125, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 193.51953125, + "height": 230.12500000000003, + "seed": 524078841, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "KaghMApuAzEgcaSXTTNKr", + "type": "arrow" + }, + { + "id": "WazM5J7lTqqm1W0vFqX_Y", + "type": "arrow" + }, + { + "id": "nxMSq2HZv6nUeEHKHKZBx", + "type": "arrow" + } + ], + "updated": 1682708696230, + "link": null, + "locked": false + }, + { + "type": "image", + "version": 1237, + "versionNonce": 1108688439, + "isDeleted": false, + "id": "_z3EHgUsfcY6oZWs4g0Ut", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 810.9609375, + "y": 293.35546875, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "width": 29.589843750000043, + "height": 29.589843750000043, + "seed": 278562777, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1682708696230, + "link": null, + "locked": false, + "status": "saved", + "fileId": "d8f8c4fa7d49f6533192608108c28b3ec1654cfc", + "scale": [ + 1, + 1 + ] + }, + { + "type": "rectangle", + "version": 1795, + "versionNonce": 1600020311, + "isDeleted": false, + "id": "ZsT9C43aNHwrX-gWjvn9H", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 853.2757517959506, + "y": 307.94140625, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 59647161, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1682708696230, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1874, + "versionNonce": 1965448311, + "isDeleted": false, + "id": "EUWLlNWi729NOTyUfnxNo", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 817.0159581809914, + "y": 330.9202979739289, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 331031961, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1682708696230, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1886, + "versionNonce": 1194440087, + "isDeleted": false, + "id": "Eu-bNCDOKSZ-I6qmvhX4Z", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 880.9161606847579, + "y": 350.3601022796171, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 475335289, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1682708696230, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1953, + "versionNonce": 750054071, + "isDeleted": false, + "id": "Ajz-Twr7z9_FKZkGVMZTA", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 858.9301011198279, + "y": 391.2294840775887, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 1176458073, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1682708696230, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1980, + "versionNonce": 575962071, + "isDeleted": false, + "id": "w0JEWcDtPqsIEvxp1cnKs", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 871.6205433095097, + "y": 468.87160899491073, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 1433239609, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1682708696230, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 2056, + "versionNonce": 472180983, + "isDeleted": false, + "id": "XPDlYEpANxrqw8y1NYD7T", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 817.113184332972, + "y": 490.3129607003748, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 941822233, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 2040, + "versionNonce": 397062679, + "isDeleted": false, + "id": "4knBDbCLuhiY2CxMo-x3_", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 814.1328125, + "y": 403.9905810772948, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 1404274169, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "KaghMApuAzEgcaSXTTNKr", + "type": "arrow" + } + ], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 2113, + "versionNonce": 1843025719, + "isDeleted": false, + "id": "sV7pPcjDRSrpA4utQvidn", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 922.4432306211875, + "y": 386.3269697467296, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 321019609, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 2234, + "versionNonce": 1696094295, + "isDeleted": false, + "id": "w-dNEz2HJzDsmiN5ubH4c", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 905.3666526190564, + "y": 308.5561298297751, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 889396153, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 2327, + "versionNonce": 54080887, + "isDeleted": false, + "id": "QI3Xwz7gM23pzMbvtq6Bo", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 898.128420918461, + "y": 431.4925387093959, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 1501117593, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 2436, + "versionNonce": 528576151, + "isDeleted": false, + "id": "0p1Ralk8rVf69qT8c6oeA", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 928.3800205087455, + "y": 472.91438569072903, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 1345864057, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 2562, + "versionNonce": 883262391, + "isDeleted": false, + "id": "6D5iRM61g4IJ1H6-dkqjC", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 963.6738920569376, + "y": 451.0648995576725, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 646163033, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "nxMSq2HZv6nUeEHKHKZBx", + "type": "arrow" + } + ], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 2628, + "versionNonce": 1925838039, + "isDeleted": false, + "id": "UMVLrcfjP_g8VrKx-FhLF", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 959.6042892674725, + "y": 339.8748858009664, + "strokeColor": "#e67700", + "backgroundColor": "#fab005", + "width": 22.152201072977142, + "height": 23.417645559270948, + "seed": 1966012217, + "groupIds": [], + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "KaghMApuAzEgcaSXTTNKr", + "type": "arrow" + }, + { + "id": "WazM5J7lTqqm1W0vFqX_Y", + "type": "arrow" + } + ], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 600, + "versionNonce": 293814775, + "isDeleted": false, + "id": "hq8k1-IO74Xwe_ayy1iSD", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dotted", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 892.7814333844713, + "y": 374.1487972068604, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 16.91320551921805, + "height": 54.7353198261924, + "seed": 1855825945, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 16.91320551921805, + 54.7353198261924 + ] + ] + }, + { + "type": "line", + "version": 605, + "versionNonce": 1641409303, + "isDeleted": false, + "id": "xMwXMmT6T-jg-PqCIsjsy", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dotted", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 891.0563307259131, + "y": 373.8082071153634, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 13.400281277559316, + "height": 19.327473623410754, + "seed": 750992633, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -13.400281277559316, + 19.327473623410754 + ] + ] + }, + { + "type": "line", + "version": 604, + "versionNonce": 540438583, + "isDeleted": false, + "id": "j9nPi6b_peK71QQ8O81i5", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 838.1518557773572, + "y": 337.51384656105313, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 15.517097587638716, + "height": 7.782981530645991, + "seed": 1059272153, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 15.517097587638716, + -7.782981530645991 + ] + ] + }, + { + "type": "line", + "version": 675, + "versionNonce": 109662551, + "isDeleted": false, + "id": "Z2iaNAVncm7k4FTCBxUYu", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 838.3334656564634, + "y": 349.3478581221981, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 42.02058292606746, + "height": 15.884678152789007, + "seed": 2026967737, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 42.02058292606746, + 15.884678152789007 + ] + ] + }, + { + "type": "line", + "version": 626, + "versionNonce": 505613943, + "isDeleted": false, + "id": "NTmkCL2cb5GNmr25pYSSu", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 875.411338718123, + "y": 321.82529827248845, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 28.778478218931433, + "height": 1.722333633423973, + "seed": 2121290649, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 28.778478218931433, + -1.722333633423973 + ] + ] + }, + { + "type": "line", + "version": 656, + "versionNonce": 1372071831, + "isDeleted": false, + "id": "G1FCnv7tfGVywhaFZAzqk", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 928.53632384756, + "y": 323.4064116240721, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 30.876207903101317, + "height": 18.090311282795597, + "seed": 338983033, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 30.876207903101317, + 18.090311282795597 + ] + ] + }, + { + "type": "line", + "version": 705, + "versionNonce": 1068309687, + "isDeleted": false, + "id": "rfPIydf3KzbxH9jHYMM82", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 917.3759146257441, + "y": 332.98446956300063, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 16.076959928713162, + "height": 53.60555757147057, + "seed": 736166233, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 16.076959928713162, + 53.60555757147057 + ] + ] + }, + { + "type": "line", + "version": 584, + "versionNonce": 1266052567, + "isDeleted": false, + "id": "cL9Qi30gc0CUBw4Vl8k5v", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 836.7889761473873, + "y": 415.5484919871206, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 21.44759234677008, + "height": 12.58688525437276, + "seed": 852061753, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 21.44759234677008, + -12.58688525437276 + ] + ] + }, + { + "type": "line", + "version": 563, + "versionNonce": 1882757879, + "isDeleted": false, + "id": "G-puwHCVcJbG-__oXkKuK", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 836.7086744184978, + "y": 419.30882811925846, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 44.53423123208643, + "height": 48.71269015947674, + "seed": 533806873, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 44.53423123208643, + 48.71269015947674 + ] + ] + }, + { + "type": "line", + "version": 798, + "versionNonce": 1383605271, + "isDeleted": false, + "id": "-5TZqfwyfcNkA88Yd-YWR", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 840.0408041931541, + "y": 504.2501392895201, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 28.631672519199697, + "height": 22.7033904883516, + "seed": 1097593849, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 28.631672519199697, + -22.7033904883516 + ] + ] + }, + { + "type": "line", + "version": 591, + "versionNonce": 304711991, + "isDeleted": false, + "id": "Lixrrc4KM3W2X7Fe677by", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dotted", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 888.6500478843609, + "y": 470.5939426283347, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 12.239091092819873, + "height": 17.073808976997135, + "seed": 1268583641, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 12.239091092819873, + -17.073808976997135 + ] + ] + }, + { + "type": "line", + "version": 656, + "versionNonce": 1555122775, + "isDeleted": false, + "id": "mcu4fajRgtgUnZZ_eUaqq", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dotted", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 963.7370376919966, + "y": 450.27516874356945, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 28.84427006779599, + "height": 40.555474663775996, + "seed": 637012409, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -28.84427006779599, + -40.555474663775996 + ] + ] + }, + { + "type": "line", + "version": 588, + "versionNonce": 1055123319, + "isDeleted": false, + "id": "lFAcatyJ_IkSe3IPSheCX", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 894.4650006660174, + "y": 480.3132208491034, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 33.496897047475564, + "height": 4.5328941445579485, + "seed": 2040556185, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 33.496897047475564, + 4.5328941445579485 + ] + ] + }, + { + "type": "line", + "version": 679, + "versionNonce": 923132055, + "isDeleted": false, + "id": "ALNdp3GJOnoB3PWAUTewd", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 835.319440246748, + "y": 417.88917382826855, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 63.151202563478364, + "height": 16.460998819541413, + "seed": 2135535481, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 63.151202563478364, + 16.460998819541413 + ] + ] + }, + { + "type": "line", + "version": 589, + "versionNonce": 114013623, + "isDeleted": false, + "id": "IVouKXrsI7N93tMXm6349", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 947.4059922052986, + "y": 472.9974564447527, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 16.97866022543542, + "height": 10.386147180679075, + "seed": 221730905, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 16.97866022543542, + -10.386147180679075 + ] + ] + }, + { + "type": "line", + "version": 1388, + "versionNonce": 1163519703, + "isDeleted": false, + "id": "7favyzYf-cu7KaDGdnMfJ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 4.715883031006205, + "x": 1074.2658700014938, + "y": 405.82553702474416, + "strokeColor": "#000000", + "backgroundColor": "#fab005", + "width": 0, + "height": 36.251343509462835, + "seed": 1698489657, + "groupIds": [ + "3WdASU0B1rFSkxHL8Q7w9" + ], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 36.251343509462835 + ] + ] + }, + { + "type": "line", + "version": 1726, + "versionNonce": 826602487, + "isDeleted": false, + "id": "E45QDj6j7EsnzAZo1-NU_", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.0006432738582038411, + "x": 1056.381326000654, + "y": 387.9242875616367, + "strokeColor": "#000000", + "backgroundColor": "#fab005", + "width": 0, + "height": 36.251343509462835, + "seed": 1119737369, + "groupIds": [ + "3WdASU0B1rFSkxHL8Q7w9" + ], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 36.251343509462835 + ] + ] + }, + { + "type": "rectangle", + "version": 1485, + "versionNonce": 1116822807, + "isDeleted": false, + "id": "bRmW9N1YJl_IZwMH2ibCc", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 4.715883031006205, + "x": 1071.014844613885, + "y": 408.9600767293337, + "strokeColor": "#000000", + "backgroundColor": "#fab005", + "width": 25.37594045662398, + "height": 4.960710164452808, + "seed": 682470137, + "groupIds": [ + "3WdASU0B1rFSkxHL8Q7w9" + ], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1556, + "versionNonce": 2092147255, + "isDeleted": false, + "id": "FzQuwqaoK8ru7DZC9rtOE", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 4.715883031006205, + "x": 1057.8429757739166, + "y": 411.0058277044367, + "strokeColor": "#000000", + "backgroundColor": "#fab005", + "width": 21.369213016104403, + "height": 4.769913619666162, + "seed": 1871700953, + "groupIds": [ + "3WdASU0B1rFSkxHL8Q7w9" + ], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1561, + "versionNonce": 9454423, + "isDeleted": false, + "id": "vXUPfyMaqjDWAou7qgKhO", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 4.715883031006205, + "x": 1061.7712509792207, + "y": 406.1712114769129, + "strokeColor": "#000000", + "backgroundColor": "#fab005", + "width": 31.099836800223375, + "height": 4.769913619666162, + "seed": 326631609, + "groupIds": [ + "3WdASU0B1rFSkxHL8Q7w9" + ], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1680, + "versionNonce": 1585168503, + "isDeleted": false, + "id": "gNI3eR04Tni7cej8y0BX7", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 4.715883031006205, + "x": 1055.4443910434977, + "y": 414.8951334999004, + "strokeColor": "#000000", + "backgroundColor": "#fab005", + "width": 13.5465546798519, + "height": 4.769913619666162, + "seed": 1107919257, + "groupIds": [ + "3WdASU0B1rFSkxHL8Q7w9" + ], + "roundness": null, + "boundElements": [ + { + "id": "Cz99G398jjIltSp4A4cIT", + "type": "arrow" + } + ], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "arrow", + "version": 4251, + "versionNonce": 1256294518, + "isDeleted": false, + "id": "WazM5J7lTqqm1W0vFqX_Y", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 991.8454246447372, + "y": 338.47963744818065, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 54.96968294722387, + "height": 17.878475890219136, + "seed": 678946425, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682709037157, + "link": null, + "locked": false, + "startBinding": { + "elementId": "UMVLrcfjP_g8VrKx-FhLF", + "focus": -0.4062572517755502, + "gap": 10.088934304287534 + }, + "endBinding": { + "elementId": "FRRhjBZA9NEVbc2zGIaT1", + "focus": 0.2951129697670072, + "gap": 1.0000000000001137 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 54.96968294722387, + -17.878475890219136 + ] + ] + }, + { + "type": "arrow", + "version": 2130, + "versionNonce": 562717174, + "isDeleted": false, + "id": "Cz99G398jjIltSp4A4cIT", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 991.3299483916, + "y": 408.39344452950974, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 56.35007076735428, + "height": 0.4553861259818177, + "seed": 25603929, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682709040997, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "gNI3eR04Tni7cej8y0BX7", + "focus": 1.2254311353262122, + "gap": 12.182062862176025 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 56.35007076735428, + 0.4553861259818177 + ] + ] + }, + { + "type": "arrow", + "version": 2841, + "versionNonce": 2037669847, + "isDeleted": false, + "id": "nxMSq2HZv6nUeEHKHKZBx", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 991.574157042641, + "y": 476.56153234822114, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 54.02120298156524, + "height": 10.990537914814979, + "seed": 1669946425, + "groupIds": [], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": { + "elementId": "6D5iRM61g4IJ1H6-dkqjC", + "focus": 0.7427135450011365, + "gap": 6.112481218997857 + }, + "endBinding": { + "elementId": "n4lOmlNvNY2eCwh7cNvel", + "focus": -0.21198464665817288, + "gap": 1 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 54.02120298156524, + 10.990537914814979 + ] + ] + }, + { + "type": "image", + "version": 2123, + "versionNonce": 1302875383, + "isDeleted": false, + "id": "Mm2MweLA86XteDguFl_VD", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 619.1814478927992, + "y": 424.4313892477454, + "strokeColor": "transparent", + "backgroundColor": "#82c91e", + "width": 34.55966137800449, + "height": 35.64893437272669, + "seed": 1315138841, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "status": "saved", + "fileId": "16b7eb7d04e59834d78ae80e062df27ea7b4987f", + "scale": [ + 1, + 1 + ] + }, + { + "type": "image", + "version": 2179, + "versionNonce": 186750487, + "isDeleted": false, + "id": "G7AxvzLOnuovLRXaYvNn0", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 617.3999194043165, + "y": 385.13458000165866, + "strokeColor": "transparent", + "backgroundColor": "#82c91e", + "width": 29.27644069772239, + "height": 29.154455528148542, + "seed": 2038981113, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "status": "saved", + "fileId": "f19857abaefea545f9015469988a913b1f3694f1", + "scale": [ + 1, + 1 + ] + }, + { + "type": "image", + "version": 2138, + "versionNonce": 416594743, + "isDeleted": false, + "id": "yynz3JdB5FyWjTA2pl3ch", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 657.4814314397449, + "y": 387.62764161148294, + "strokeColor": "transparent", + "backgroundColor": "#82c91e", + "width": 39.272122196359064, + "height": 39.272122196359064, + "seed": 1643887321, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "status": "saved", + "fileId": "1958a10df1b24a944d085ee8efa0f0cd27c00c14", + "scale": [ + 1, + 1 + ] + }, + { + "type": "image", + "version": 2190, + "versionNonce": 187055191, + "isDeleted": false, + "id": "paXJ70-xPvXLotkExd1SC", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 702.0324882007798, + "y": 353.0508255731666, + "strokeColor": "transparent", + "backgroundColor": "#82c91e", + "width": 37.55527305080062, + "height": 44.42021543643085, + "seed": 407959481, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "status": "saved", + "fileId": "e479fdf0f5f95cbd17ff78227eb1179ffb244d92", + "scale": [ + 1, + 1 + ] + }, + { + "type": "image", + "version": 1834, + "versionNonce": 1978308983, + "isDeleted": false, + "id": "tv6-Y6aen-HOtm8lTa1yt", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 669.5086722328381, + "y": 337.6847408803527, + "strokeColor": "transparent", + "backgroundColor": "#82c91e", + "width": 39.52657283604631, + "height": 39.978305097029704, + "seed": 1319868569, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "status": "saved", + "fileId": "6cb47ce45d94266a638df22730526c3d9df77d5c", + "scale": [ + 1, + 1 + ] + }, + { + "type": "image", + "version": 1945, + "versionNonce": 2120635031, + "isDeleted": false, + "id": "NA4HkZZcmNTNlT35WJo0e", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 658.1324778409881, + "y": 434.3375836539526, + "strokeColor": "transparent", + "backgroundColor": "#82c91e", + "width": 40.91973713445072, + "height": 34.232464917925704, + "seed": 650059129, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "status": "saved", + "fileId": "ae01a9cb4d629f220432643a81588ff546aa7c12", + "scale": [ + 1, + 1 + ] + }, + { + "type": "image", + "version": 2191, + "versionNonce": 760080311, + "isDeleted": false, + "id": "Tq0NqCrgvbHhSBzc6o8Em", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 701.1674317267664, + "y": 418.5698420958908, + "strokeColor": "transparent", + "backgroundColor": "#82c91e", + "width": 33.28437631035857, + "height": 29.586112275874285, + "seed": 1070532185, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "status": "saved", + "fileId": "5439b0055c45de1aa78d5fa59f1e247227f05ece", + "scale": [ + 1, + 1 + ] + }, + { + "type": "image", + "version": 453, + "versionNonce": 12966103, + "isDeleted": false, + "id": "cCWdf6XzyOjfEoiwBGfU5", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 630.5860991313698, + "y": 345.1416075241098, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "width": 33.398335114112335, + "height": 33.398335114112335, + "seed": 78566201, + "groupIds": [], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "status": "saved", + "fileId": "361d356b360d2f74e05167bb08bff66b028fede7", + "scale": [ + 1, + 1 + ] + }, + { + "type": "image", + "version": 1034, + "versionNonce": 1046652407, + "isDeleted": false, + "id": "sXO4nVSQ-6U8XVRYQKfoH", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1070.1789115641836, + "y": 452.9074170669565, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "width": 69.3711360812153, + "height": 69.3711360812153, + "seed": 969016345, + "groupIds": [ + "26TIqyqNCR4z23suCUf5k" + ], + "roundness": null, + "boundElements": [ + { + "id": "nxMSq2HZv6nUeEHKHKZBx", + "type": "arrow" + } + ], + "updated": 1682708696231, + "link": null, + "locked": false, + "status": "saved", + "fileId": "c22ccc052a712a90be0c89f626282b54d2eb0d44", + "scale": [ + 1, + 1 + ] + }, + { + "type": "rectangle", + "version": 673, + "versionNonce": 86560535, + "isDeleted": false, + "id": "n4lOmlNvNY2eCwh7cNvel", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1046.5953600242062, + "y": 446.6113116788142, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 105.50459122825124, + "height": 81.85167678971857, + "seed": 1258035449, + "groupIds": [ + "26TIqyqNCR4z23suCUf5k" + ], + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "nxMSq2HZv6nUeEHKHKZBx", + "type": "arrow" + } + ], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "image", + "version": 751, + "versionNonce": 529323063, + "isDeleted": false, + "id": "tAxvM1YRY9grmIGY-oh3d", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1053.5497262065146, + "y": 450.69291560910875, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "width": 19.898737010078655, + "height": 20.167638861566203, + "seed": 1507889625, + "groupIds": [ + "26TIqyqNCR4z23suCUf5k" + ], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "status": "saved", + "fileId": "40ff0b711792c337b777312f8fdeb5196e9d3322", + "scale": [ + 1, + 1 + ] + }, + { + "type": "line", + "version": 2118, + "versionNonce": 359871831, + "isDeleted": false, + "id": "OtEtfLO4FFgzsYIT59E7H", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1052.308766109531, + "y": 380.6368651918176, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 88.41226320145954, + "height": 0.16079972694007497, + "seed": 193130169, + "groupIds": [ + "5QlEvIfNzDbkWCHHG04yZ" + ], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 88.41226320145954, + 0.16079972694007497 + ] + ] + }, + { + "type": "ellipse", + "version": 2058, + "versionNonce": 2117104247, + "isDeleted": false, + "id": "lWNg-GJbNZGZzQqYSGkxZ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1055.1483161625526, + "y": 374.1124911005959, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 3.463071453523677, + "height": 3.412273339109551, + "seed": 377925529, + "groupIds": [ + "5QlEvIfNzDbkWCHHG04yZ" + ], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 2128, + "versionNonce": 310971287, + "isDeleted": false, + "id": "14GNojY0FxtyxE5dOZFc4", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1060.134931063347, + "y": 374.1124911005959, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 3.463071453523677, + "height": 3.412273339109551, + "seed": 287265913, + "groupIds": [ + "5QlEvIfNzDbkWCHHG04yZ" + ], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 2175, + "versionNonce": 2125562039, + "isDeleted": false, + "id": "xdMHOab6CkcSZNvVpuoJm", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1064.950752303868, + "y": 374.1124911005959, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 3.463071453523677, + "height": 3.412273339109551, + "seed": 2131778905, + "groupIds": [ + "5QlEvIfNzDbkWCHHG04yZ" + ], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 3142, + "versionNonce": 1951289815, + "isDeleted": false, + "id": "IgcAln3aDqyAnbB8Pb_Bt", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1049.2920455979256, + "y": 401.07893263588755, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 93.90128779405138, + "height": 62.71847522541996, + "seed": 531450425, + "groupIds": [ + "5QlEvIfNzDbkWCHHG04yZ" + ], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0.1077591092426571, + -12.628833246747456 + ], + [ + 0.15086275293971996, + -25.105633990524435 + ], + [ + 1.3793165983060114, + -30.1835195897383 + ], + [ + 6.875031169681524, + -30.953817604788703 + ], + [ + 29.375133179548328, + -30.791649601620207 + ], + [ + 59.849409273371755, + -30.91327560399659 + ], + [ + 87.802122210917, + -30.791649601620207 + ], + [ + 92.84524852347336, + -29.838912583005218 + ], + [ + 93.85818415035433, + -24.973872487950025 + ], + [ + 93.642665931869, + -11.736909229320672 + ], + [ + 93.58878637724767, + 1.2263955239618314 + ], + [ + 93.42714771338373, + 13.176150257441185 + ], + [ + 93.70732139741462, + 26.169861511317766 + ], + [ + 93.08231856380719, + 31.278153611125724 + ], + [ + 88.01764042940232, + 31.764657620631247 + ], + [ + 63.27614894728823, + 31.643031618254852 + ], + [ + 28.707026702243862, + 31.237611610333587 + ], + [ + 6.5086501982564915, + 31.278153611125724 + ], + [ + 0.7327619428500683, + 30.791649601620207 + ], + [ + 0.08620728739412567, + 25.440105497059484 + ], + [ + -0.043103643697062856, + 12.973440253480549 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 2113, + "versionNonce": 2087976695, + "isDeleted": false, + "id": "iz2dvglJiaUIyLVFnhzvQ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1105.373093586531, + "y": 373.3394315785798, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 63.60351100546203, + "height": 4.206980887633377, + "seed": 2077858585, + "groupIds": [ + "5QlEvIfNzDbkWCHHG04yZ" + ], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 28.112049063187634, + 0 + ], + [ + 32.24100626934332, + 0.9073880345875908 + ], + [ + 32.24100626934332, + 3.794531781002653 + ], + [ + 28.19989921651009, + 4.1244910663072325 + ], + [ + 0, + 4.206980887633377 + ], + [ + -26.091495536771024, + 4.1244910663072325 + ], + [ + -31.098954276151318, + 4.1244910663072325 + ], + [ + -31.36250473611871, + 0.9898778559137358 + ], + [ + -27.14569737664056, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 966, + "versionNonce": 161941527, + "isDeleted": false, + "id": "ZfAkm4iL8OyIJEs5VOHV2", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 4.715883031006205, + "x": 1114.5980762776746, + "y": 407.5050186960246, + "strokeColor": "#d9480f", + "backgroundColor": "transparent", + "width": 0, + "height": 32.67436548853834, + "seed": 872992761, + "groupIds": [ + "m5jHJ5ScJGbaBEgPTFEjq" + ], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 32.67436548853834 + ] + ] + }, + { + "type": "line", + "version": 1304, + "versionNonce": 1239149879, + "isDeleted": false, + "id": "KhFb_InHNdxobk_iQH2q4", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.0006432738582038411, + "x": 1098.4782288263175, + "y": 391.3701141370696, + "strokeColor": "#d9480f", + "backgroundColor": "transparent", + "width": 0, + "height": 32.67436548853834, + "seed": 238971097, + "groupIds": [ + "m5jHJ5ScJGbaBEgPTFEjq" + ], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 32.67436548853834 + ] + ] + }, + { + "type": "line", + "version": 979, + "versionNonce": 947334743, + "isDeleted": false, + "id": "G7KUsaQf12C7kS6x9Ubdv", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1098.1336994853866, + "y": 423.72206003255314, + "strokeColor": "#d9480f", + "backgroundColor": "#ced4da", + "width": 31.900498937494014, + "height": 22.70008549730032, + "seed": 1141761465, + "groupIds": [ + "m5jHJ5ScJGbaBEgPTFEjq" + ], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 12.81179067840056, + -19.776589637799525 + ], + [ + 22.528115152623805, + -12.63982033372404 + ], + [ + 31.900498937494014, + -22.70008549730032 + ] + ] + }, + { + "type": "line", + "version": 973, + "versionNonce": 1884113783, + "isDeleted": false, + "id": "HXYtvriAAsuBg70eICpfv", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1098.2196846577247, + "y": 415.89740934977164, + "strokeColor": "#d9480f", + "backgroundColor": "#868e96", + "width": 31.728528592817494, + "height": 18.142871363372606, + "seed": 278578841, + "groupIds": [ + "m5jHJ5ScJGbaBEgPTFEjq" + ], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 12.381864816709268, + -4.041303099898164 + ], + [ + 22.27015963560903, + -18.142871363372606 + ], + [ + 31.728528592817494, + -9.544354129546727 + ] + ] + }, + { + "type": "image", + "version": 1047, + "versionNonce": 1034077335, + "isDeleted": false, + "id": "FRRhjBZA9NEVbc2zGIaT1", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1047.8151075919614, + "y": 287.6459256853709, + "strokeColor": "transparent", + "backgroundColor": "#fab005", + "width": 57.1720836934967, + "height": 73.9874024268781, + "seed": 2123926393, + "groupIds": [ + "qmynAsjGqcASpxobX-D5a" + ], + "roundness": null, + "boundElements": [ + { + "id": "WazM5J7lTqqm1W0vFqX_Y", + "type": "arrow" + } + ], + "updated": 1682708696231, + "link": null, + "locked": false, + "status": "saved", + "fileId": "c6ada7d6abdca24ea19b3808802d0a33e330587a", + "scale": [ + 1, + 1 + ] + }, + { + "type": "line", + "version": 1420, + "versionNonce": 1881938359, + "isDeleted": false, + "id": "ddBPx0uiJrNnN5jtwm0q7", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1046.0823495732793, + "y": 286.6138273937215, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 56.42793642526476, + "height": 0, + "seed": 1984607321, + "groupIds": [ + "qmynAsjGqcASpxobX-D5a" + ], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 56.42793642526476, + 0 + ] + ] + }, + { + "type": "line", + "version": 3433, + "versionNonce": 1829620439, + "isDeleted": false, + "id": "GRXrnvkdGh4EdL3cwrKFF", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1047.6796945703927, + "y": 314.62355677951024, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 57.884443460671086, + "height": 93.6284116319061, + "seed": 193249593, + "groupIds": [ + "qmynAsjGqcASpxobX-D5a" + ], + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0.0664269491171346, + -18.85277971773675 + ], + [ + 0.09299772876398844, + -37.47859980805292 + ], + [ + 0.8502649486993231, + -45.05905136390052 + ], + [ + 4.238039353673188, + -46.20898014283149 + ], + [ + 18.107986329330892, + -45.966889873582865 + ], + [ + 36.89352753965655, + -46.14845757551934 + ], + [ + 54.12467814064127, + -45.966889873582865 + ], + [ + 57.23345935932318, + -44.54460954174719 + ], + [ + 57.85787268102424, + -37.2819014642884 + ], + [ + 57.72501878278996, + -17.52128323686932 + ], + [ + 57.691805308231395, + 1.8308076611927353 + ], + [ + 57.59216488455571, + 19.66983437645089 + ], + [ + 57.76487495226024, + 39.06731719999703 + ], + [ + 57.37959864738087, + 46.69316068132875 + ], + [ + 54.25753203887554, + 47.41943148907462 + ], + [ + 39.005904521581435, + 47.23786378713815 + ], + [ + 17.69613924480466, + 46.63263811401657 + ], + [ + 4.012187726674932, + 46.69316068132875 + ], + [ + 0.4517032539965153, + 45.966889873582865 + ], + [ + 0.053141559293707674, + 37.9779109883782 + ], + [ + -0.026570779646853847, + 19.367221539890103 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "image", + "version": 637, + "versionNonce": 14440439, + "isDeleted": false, + "id": "fISMxdlM2fwN7xlAhN7nX", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1052.6412741873169, + "y": 272.2647737753284, + "strokeColor": "transparent", + "backgroundColor": "#fab005", + "width": 46.75261409494893, + "height": 12.597232131139016, + "seed": 195703321, + "groupIds": [ + "qmynAsjGqcASpxobX-D5a" + ], + "roundness": null, + "boundElements": [], + "updated": 1682708696231, + "link": null, + "locked": false, + "status": "saved", + "fileId": "4e0bac09e2343972b6d7a6655d26d99a2687b94e", + "scale": [ + 1, + 1 + ] + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": { + "d8f8c4fa7d49f6533192608108c28b3ec1654cfc": { + "mimeType": "image/png", + "id": "d8f8c4fa7d49f6533192608108c28b3ec1654cfc", + "dataURL": "data:image/png;base64,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", + "created": 1682671009683, + "lastRetrieved": 1682708691261 + }, + "16b7eb7d04e59834d78ae80e062df27ea7b4987f": { + "mimeType": "image/png", + "id": "16b7eb7d04e59834d78ae80e062df27ea7b4987f", + "dataURL": "data:image/png;base64,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", + "created": 1682672485484, + "lastRetrieved": 1682708691261 + }, + "f19857abaefea545f9015469988a913b1f3694f1": { + "mimeType": "image/png", + "id": "f19857abaefea545f9015469988a913b1f3694f1", + "dataURL": "data:image/png;base64,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EchiAl5cMTvmIy8Itv8XKmbdw8/PXEtpqfHm0kY6i5WvpomACIiACIiACIiACIiACGQkgcZ92iPfjKSkxzl06b4pCyt8WH43GSfaczBSEQEREIHmE/AOs/Lz+xsP6ElgnScDdPMp6g0REIFECA2beCyQMDF37vE7i3ieF6+exG3nzU9efVayJHUUERABERABERABERABERABEUhPAmYnZ0JWxSktDVCy/Vg23eEw6pZshONAIGQciowRWobo9NSfpBKBdCbgGaAbQhtavLbgKlzOlQE6nfUl2UQgDQlYxInHA4krWt16l+HyEq88fgvjR3yVlDbwF7Es07AxEkkEREAEREAEREAEREAEREAEcpqAMUKbYvPPSb057rwxRDmQsnlrJ3L7+AMxwOzxFB86p7uJGi8CzSKQDOcT2Mbi1UXXgnOWDNDNAqiHRSB3CViWg23HiUZClHSLUBJ6msl3P8Sk0a8loYSShmcvLIeKCIiACIiACIiACIiACIiACIhAJhAwxmVjZDbGZjj7xoEcefooapyRLJjXmWAojj9g4bopY3UmtEkyioAItB8BzwDtd3a2eHXBDcAYGaDbTxuqWQQyhICLZcWJRYOE8mGNoje5/4G7uOKYKUn5g4AZXOwMaY/EFAEREAEREAEREAEREAEREAER+F8CJtyGMURHE/805uY9GP3PoyirHUlNDYRDEVzXOB7JG1q9RwRE4O8ImLCtFn72tHht4U247mkyQKvHiIAI/A0BG9eF+jo/ffrN5J3nrufTp5/kmUfmJRcdXswwFREQAREQAREQAREQAREQAREQgWwh0BiWY9iwfNbZa38OOeZMfp+1DfmFJByUPEO1igiIgAgsi4BngA6wl8UrC2/CkgFa/UQERGCZBJJJBqMB/EEY0OVq3nvtQUbv+V3yaeP1bBYd5jkVERABERABERABERABERABERCB7CLQmKTQtOvyl1Zj370PYf6CccTiJQSDZj9oDNXyhs4uvas1ItASBDwDtMMeFq8vuAaHs+UB3RJc9Q0RyCoCyYHCgW5dX+L7ry/j/jO+ZNq0VBZk01jFec4qlasxIiACIiACIiACIiACIiACIrBMAksbom94ZV2G7HkmcytPTJieLZ+3f1QRAREQgUYC3rhgsbvF64sn4NgXyACt/iECIvD/BEy4DZ8fQsE51Cw5jWOHvkHF9zXJ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", + "created": 1682672497108, + "lastRetrieved": 1682708691261 + }, + "1958a10df1b24a944d085ee8efa0f0cd27c00c14": { + "mimeType": "image/png", + "id": "1958a10df1b24a944d085ee8efa0f0cd27c00c14", + "dataURL": "data:image/png;base64,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", + "created": 1682672507405, + "lastRetrieved": 1682708691261 + }, + "e479fdf0f5f95cbd17ff78227eb1179ffb244d92": { + "mimeType": "image/png", + "id": "e479fdf0f5f95cbd17ff78227eb1179ffb244d92", + "dataURL": "data:image/png;base64,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", + "created": 1682672571436, + "lastRetrieved": 1682708691261 + }, + "6cb47ce45d94266a638df22730526c3d9df77d5c": { + "mimeType": "image/png", + "id": "6cb47ce45d94266a638df22730526c3d9df77d5c", + "dataURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAK8AAACxCAYAAACyTu2eAAAAAXNSR0IArs4c6QAAFi1JREFUeF7tnX1sHMd1wN/s3ZHmh2K5Llw7QSHaKZwqcSrajNE/LNsS+k/RorAMBHBgGzBtFA0aW85RcVogFa2TSEsJAogn0yrSwq1JwDJaCIUo5L8CASXILlIkkskGiJzAtkgXrlSrtkXxm3e7U7zlLnVa7t3N7Hzsx80ChiFw5s3Mmx+Hb957M0PAfEYDKdUASWm/TbeNBsDAayBIrQYMvKmdOtNxA69EBugkbK0C9DoAu3yx1vq/p/DfBGAGAKbadq//23xiGjDwiukPENgKQD+s/7eDUdwcAExQgIn23TDBWMcUC2jAwBsRCQ/akgftrRHFYLVZACi17YYxARktWdXAG2HaVyehRACKACACbbDlaZRZ2A1nInSpJasYeDmmvTIJuyi4K+Q2jmpcRQnA6SpAsWO3ax+br4EGDLwMeCxPQk9uHdpHGYpLKUIBDrYBlMluuCZFYAaFGHgbTGqNXfvdmOYeN3ZFYw+Ha9/AW4fKtUnXe1CWbNdG/R0w9nCI5gy8AaV4di1Cy+r2igpklHrjNkDJ2MPrqjPwegihXZsHKFOAx6JQpbHOHAUoG3vYwOsGGdYAigTggEYAZTTV8v7hll55PbsWAw3KXF8yKG0i4ywBKLWif7gl4fXsWoRWm+tLA8QtZw+3FLyevxahfUYDTHE00VL2cMvAqyikGwegLG22hD2ceXhXJ2EPWffXptmuZQE2rMxZL8iRyRTMzMK7Ngm9XpAhS3ZtVIjHC+hRyVioOXPweiFdXGmzatdGBdi1h9t3A9r8mfgyBe/apJumiJMjM1UxExPtRaRO5wH6s7ICZwJeHamKKSd4lgD0Z80XnGp440hVTCHExwoApeBq+42hUdd8qNrV8lRpIJVpl6mEN8UhXZ3s111t+4ZG9wDAKa8zs5TS0oWXX0zdMaTUwZuwVEWdMDK3hYns9TZmvaWRrblcHk9p3LQvoABnLYDSLwf3puYYUmrgTXiqIjNYigtO44HQRkfrHxgaPUMah8XHbbtaTIMpkXh4U5SqqJjLxuIbrbZ+zQeGXysSSkcYOjqHgZ1fDu5NtFstsfDW2LWyT+kyzF2qipy1AfqbJag/+MrxXsdx3uUcGYaZi+cH9ybybolEwpuRVEVOTriL4/k2vO8BAzINP8/ORVs20ukQtIdzllX8xd89n6gwc6LgzWiqYjO2ovycabX1BfcNjaInQUbE8ZhtV0tJsYcTAa8J6TLzy32aOOAWY26oQcE5Skjpwv4Xmq74MhprJCN2eFssVTHyfOJlJLyh3d7SSE8ul8c/9SrC5a4fOU7XWmzwtniqIg/EmFDTH+VCPga3GE8/QsuiPezY1f6p0oD2G360w2tSFbl4CQ3tskjA8C/VeKiUABzUHWrWBm8Cbp9hmfOklBFKpInoFpMx9jlKaVFXqFkLvCZVkYuLyKsttuK5xdDOjfPkiHvDj2p7WCm8JlWRC1opVzpJdItxdb5OYQw1o2tNiT2sBF4T0uWbd5bQLovEBw692k8IeYOlrMYybqhZhT0sHV6T9cWFRdNEGlZpit1irN1oVG7Wsqw9MqN0UuFdm3TvsJURyZGhrCTLkH6erG9oFO3cSOFfnYqilD4ra0MnDV4DLjMCXKFdFqm63WIsfWoSGTsoI2NNCrwGXKbpZE6kYZLmFfrG0Cg+NTDJUycJZWWswMLwem4wlhzRJOgslj6oemciIW4xEZ0+LpJuKQSvdwBSVexcRClJqRs5tMsygL5Dr04AIUm/T7jRUOZsu9oTNUtNCN7Vs1t+Tpz5P2ZRdKuViZJIw6OjhLrFeIbglq2urPzb9Cvf/yZ3RZGb0Zfe/qNixbpnJG9/APnqe0CgEqX9LNaZpQDFKIk0rMpIgVus6VDsSgUWL1+FtflF6Lz91oGLx4e4Uywjr7zz7+z63CFbt2IvEVwEGEFu8U8otMuqu7S4xcLGQ20Hlq5+Csuf3rgqotDd9cH7rx/5A9bx++Uiwbt87qs713L3ngs2RugStFUvgOX8H28/0l5eKJGGZ/B9Q6O4QsX1tBZPVzeVXfrkU1j+7BogwMGv/Xdue/i3f3/wbZ4GIsE7f67vlJP7fby4IvRDeBFihLkFPi2rLeoxrW6xlWvXAcF1KtW6OLTf2j3x2384/DgPL9HgrTEZGjWWcXtYWmiXZcLqXRbCUjeuMpXFZddEwP83+wqdHVfe/+cf3dWsXO3PI8E79x97KGsjWbSHZSXSsOoQy6XJLVa7GWMdI7EsOvNW2WItj+W44a1n7zZrFE2IQvVXkHMuNyua5J9rXW19RXBcFhKr7tCWXf70c1i6+lmkfvDavdzw+i6ySL0DcDdzCLFFMVqamk9JaJdl9N6pCLxzQcUhSpYuMJVB7wGaCGGbMSYBANwuM+3w+gPJ2R9BwX4vDZs66Yk0rJPpmgsJzxZbm1+AhctXG27GWMfL6++NDd51m6UCOTfI8UESgxyxrbb+ZCfZLcazGcskvP6gkrapUx3aZZlMBZeFsDTbtAxuxpY++QxWr11vWpa3QKpW3uDgErCpU5pIwzqZSXSLiW7GWMaeanj9AbqbOvs9rZG6JKy2G96F5nfosrAgrQwGGRavXBXajLF0JhPwbmzqnMuuZ0JxpE5baJdlApPkFkO7dv7jK1I2YyxjzxS8N3kmEGL5mWvaQrsskxfjZSE3da+6suqutCyRMZZxsZbJJLwKPBOJWm1xfKJ36LIC0qgc2rULV64q2Yyx9C+z8NZ6JtxInf0Riz42lcHQbhtAOWkP6cXpFvM3Y/UyviIpOkKlzMO7ATF/uDmW0C7LHMbpFmPJ+GIZg4wyLQMvj2cijkQansnsGxrF65C03i2G9izatWjfJuVrOXibeCZiDe2yQKE7RzdKxhfLOGSUaVl4feW5OcT2e9cJrRxgeWxEhtJFZOi6MCTs+I1Iv1XUbXl4LXrtWt755C86Hv4115ESFZPBIlMHvI2O37D0UVeZlobXsv97YsvD57mOkuiamHrtqAxKyMz40qEn5fBiMnrF+uK/U9LdoWNArG0UnA8HOnf+F/fxaVb5qsqpCEyoyPhSNf5aucrh9RtbOHf/63buS88B5LnTKmUrIudcnure+Z/3y5arS56sh09UZnzp0IU2eHEwy+9s32ZD97hNvvSojsHVa6ON/qan46GL+NRoKj/R1TcpQQZR5WuF1+8smhJVcvtJx7r9TtEB8NbHDdqWh87cxlsvaeW965vQ7OE67qMr40uHvpTD6/kl+8PeGsDzbVVyx2Gd9nDaTYZaKNxrnKxcmeXyPN0ZX1mCF++DrfvWgE57OEc/Ptv90C926VCurja8BQJvmd8UdUO7duHj/9We8aVj7LpW3trLjGcppaXgVe2uPUy/MGFbd/WqHHgW4fX15bnRSmhKxJ3xpXIOfdlxwOu2jc94WgCl4Ntbrj1s3fFT/1I+2UrIMryoK0yVpJXq0bmZj58VOVYuW+8q5MUGb81gQt/eWreHv/hjSm7Jyxx41uH1dXXvt/f/mb26eqK6surezJnFLwnwol5dezjs0Yz1S/ruekyWf7hV4PVh/cPvDB5ZnV98yalUpS4CSfhlSAq8vi7Q91oMvjsg0x5uNXh9xX7lr35wZnVh6RHqOLEHiWSBnzR4N+zhnGUVgw/IybCHWxVeVOz254e3VVYXf1ZZWPyyLIDilJNIeAP2cDH4gMbSuR1HqtZdL0Wxh1sZXl+v258fLK4trhxIuz2cdHgb2sML7zx4xia/9wiPPZylIIXoqveVv97/+trC0jNptYfTAG9Te7hKb/uZY93B9KcwK+FhUXBvrMLD25zK0niYPYx34OY7Oz7Mt+VPEotMXXzt0L9uf+HlJ6hDe+1K9U8ri8s74rSh0wTvhj3s2NX+4LP2S2/f94RNfvcnLP7htCfmyAK3Vs693zmw01lZOVlZWnbzTfDRkkJ7159cPL6/YQLTvd/+wam1+aXH4oA4dfDWKPyYly9x45kYAGCxh43pUB9/tIedir3nN/94mDmEHtdGMM3wuvYwJaR0Yf8Lm5LKm9nDOXv2n7offvcvVaxiWZLZ861iD+SsPYTQjQdxCJAzTtUZm/mXMp5i3vjQpFhbWP6Jro1g2uHdsIcJQH9oqJlsHatnD5sVuPmv2d1PDUwAQOiTr4TCMaeNlmbGyjf99dMVGMkKvA3tYfQP22TLKCUdPf50Ebo8k6Pze9Ny8LI5ZupK9Dy5r58Q+kadFuYokNLMiaOb/vqpDoxkCt5m9nCj6cXkbouQmeDqrQ6JdEm+58mBMiUNHyOcpYT2z7xZxvcwbpgSCgMjWYV33R6mtBhMvQxDpm9oFHNhn/F+holCmwIj6UJNfm97+otbSYV83lQygbO0Svt12MNZhnfDHgaACQIwUbWrUxitw7TBfC7fSwFwE4L/BZO46yYKNZ28DBe4++mBM0CB6fxhPXtYZmCkFeAVwSk0UUhEYJrrMpgOweGF2sPoWqsXGOHRj4GXQVuYOB8WGGGomqki9zy9r0QpPRBhUNOU0GLQHnYDI2trY1EThQy8fDMRGhjhE5He0gLw+oM+TW1a3GQPR0wUMvDys1Q3MMIvKl01JMDrDpgQctDJO+Wgf5jXHjbwRufHveq/lVxrsuD1VD5HKSnOvHUUPT0bn28Pr1xfaLoxNPBGh3e9JqWnbcdG19pNoVJRsUmsLxlef4j17eGaRKEwfRh4JVFCAA5W7Wo5mDgvSXwixCiCt6k9vLqwfNheXdt0UaOBVy4WzIERuc3qkaYYXhzEHCGkXM8eXp1beK429dLAq2bepwlAMWv2sAZ4/dmYpZSUmtnDBl418PpSQ++kUNukOuka4V0fBIaagZZC/cMrKycLHW0/unh8iPmOZe5j07ofAFE3dZElz1mWtSt4EjqytBgr8oSHJXdznNq0FPQP87Zh4OXV2Hr5TAB891MDmLfLdaVqNHWF1qprD7O2YeBl1VSgHIaYLwzuZT5aE7EZZdW0mwz1R4KplxhqxiR5rs/Ay6WumwtblnV/Gs2HnieLvYSQdwWGLr9qndTLRg0ZeAWmgRIyEHbeTkCk8qreKQruG9iVdwzjQ4TuDm7mDLyKNI+BjLDLBBU1JyTWSz7H0G3o+TUh4ZIqG3glKZJFTFrg7Xm6uIdQguDGtTljUadZeZm0JK/Q48EbMOWJFpeEx9xJnoyxnpYQb1FMgll5xfTHU3vu/ODexF703PPUviIB6j4JwDOoOMsaeDVpn1L6LMthUE3d2WgmbattrX4MvDpoofT0+Zdf3LhxRkeTLG14vttimlZbAy/LzMorM2vb1d4kpUp6flvckO2QN0z9kszKq1jnBGB3krLLEhQpE9a8gVdYhfUFJMk11vN0cZfn/tr00KBCFSgVbeBVp97p84N7lT6IyNJ1DDZYa6TU5KomFlGJK2PgVTMlc56dG+u5tiyutmbDpgbYWqmxBiO80C7mI/j3r6kfcQwtmJVXvtLHzw/u7Zcvlk1iWkK7bKNpXMrAK0OLN2TE5hZLQyKNXFWbrDKp+owrXzfJaYtSFRwQpnzl7S2N9ORy+UsqB5EE2XG4xdIc2pUxZ5TS+2feKk+xyuJORkfBfUOjcZ59Yh1b5HJxHPFJYyJNZAXXqXjpxAgXj1yF/Tb7Dr06AYQkNqlZUKla3WKtvtpuzBWBs5feHOE6ExgJ3geGXysSSkcEIUlqdW1usSyFdkUnE2+a/PDNo5jCyfxFgjfDdu+x84N7MStL6ZeVRBqZSuK1d7HtSPB6dm/toyUyxxGXrGnbru5SmS3mhnarVjHibeRx6UV9uxFMBiF4s3Zzjmq3WNZDuyKEU0qeDd5jxiIv8sqbpdVX5RH2LCfSsADWtEzEVVdo5cXK+IRULpfHZJXUnJMKKlOlW2z784PfWrm28MOQp7WazmmrFIhi6/q6EVp5vdUXj8OcSqmy0S3WI9vO3f694W1tbe0/B4A7r3/0PylVjfpuR/Ew1PZKGF4UlmLXmXS32Ne+N3yk/Qtb/pZYFqkurYCBt+4vwfilEyNCCU9S4E2p/SvVLeavtvnOW+70p8vAWxfc6UsnRoQT+6XB667Ah17tJ4TUe01c/d8h9haknor4+vd/eKrQ3fEYrra1XTDwhk6I8IorzeYNds9zoaEPOKlnq6TdrfvVF4d3Frbc8tP8Le2hl48YeG+mgwIZmDlxlPnm82ZrkdSV12/Me8i6SAGiPA3arM9CP5flFqu32pqVN2R68PpShxZ5MsZYJlkJvDUQ91i5/BgBtpfFWTosVEbCZSH37Rt+It/V+UaurbDpKaZg38zKC5EvjmaZZ6Xw+h1IiCkh7Bb7+t8cOdO2pbvpS45mw1b/CSsWKFnLaIHX74znUovl8jeRy0J4VltjNoCUx1JYANYKL3bIi8ohwN9l6aCkMpHcYuj+KuQLE4XuzkhunZYyG+o8UyVp/kLFaIfX78WDrxzvtR2nrMEejuQW+9q+4WLblq4fW/l8PuoEtAi8oQ9mR9UZT73Y4PU72Tc0iuFldJ+ocK1xn4oQXW1byWzA8G7Y06w8AIqUjR3emk1diQJIvZ6T9w5dDO22dXe9JLLatgi8p6lNi6KPAIqAi3UTA2+NPSznZhgOt1hYaFdUsVg/g2YDur76eV7skaHHejISBW+ta80BKAnYw8yXhdQm0shWdIbgnaNASjKjYzJ0nUh4N1xr67kS6JngsodZ3GLNQrsylJsFeAmFY04bLc2MlfG6g0R9iYbXNyXyuTyGmpnsYZbLQlhCuzJmKdXwRniRUobOeGQkHl5/MO6JZStXbnJfREO3mI7VNgMbNqUhXR44m5VNDby19jBdd60F319o6BbD0G6hq/ORYNpiMwWJ/DxlK6/7Cjvv3Qki+hGtmzp4A/Zw7Ru6oaciooZ2RRWbMm/DOC3QYhLt2kbzkFp4a1xruKHbGnaHLm8ijQxgU2U2KEpVlK3HVLnKRAePod1CV+dhlrRF0bYa1U+w2TBLKSlFuStBpb54Zad65Q0OVmZol1eRYeUTCK9r18YZ0pWhV19G5uDN53Ljujdm9SYkYfBqS1WUCWhmbd56A3NdYl3tJ2tP8upSaAJt3mlKaDEpIV2Z85CplTeomLht35hX3thSFWUC2nIrb3DA9710+PW2LV3P6fTxxukqiztV0cArWQO4mUN7mOcMmmgXtK+8KQjpiuq0tn6mzYYwRem0hzXCm6hURZmAtrzZEKYAHfawBngTmapo4NWkAZX2sEp4k5yqqGnqknWSQtegw4IbKuxhJfCmPKQrc45bzuZtpDzZKZOS4U1NqqJMQI3Ny6lNGcfeJbrKUpeqyKnuyMXNyttAdaInLiSsvKlMVYxMI2dFA28ThYkk+0SGN4bbZzi5SURxAy/jNESxhyPAm4lURUaVChcz8HKqkMce5oA3U6mKnCqNXNzAG1F16B8udHU80+h2HUZ4M5eqGFGl3NUMvNwqu7kCQpwrFP48LP2yAbzThJAJp+qMxX1lkuDwY61u4JWofjQprFz+USD0NqtQ2GGvri0uXrn6PnFgCixyzQFnCqowZYCVo3QDrxw9GikxaMDAG4PSTZNyNGDglaNHIyUGDRh4Y1C6aVKOBv4fM+fcZqwfkP4AAAAASUVORK5CYII=", + "created": 1682672620551, + "lastRetrieved": 1682708691261 + }, + "ae01a9cb4d629f220432643a81588ff546aa7c12": { + "mimeType": "image/png", + "id": "ae01a9cb4d629f220432643a81588ff546aa7c12", + "dataURL": "data:image/png;base64,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", + "created": 1682672654640, + "lastRetrieved": 1682708691261 + }, + "5439b0055c45de1aa78d5fa59f1e247227f05ece": { + "mimeType": "image/png", + "id": "5439b0055c45de1aa78d5fa59f1e247227f05ece", + "dataURL": "data:image/png;base64,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", + "created": 1682672665451, + "lastRetrieved": 1682708691261 + }, + "361d356b360d2f74e05167bb08bff66b028fede7": { + "mimeType": "image/png", + "id": "361d356b360d2f74e05167bb08bff66b028fede7", + "dataURL": "data:image/png;base64,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", + "created": 1682674045475, + "lastRetrieved": 1682708691261 + }, + "c22ccc052a712a90be0c89f626282b54d2eb0d44": { + "mimeType": "image/png", + "id": "c22ccc052a712a90be0c89f626282b54d2eb0d44", + "dataURL": "data:image/png;base64,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", + "created": 1682697705033, + "lastRetrieved": 1682708691261 + }, + "40ff0b711792c337b777312f8fdeb5196e9d3322": { + "mimeType": "image/png", + "id": "40ff0b711792c337b777312f8fdeb5196e9d3322", + "dataURL": "data:image/png;base64,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", + "created": 1682697763983, + "lastRetrieved": 1682708691261 + }, + "c6ada7d6abdca24ea19b3808802d0a33e330587a": { + "mimeType": "image/png", + "id": "c6ada7d6abdca24ea19b3808802d0a33e330587a", + "dataURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmQAAAMYCAYAAABlhvuyAAAAAXNSR0IArs4c6QAAIABJREFUeF7snQd0VNXXxQ+9l9B7AOm9d6T3XgQUBGlKs6OggIIgggVRFEEQAaV3Enon9CJVqoD0TqhiqN/ax/+bLxnem5mESaZk37VYYvLeLb/7wuycc9/ZcZ4+ffpU2EiABEiABEiABEiABDxGIA4FmcfYc2ASIAESIAESIAESUAIUZHwQSIAESIAESIAESMDDBCjIPLwBHJ4ESIAESIAESIAEKMj4DJAACZAACZAACZCAhwlQkHl4Azg8CZAACZAACZAACVCQ8RkgARIgARIgARIgAQ8ToCDz8AZweBIgARIgARIgARKgIOMzQAIkQAIkQAIkQAIeJkBB5uEN4PAkQAIkQAIkQAIkQEHGZ4AESIAESIAESIAEPEyAgszDG8DhSYAESIAESIAESICCjM8ACZAACZAACZAACXiYAAWZhzeAw5MACZAACZAACZAABRmfARIgARIgARIgARLwMAEKMg9vAIcnARIgARIgARIgAQoyPgMkQAIkQAIkQAIk4GECFGQe3gAOTwIkQAIkQAIkQAIUZHwGSIAESIAESIAESMDDBCjIPLwBHJ4ESIAESIAESIAEKMj4DJAACZAACZAACZCAhwlQkHl4Azg8CZAACZAACZAACVCQ8RkgARIgARIgARIgAQ8ToCDz8AZweBIgARIgARIgARKgIOMzQAIkQAIkQAIkQAIeJkBB5uEN4PAkQAIkQAIkQAIkQEHGZ4AESIAESIAESIAEPEyAgszDG8DhSYAESIAESIAESICCjM8ACZAACZAACZAACXiYAAWZhzeAw5MACZAACZAACZAABRmfARIgARIgARIgARLwMAEKMg9vAIcnARIgARIgARIgAQoyPgMkQAIkQAIkQAIk4GECFGQe3gAOTwIkQAIkQAIkQAIUZHwGSIAESIAESIAESMDDBCjIPLwBHJ4ESIAESIAESIAEKMj4DJAACZAACZAACZCAhwlQkHl4Azg8CZAACZAACZAACVCQ8RkgARIgARIgARIgAQ8ToCDz8AZweBIgARIgARIgARKgIOMzQAIkQAIkQAIkQAIeJkBB5uEN4PAkQAIkQAIkQAIkQEHGZ4AESIAESIAESIAEPEyAgszDG8DhSYAESIAESIAESICCjM8A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", + "created": 1682707447485, + "lastRetrieved": 1682708691261 + }, + "4e0bac09e2343972b6d7a6655d26d99a2687b94e": { + "mimeType": "image/png", + "id": "4e0bac09e2343972b6d7a6655d26d99a2687b94e", + "dataURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABaAAAAGECAYAAADA0of7AAAAAXNSR0IArs4c6QAAIABJREFUeF7snQd4VFX6/99z752ZZFIJIFVaQug1CAKCNAEBaRbsZde2uq66tp9r+eu6omtZdVddV91dewFRiegKIiAtRKULCBI6hBJKSJnMzL33/J9zMnccQoBJMjX53ufBILn33HM+573te97zPYywgQAINFQCjIgUIhI/xWZWASF+Jzbu+534iQ0EQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEgiZgCU9BH4AdQQAE4pqAuOatP0YNW6L6xGghREOMriE87A4CIAACIAACIAACIAACIAACIAACIAACDZEABOiG2Otoc0MkYGU6+0XnwYMHpxw/frzPgAED2gwaNKjpmjVrWiclJSWJbGddN8rT0lL3ffvtt/uPHj36i6qqG1auXFkeAE4jIlEWhOiGGE1oMwiAAAiAAAiAAAiAAAiAAAiAAAiAAAgESQACdJCgsBsIxCkBy2ZDCs8DBw7M6Nat27iUlJSJjRs36anr3vZut9deUeEiu91BnFe6cDBGZBgmOZ1OUhRWkpCQsGfv3sL8ioryGYsXL166efPmEh8PkRUtDoIQHacBgmqDAAiAAAiAAAiAAAiAAAiAAAiAAAiAQDgJQIAOJ12UDQLRJSCynqWiPGjQoMycnJxrGzVqfJ1pGm29Xi/puk6MMfHHUBTF5JxXvR9w0zRFGappmmS320nTNFJVdd3+/YVv//jjj/9euXJlcYCHNETo6PY3zg4CIAACIAACIAACIAACIAACIAACIAACMUcAAnTMdQkqBAIhISAyk42BAwcmDhgw4I7U1PR7PR53U103RHazzFhmQn3+1Q/6dCeVns+cV+rLnHPFJ0ZvLSo68uSrr/7jLd/B8pwhqT0KAQEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQqBcEIEDXi25EI0DgBAJSCJ40adKgzp27PKtp2qCKigpSFEVnjInf1fW6F9nSpmmamrDoKC8v/3TDhp/u+frrr3cQkfCG1tEfIAACIAACIAACIAACIAACIAACIAACIAACICAI1FWIAkUQAIHYIiDF5z/+8d6rGzdu9NrRo8eShMUGY8xahDCUtRVCtEiLVtPT07ft3bvnuldffXWp+H9kQocSM8oCARAAARAAARAAARAAARAAARAAARAAgfglAAE6fvsONQeBQAKWnYZ555133pOcnPKMy+VSNE0TlhhCEA7npnu9Xi0lJaW4sHDf1W+88cYcZEKHEzfKBgEQAAEQAAEQAAEQAAEQAAEQAAEQAIH4IQABOn76CjUFgdMRkFnHd9xx112pqckvuN1uU1EUS5SOBDnTMAzF6UyuKCwsvPjNN//1FTKhI4Ed5wABEAABEAABEAABEAABEAABEAABEACB2CYAATq2+we1A4FgCEjx+f7777/EZrN/JPyeVVUNh+XGmepiGIahJiUliUzoYW+88cYaIhL1EIseYgMBEAABEAABEAABEAABEAABEAABEAABEGiABCBAN8BOR5PrFQEp8Pbp06fXpEmTF5eUlKSoqip8mcW/R2PzidDJ6997750h27ZtO+6rhKgTNhAAARAAARAAARAAARAAARAAARAAARAAgQZGAAJ0A+twNLfeEZDX8GOP/XlBaWnJMEVRIuH5fFqInHNdeEA7nYkvP/7443fAiqPexRwaBAIgAAIgAAIgAAIgAAIgAAIgAAIgAAJBE4AAHTQq7AgCMUdAZj9Pm3bl77OzM//hclUYiqKEe8HBYCBw0zRNp9NprFq1csycOXMWQYQOBhv2AQEQAAEQAAEQAAEQAAEQAAEQAAEQAIH6RwACdP3rU7SoYRAQ4jNv3rx5k5tvvnVteXlZM0WRrhvRst6oSt3gnKt2u23+k08+Odr3S9hwNIzYRCtBAARAAARAAARAAARAAARAAARAAARAwE8AAjSCAQTik4BcePC66667u3Xrs//m8XhMxlisiM+SqGma3Ol0snXr1o75/PPP52FBwvgMNNQaBEAABEAABEAABEAABEAABEAABEAABOpCAAJ0XejhWBCIDgF53fbs2dM5YcLEH7xeT2fGWDQXHjwVBV1kQWuaNuupp6Zf5hOghUc1NhAAARAAARAAARAAARAAARAAARAAARAAgQZCAAJ0A+loNLNeEdCISM/JyZk8adKUWcePHyNFUWMq+9lHmxuGwVJTU4+8/fZb527btu0XeEHXqzhEY0AABEAABEAABEAABEAABEAABEAABEDgjAQgQJ8REXYAgZgjIBcfvPPOO59LSEi8h3OuE5EQpWNu45wbmqapRUWHrn3jjTfehQAdc12ECoEACIAACIAACIAACIAACIAACIAACIBAWAlAgA4r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", + "created": 1682707630897, + "lastRetrieved": 1682708691261 + } + } +} \ No newline at end of file diff --git a/docs/static/excalidraw-drawings/excalidraw_is_awesome.excalidraw b/docs/static/excalidraw-drawings/excalidraw_is_awesome.excalidraw new file mode 100644 index 000000000..01845b866 --- /dev/null +++ b/docs/static/excalidraw-drawings/excalidraw_is_awesome.excalidraw @@ -0,0 +1,439 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "type": "rectangle", + "version": 580, + "versionNonce": 581574801, + "isDeleted": false, + "id": "c3Y2RAvySqR4DvLVl1M9j", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 215.40526126992648, + "y": 267.8089492252717, + "strokeColor": "#000000", + "backgroundColor": "#ffff01", + "width": 288.77532958984375, + "height": 48.04499816894531, + "seed": 1978446199, + "groupIds": [ + "RrIwKI_4Jm_6D7fm1aF85" + ], + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "BFInajKhrxlTTXrxIlHO3" + } + ], + "updated": 1681683022099, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 503, + "versionNonce": 1290972415, + "isDeleted": false, + "id": "BFInajKhrxlTTXrxIlHO3", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 294.9929840482469, + "y": 279.33144830974436, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 129.59988403320312, + "height": 25, + "seed": 41703609, + "groupIds": [ + "RrIwKI_4Jm_6D7fm1aF85" + ], + "roundness": null, + "boundElements": [], + "updated": 1681683022099, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "...is awesome!", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "c3Y2RAvySqR4DvLVl1M9j", + "originalText": "...is awesome!", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "line", + "version": 581, + "versionNonce": 848946801, + "isDeleted": false, + "id": "ntZcF-bMFQcnE_kXFdtBg", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 459.42694782388145, + "y": 190.50435327800608, + "strokeColor": "#000000", + "backgroundColor": "#ffff01", + "width": 86.8590087890625, + "height": 48.04501342773438, + "seed": 1717922455, + "groupIds": [ + "G0vZnd7Nf-8UW_fS8MpUK", + "RrIwKI_4Jm_6D7fm1aF85" + ], + "roundness": null, + "boundElements": [], + "updated": 1681683022099, + "link": null, + "locked": false, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 44.75, + -3.552713678800501e-15 + ], + [ + 44.75, + 38.292022705078125 + ], + [ + 86.70098876953125, + 38.29202270507812 + ], + [ + 52.31500244140626, + 14.580993652343757 + ], + [ + 86.8590087890625, + -9.239990234375 + ], + [ + 86.8590087890625, + -9.752990722656257 + ], + [ + 0, + -9.75299072265625 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 579, + "versionNonce": 1228059935, + "isDeleted": false, + "id": "y8BXQqyVafkHqZaICEyOK", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 459.42694782388145, + "y": 180.75136255534983, + "strokeColor": "#000000", + "backgroundColor": "#fa5252", + "width": 44.75, + "height": 9.75299072265625, + "seed": 1974075319, + "groupIds": [ + "oW9GpctyJI59Dt9k3AfWx", + "RrIwKI_4Jm_6D7fm1aF85" + ], + "roundness": null, + "boundElements": [], + "updated": 1681683022099, + "link": null, + "locked": false, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 44.75, + 9.75299072265625 + ] + ] + }, + { + "type": "line", + "version": 581, + "versionNonce": 542502993, + "isDeleted": false, + "id": "h2Csb5JThD3SS-ECFtr2S", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 259.91889118325656, + "y": 315.87733911784983, + "strokeColor": "#000000", + "backgroundColor": "#ffff01", + "width": 86.8590087890625, + "height": 48.04501342773438, + "seed": 366932183, + "groupIds": [ + "_FD1VkV7hagWKonBpsRZ6", + "RrIwKI_4Jm_6D7fm1aF85" + ], + "roundness": null, + "boundElements": [], + "updated": 1681683022099, + "link": null, + "locked": false, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -44.75, + 3.552713678800501e-15 + ], + [ + -44.75, + -38.292022705078125 + ], + [ + -86.70098876953125, + -38.29202270507812 + ], + [ + -52.31500244140626, + -14.580993652343757 + ], + [ + -86.8590087890625, + 9.239990234375 + ], + [ + -86.8590087890625, + 9.752990722656257 + ], + [ + 0, + 9.75299072265625 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 579, + "versionNonce": 746371391, + "isDeleted": false, + "id": "uJu9aL9qNfixp-o33hEh7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 259.91889118325656, + "y": 325.6303298405061, + "strokeColor": "#000000", + "backgroundColor": "#fa5252", + "width": 44.75, + "height": 9.75299072265625, + "seed": 10010103, + "groupIds": [ + "u4v2V7Icu3FM83jH18Gxp", + "RrIwKI_4Jm_6D7fm1aF85" + ], + "roundness": null, + "boundElements": [], + "updated": 1681683022099, + "link": null, + "locked": false, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -44.75, + -9.75299072265625 + ] + ] + }, + { + "type": "rectangle", + "version": 581, + "versionNonce": 1907736113, + "isDeleted": false, + "id": "3V7fv5Mf1u9uTHSmA-gIb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 215.40526126992648, + "y": 190.50395654949045, + "strokeColor": "#000000", + "backgroundColor": "#ffff01", + "width": 288.77532958984375, + "height": 48.04499816894531, + "seed": 1222371095, + "groupIds": [ + "RrIwKI_4Jm_6D7fm1aF85" + ], + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "3VBIT2lqHuXdKoXcgnwyi" + } + ], + "updated": 1681683022099, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 511, + "versionNonce": 2083600735, + "isDeleted": false, + "id": "3VBIT2lqHuXdKoXcgnwyi", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 299.9529678739305, + "y": 202.0264556339631, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 119.67991638183594, + "height": 25, + "seed": 1691986841, + "groupIds": [ + "RrIwKI_4Jm_6D7fm1aF85" + ], + "roundness": null, + "boundElements": [], + "updated": 1681683022099, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Excalidraw...", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "3V7fv5Mf1u9uTHSmA-gIb", + "originalText": "Excalidraw...", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "line", + "version": 885, + "versionNonce": 94425105, + "isDeleted": false, + "id": "H5cRnJN1GqcNcqR75fWFc", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 215.85109593027113, + "y": 238.17762972361527, + "strokeColor": "#000000", + "backgroundColor": "#fd7e14", + "width": 288.5206653225805, + "height": 29.61693548387092, + "seed": 756870199, + "groupIds": [ + "RrIwKI_4Jm_6D7fm1aF85" + ], + "roundness": null, + "boundElements": [], + "updated": 1681683022099, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 109.2578125, + 29.518649193548328 + ], + [ + 288.5206653225805, + 29.61693548387092 + ], + [ + 180.33140120967732, + 0.5972782258064768 + ], + [ + 0, + 0 + ] + ] + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/docs/static/images/attach-engine-to-catalog.png b/docs/static/images/attach-engine-to-catalog.png new file mode 100644 index 000000000..46fe5379a Binary files /dev/null and b/docs/static/images/attach-engine-to-catalog.png differ diff --git a/docs/static/images/create-catalog.png b/docs/static/images/create-catalog.png new file mode 100644 index 000000000..593057357 Binary files /dev/null and b/docs/static/images/create-catalog.png differ diff --git a/docs/static/images/create-engine.png b/docs/static/images/create-engine.png new file mode 100644 index 000000000..8ff3249a8 Binary files /dev/null and b/docs/static/images/create-engine.png differ diff --git a/docs/static/images/dj-landing.png b/docs/static/images/dj-landing.png new file mode 100644 index 000000000..44f9ba5ea Binary files /dev/null and b/docs/static/images/dj-landing.png differ diff --git a/docs/static/images/dj-logo.svg b/docs/static/images/dj-logo.svg new file mode 100644 index 000000000..7c6ef90da --- /dev/null +++ b/docs/static/images/dj-logo.svg @@ -0,0 +1,26 @@ + + + DJ 2 + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/static/images/login-screenshot.png b/docs/static/images/login-screenshot.png new file mode 100644 index 000000000..5fcd88383 Binary files /dev/null and b/docs/static/images/login-screenshot.png differ diff --git a/docs/static/images/signup-screenshot.png b/docs/static/images/signup-screenshot.png new file mode 100644 index 000000000..1d7d94743 Binary files /dev/null and b/docs/static/images/signup-screenshot.png differ diff --git a/docs/static/images/tpch-erd.png b/docs/static/images/tpch-erd.png new file mode 100644 index 000000000..4c88b20ec Binary files /dev/null and b/docs/static/images/tpch-erd.png differ diff --git a/docs/static/images/tpch-explore-view.png b/docs/static/images/tpch-explore-view.png new file mode 100644 index 000000000..4cd60b80f Binary files /dev/null and b/docs/static/images/tpch-explore-view.png differ diff --git a/docs/static/images/tpch_metrics.num_orders.png b/docs/static/images/tpch_metrics.num_orders.png new file mode 100644 index 000000000..9e6f31f5f Binary files /dev/null and b/docs/static/images/tpch_metrics.num_orders.png differ diff --git a/docs/themes/doks b/docs/themes/doks new file mode 160000 index 000000000..f7e92deee --- /dev/null +++ b/docs/themes/doks @@ -0,0 +1 @@ +Subproject commit f7e92deee3c905693b7e412cca66bd537e7be7d5 diff --git a/docs/yarn.lock b/docs/yarn.lock new file mode 100644 index 000000000..cfda245d9 --- /dev/null +++ b/docs/yarn.lock @@ -0,0 +1,4813 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@aashutoshrathi/word-wrap@^1.2.3": + "integrity" "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==" + "resolved" "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz" + "version" "1.2.6" + +"@ampproject/remapping@^2.2.0": + "integrity" "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==" + "resolved" "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz" + "version" "2.2.1" + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/cli@^7.18": + "integrity" "sha512-rM9ZMmaII630zGvtMtQ3P4GyHs28CHLYE9apLG7L8TgaSqcfoIGrlLSLsh4Q8kDTdZQQEXZm1M0nQtOvU/2heg==" + "resolved" "https://registry.npmjs.org/@babel/cli/-/cli-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + "commander" "^4.0.1" + "convert-source-map" "^1.1.0" + "fs-readdir-recursive" "^1.1.0" + "glob" "^7.2.0" + "make-dir" "^2.1.0" + "slash" "^2.0.0" + optionalDependencies: + "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3" + "chokidar" "^3.4.0" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.10", "@babel/code-frame@^7.22.5": + "integrity" "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==" + "resolved" "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/highlight" "^7.22.10" + "chalk" "^2.4.2" + +"@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": + "integrity" "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==" + "resolved" "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz" + "version" "7.22.9" + +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.0.0-0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.12.0", "@babel/core@^7.13.0", "@babel/core@^7.18", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0": + "integrity" "sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw==" + "resolved" "https://registry.npmjs.org/@babel/core/-/core-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.10" + "@babel/generator" "^7.22.10" + "@babel/helper-compilation-targets" "^7.22.10" + "@babel/helper-module-transforms" "^7.22.9" + "@babel/helpers" "^7.22.10" + "@babel/parser" "^7.22.10" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.10" + "@babel/types" "^7.22.10" + "convert-source-map" "^1.7.0" + "debug" "^4.1.0" + "gensync" "^1.0.0-beta.2" + "json5" "^2.2.2" + "semver" "^6.3.1" + +"@babel/generator@^7.22.10": + "integrity" "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==" + "resolved" "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/types" "^7.22.10" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + "jsesc" "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.22.5": + "integrity" "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==" + "resolved" "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": + "integrity" "sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/types" "^7.22.10" + +"@babel/helper-compilation-targets@^7.22.10", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": + "integrity" "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==" + "resolved" "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-validator-option" "^7.22.5" + "browserslist" "^4.21.9" + "lru-cache" "^5.1.1" + "semver" "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.22.5": + "integrity" "sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA==" + "resolved" "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "semver" "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5": + "integrity" "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==" + "resolved" "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz" + "version" "7.22.9" + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "regexpu-core" "^5.3.1" + "semver" "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.4.2": + "integrity" "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==" + "resolved" "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz" + "version" "0.4.2" + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + "debug" "^4.1.1" + "lodash.debounce" "^4.0.8" + "resolve" "^1.14.2" + +"@babel/helper-environment-visitor@^7.22.5": + "integrity" "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==" + "resolved" "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz" + "version" "7.22.5" + +"@babel/helper-function-name@^7.22.5": + "integrity" "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-hoist-variables@^7.22.5": + "integrity" "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==" + "resolved" "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-member-expression-to-functions@^7.22.5": + "integrity" "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.22.5": + "integrity" "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==" + "resolved" "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9": + "integrity" "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz" + "version" "7.22.9" + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.5" + +"@babel/helper-optimise-call-expression@^7.22.5": + "integrity" "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==" + "resolved" "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + "integrity" "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==" + "resolved" "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz" + "version" "7.22.5" + +"@babel/helper-remap-async-to-generator@^7.22.5", "@babel/helper-remap-async-to-generator@^7.22.9": + "integrity" "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz" + "version" "7.22.9" + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-wrap-function" "^7.22.9" + +"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9": + "integrity" "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==" + "resolved" "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz" + "version" "7.22.9" + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + +"@babel/helper-simple-access@^7.22.5": + "integrity" "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==" + "resolved" "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + "integrity" "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==" + "resolved" "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + "integrity" "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==" + "resolved" "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz" + "version" "7.22.6" + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + "integrity" "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" + "resolved" "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz" + "version" "7.22.5" + +"@babel/helper-validator-identifier@^7.22.5": + "integrity" "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz" + "version" "7.22.5" + +"@babel/helper-validator-option@^7.22.5": + "integrity" "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==" + "resolved" "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz" + "version" "7.22.5" + +"@babel/helper-wrap-function@^7.22.9": + "integrity" "sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/helper-function-name" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.10" + +"@babel/helpers@^7.22.10": + "integrity" "sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==" + "resolved" "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.10" + "@babel/types" "^7.22.10" + +"@babel/highlight@^7.22.10": + "integrity" "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==" + "resolved" "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/helper-validator-identifier" "^7.22.5" + "chalk" "^2.4.2" + "js-tokens" "^4.0.0" + +"@babel/parser@^7.22.10", "@babel/parser@^7.22.5": + "integrity" "sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==" + "resolved" "https://registry.npmjs.org/@babel/parser/-/parser-7.22.10.tgz" + "version" "7.22.10" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": + "integrity" "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.5": + "integrity" "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==" + "resolved" "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.5" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + "integrity" "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz" + "version" "7.21.0-placeholder-for-preset-env.2" + +"@babel/plugin-syntax-async-generators@^7.8.4": + "integrity" "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" + "version" "7.8.4" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + "integrity" "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" + "version" "7.12.13" + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + "integrity" "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + "integrity" "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + "integrity" "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-import-assertions@^7.22.5": + "integrity" "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-attributes@^7.22.5": + "integrity" "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-meta@^7.10.4": + "integrity" "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" + "version" "7.10.4" + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + "integrity" "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + "integrity" "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" + "version" "7.10.4" + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + "integrity" "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + "integrity" "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" + "version" "7.10.4" + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + "integrity" "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + "integrity" "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + "integrity" "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + "integrity" "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + "integrity" "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + "integrity" "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz" + "version" "7.18.6" + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.22.5": + "integrity" "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-async-generator-functions@^7.22.10": + "integrity" "sha512-eueE8lvKVzq5wIObKK/7dvoeKJ+xc6TvRn6aysIjS6pSCeLy7S/eVi7pEQknZqyqvzaNKdDtem8nUNTBgDVR2g==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.9" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-transform-async-to-generator@^7.22.5": + "integrity" "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.5" + +"@babel/plugin-transform-block-scoped-functions@^7.22.5": + "integrity" "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-block-scoping@^7.22.10": + "integrity" "sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-properties@^7.22.5": + "integrity" "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-static-block@^7.22.5": + "integrity" "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-transform-classes@^7.22.6": + "integrity" "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz" + "version" "7.22.6" + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "globals" "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.22.5": + "integrity" "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/template" "^7.22.5" + +"@babel/plugin-transform-destructuring@^7.22.10": + "integrity" "sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dotall-regex@^7.22.5": + "integrity" "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-duplicate-keys@^7.22.5": + "integrity" "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dynamic-import@^7.22.5": + "integrity" "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-transform-exponentiation-operator@^7.22.5": + "integrity" "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-export-namespace-from@^7.22.5": + "integrity" "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-transform-for-of@^7.22.5": + "integrity" "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-function-name@^7.22.5": + "integrity" "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-json-strings@^7.22.5": + "integrity" "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-transform-literals@^7.22.5": + "integrity" "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-logical-assignment-operators@^7.22.5": + "integrity" "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.22.5": + "integrity" "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-modules-amd@^7.22.5": + "integrity" "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-modules-commonjs@^7.22.5": + "integrity" "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + +"@babel/plugin-transform-modules-systemjs@^7.22.5": + "integrity" "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + +"@babel/plugin-transform-modules-umd@^7.22.5": + "integrity" "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": + "integrity" "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-new-target@^7.22.5": + "integrity" "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.22.5": + "integrity" "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-transform-numeric-separator@^7.22.5": + "integrity" "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-transform-object-rest-spread@^7.22.5": + "integrity" "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/compat-data" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.22.5" + +"@babel/plugin-transform-object-super@^7.22.5": + "integrity" "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + +"@babel/plugin-transform-optional-catch-binding@^7.22.5": + "integrity" "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-transform-optional-chaining@^7.22.10", "@babel/plugin-transform-optional-chaining@^7.22.5": + "integrity" "sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-transform-parameters@^7.22.5": + "integrity" "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-methods@^7.22.5": + "integrity" "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-property-in-object@^7.22.5": + "integrity" "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-transform-property-literals@^7.22.5": + "integrity" "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-regenerator@^7.22.10": + "integrity" "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "regenerator-transform" "^0.15.2" + +"@babel/plugin-transform-reserved-words@^7.22.5": + "integrity" "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-shorthand-properties@^7.22.5": + "integrity" "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-spread@^7.22.5": + "integrity" "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + +"@babel/plugin-transform-sticky-regex@^7.22.5": + "integrity" "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-template-literals@^7.22.5": + "integrity" "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-typeof-symbol@^7.22.5": + "integrity" "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-escapes@^7.22.10": + "integrity" "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-property-regex@^7.22.5": + "integrity" "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-regex@^7.22.5": + "integrity" "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-sets-regex@^7.22.5": + "integrity" "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/preset-env@^7.18": + "integrity" "sha512-riHpLb1drNkpLlocmSyEg4oYJIQFeXAK/d7rI6mbD0XsvoTOOweXDmQPG/ErxsEhWk3rl3Q/3F6RFQlVFS8m0A==" + "resolved" "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.10" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.5" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.5" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.5" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.22.5" + "@babel/plugin-syntax-import-attributes" "^7.22.5" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.22.5" + "@babel/plugin-transform-async-generator-functions" "^7.22.10" + "@babel/plugin-transform-async-to-generator" "^7.22.5" + "@babel/plugin-transform-block-scoped-functions" "^7.22.5" + "@babel/plugin-transform-block-scoping" "^7.22.10" + "@babel/plugin-transform-class-properties" "^7.22.5" + "@babel/plugin-transform-class-static-block" "^7.22.5" + "@babel/plugin-transform-classes" "^7.22.6" + "@babel/plugin-transform-computed-properties" "^7.22.5" + "@babel/plugin-transform-destructuring" "^7.22.10" + "@babel/plugin-transform-dotall-regex" "^7.22.5" + "@babel/plugin-transform-duplicate-keys" "^7.22.5" + "@babel/plugin-transform-dynamic-import" "^7.22.5" + "@babel/plugin-transform-exponentiation-operator" "^7.22.5" + "@babel/plugin-transform-export-namespace-from" "^7.22.5" + "@babel/plugin-transform-for-of" "^7.22.5" + "@babel/plugin-transform-function-name" "^7.22.5" + "@babel/plugin-transform-json-strings" "^7.22.5" + "@babel/plugin-transform-literals" "^7.22.5" + "@babel/plugin-transform-logical-assignment-operators" "^7.22.5" + "@babel/plugin-transform-member-expression-literals" "^7.22.5" + "@babel/plugin-transform-modules-amd" "^7.22.5" + "@babel/plugin-transform-modules-commonjs" "^7.22.5" + "@babel/plugin-transform-modules-systemjs" "^7.22.5" + "@babel/plugin-transform-modules-umd" "^7.22.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" + "@babel/plugin-transform-new-target" "^7.22.5" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.5" + "@babel/plugin-transform-numeric-separator" "^7.22.5" + "@babel/plugin-transform-object-rest-spread" "^7.22.5" + "@babel/plugin-transform-object-super" "^7.22.5" + "@babel/plugin-transform-optional-catch-binding" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.10" + "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/plugin-transform-private-methods" "^7.22.5" + "@babel/plugin-transform-private-property-in-object" "^7.22.5" + "@babel/plugin-transform-property-literals" "^7.22.5" + "@babel/plugin-transform-regenerator" "^7.22.10" + "@babel/plugin-transform-reserved-words" "^7.22.5" + "@babel/plugin-transform-shorthand-properties" "^7.22.5" + "@babel/plugin-transform-spread" "^7.22.5" + "@babel/plugin-transform-sticky-regex" "^7.22.5" + "@babel/plugin-transform-template-literals" "^7.22.5" + "@babel/plugin-transform-typeof-symbol" "^7.22.5" + "@babel/plugin-transform-unicode-escapes" "^7.22.10" + "@babel/plugin-transform-unicode-property-regex" "^7.22.5" + "@babel/plugin-transform-unicode-regex" "^7.22.5" + "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" + "@babel/preset-modules" "0.1.6-no-external-plugins" + "@babel/types" "^7.22.10" + "babel-plugin-polyfill-corejs2" "^0.4.5" + "babel-plugin-polyfill-corejs3" "^0.8.3" + "babel-plugin-polyfill-regenerator" "^0.5.2" + "core-js-compat" "^3.31.0" + "semver" "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + "integrity" "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==" + "resolved" "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz" + "version" "0.1.6-no-external-plugins" + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + "esutils" "^2.0.2" + +"@babel/regjsgen@^0.8.0": + "integrity" "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" + "resolved" "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz" + "version" "0.8.0" + +"@babel/runtime@^7.8.4": + "integrity" "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==" + "resolved" "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "regenerator-runtime" "^0.14.0" + +"@babel/template@^7.22.5": + "integrity" "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==" + "resolved" "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz" + "version" "7.22.5" + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/traverse@^7.22.10": + "integrity" "sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==" + "resolved" "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/code-frame" "^7.22.10" + "@babel/generator" "^7.22.10" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.22.10" + "@babel/types" "^7.22.10" + "debug" "^4.1.0" + "globals" "^11.1.0" + +"@babel/types@^7.22.10", "@babel/types@^7.22.5", "@babel/types@^7.4.4": + "integrity" "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==" + "resolved" "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz" + "version" "7.22.10" + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + "to-fast-properties" "^2.0.0" + +"@braintree/sanitize-url@^6.0.1": + "integrity" "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==" + "resolved" "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz" + "version" "6.0.4" + +"@csstools/selector-specificity@^2.0.2": + "integrity" "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==" + "resolved" "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz" + "version" "2.2.0" + +"@eslint-community/eslint-utils@^4.2.0": + "integrity" "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==" + "resolved" "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" + "version" "4.4.0" + dependencies: + "eslint-visitor-keys" "^3.3.0" + +"@eslint-community/regexpp@^4.6.1": + "integrity" "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==" + "resolved" "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz" + "version" "4.6.2" + +"@eslint/eslintrc@^2.1.1": + "integrity" "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==" + "resolved" "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz" + "version" "2.1.1" + dependencies: + "ajv" "^6.12.4" + "debug" "^4.3.2" + "espree" "^9.6.0" + "globals" "^13.19.0" + "ignore" "^5.2.0" + "import-fresh" "^3.2.1" + "js-yaml" "^4.1.0" + "minimatch" "^3.1.2" + "strip-json-comments" "^3.1.1" + +"@eslint/js@^8.46.0": + "integrity" "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==" + "resolved" "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz" + "version" "8.46.0" + +"@fullhuman/postcss-purgecss@^4.1": + "integrity" "sha512-jqcsyfvq09VOsMXxJMPLRF6Fhg/NNltzWKnC9qtzva+QKTxerCO4esG6je7hbnmkpZtaDyPTwMBj9bzfWorsrw==" + "resolved" "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-4.1.3.tgz" + "version" "4.1.3" + dependencies: + "purgecss" "^4.1.3" + +"@humanwhocodes/config-array@^0.11.10": + "integrity" "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==" + "resolved" "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz" + "version" "0.11.10" + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + "debug" "^4.1.1" + "minimatch" "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + "integrity" "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + "resolved" "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" + "version" "1.0.1" + +"@humanwhocodes/object-schema@^1.2.1": + "integrity" "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + "resolved" "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" + "version" "1.2.1" + +"@hyas/images@^0.2.2": + "integrity" "sha512-PoGQ8DC3erHHS9sAlsrrGOlfqV3RgmIFYhSWcigB/7tE6tUNj4j57T4MjmlEyNcawlVsHvCaoYqPaElQn24aaQ==" + "resolved" "https://registry.npmjs.org/@hyas/images/-/images-0.2.3.tgz" + "version" "0.2.3" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + "integrity" "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==" + "resolved" "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz" + "version" "0.3.3" + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.1.0": + "integrity" "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" + "resolved" "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz" + "version" "3.1.1" + +"@jridgewell/set-array@^1.0.1": + "integrity" "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + "resolved" "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" + "version" "1.1.2" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + "integrity" "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "resolved" "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" + "version" "1.4.15" + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + "integrity" "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==" + "resolved" "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz" + "version" "0.3.19" + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": + "integrity" "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==" + "resolved" "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz" + "version" "2.1.8-no-fsevents.3" + +"@nodelib/fs.scandir@2.1.5": + "integrity" "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==" + "resolved" "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + "version" "2.1.5" + dependencies: + "@nodelib/fs.stat" "2.0.5" + "run-parallel" "^1.1.9" + +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": + "integrity" "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + "resolved" "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + "version" "2.0.5" + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + "integrity" "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==" + "resolved" "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + "version" "1.2.8" + dependencies: + "@nodelib/fs.scandir" "2.1.5" + "fastq" "^1.6.0" + +"@popperjs/core@^2.11.8": + "integrity" "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" + "resolved" "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" + "version" "2.11.8" + +"@sindresorhus/is@^5.2.0": + "integrity" "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==" + "resolved" "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz" + "version" "5.6.0" + +"@szmarczak/http-timer@^5.0.1": + "integrity" "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==" + "resolved" "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "defer-to-connect" "^2.0.1" + +"@types/cacheable-request@^6.0.2": + "integrity" "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==" + "resolved" "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz" + "version" "6.0.3" + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "^3.1.4" + "@types/node" "*" + "@types/responselike" "^1.0.0" + +"@types/d3-scale-chromatic@^3.0.0": + "integrity" "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==" + "resolved" "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz" + "version" "3.0.0" + +"@types/d3-scale@^4.0.3": + "integrity" "sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==" + "resolved" "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz" + "version" "4.0.3" + dependencies: + "@types/d3-time" "*" + +"@types/d3-time@*": + "integrity" "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==" + "resolved" "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz" + "version" "3.0.0" + +"@types/debug@^4.0.0": + "integrity" "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==" + "resolved" "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz" + "version" "4.1.8" + dependencies: + "@types/ms" "*" + +"@types/http-cache-semantics@*": + "integrity" "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + "resolved" "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz" + "version" "4.0.1" + +"@types/keyv@^3.1.4": + "integrity" "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==" + "resolved" "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz" + "version" "3.1.4" + dependencies: + "@types/node" "*" + +"@types/mdast@^3.0.0": + "integrity" "sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==" + "resolved" "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz" + "version" "3.0.12" + dependencies: + "@types/unist" "^2" + +"@types/minimist@^1.2.0": + "integrity" "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" + "resolved" "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz" + "version" "1.2.2" + +"@types/ms@*": + "integrity" "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + "resolved" "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz" + "version" "0.7.31" + +"@types/node@*": + "integrity" "sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg==" + "resolved" "https://registry.npmjs.org/@types/node/-/node-20.4.8.tgz" + "version" "20.4.8" + +"@types/normalize-package-data@^2.4.0": + "integrity" "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" + "resolved" "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz" + "version" "2.4.1" + +"@types/parse-json@^4.0.0": + "integrity" "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + "resolved" "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz" + "version" "4.0.0" + +"@types/responselike@^1.0.0": + "integrity" "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==" + "resolved" "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "@types/node" "*" + +"@types/unist@^2", "@types/unist@^2.0.0": + "integrity" "sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==" + "resolved" "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz" + "version" "2.0.7" + +"acorn-jsx@^5.3.2": + "integrity" "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" + "resolved" "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + "version" "5.3.2" + +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", "acorn@^8.9.0": + "integrity" "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" + "resolved" "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz" + "version" "8.10.0" + +"aggregate-error@^4.0.0": + "integrity" "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==" + "resolved" "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "clean-stack" "^4.0.0" + "indent-string" "^5.0.0" + +"ajv@^6.12.4": + "integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==" + "resolved" "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + "version" "6.12.6" + dependencies: + "fast-deep-equal" "^3.1.1" + "fast-json-stable-stringify" "^2.0.0" + "json-schema-traverse" "^0.4.1" + "uri-js" "^4.2.2" + +"ajv@^8.0.1": + "integrity" "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==" + "resolved" "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" + "version" "8.12.0" + dependencies: + "fast-deep-equal" "^3.1.1" + "json-schema-traverse" "^1.0.0" + "require-from-string" "^2.0.2" + "uri-js" "^4.2.2" + +"ansi-regex@^5.0.1": + "integrity" "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "resolved" "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + "version" "5.0.1" + +"ansi-styles@^3.2.1": + "integrity" "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==" + "resolved" "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" + "version" "3.2.1" + dependencies: + "color-convert" "^1.9.0" + +"ansi-styles@^4.0.0": + "integrity" "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==" + "resolved" "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + "version" "4.3.0" + dependencies: + "color-convert" "^2.0.1" + +"ansi-styles@^4.1.0": + "integrity" "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==" + "resolved" "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + "version" "4.3.0" + dependencies: + "color-convert" "^2.0.1" + +"anymatch@~3.1.2": + "integrity" "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==" + "resolved" "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" + "version" "3.1.3" + dependencies: + "normalize-path" "^3.0.0" + "picomatch" "^2.0.4" + +"argparse@^2.0.1": + "integrity" "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "resolved" "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + "version" "2.0.1" + +"array-union@^2.1.0": + "integrity" "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + "resolved" "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" + "version" "2.1.0" + +"array-union@^3.0.1": + "integrity" "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==" + "resolved" "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz" + "version" "3.0.1" + +"arrify@^1.0.1": + "integrity" "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==" + "resolved" "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" + "version" "1.0.1" + +"astral-regex@^2.0.0": + "integrity" "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==" + "resolved" "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz" + "version" "2.0.0" + +"auto-changelog@^2.4": + "integrity" "sha512-vh17hko1c0ItsEcw6m7qPRf3m45u+XK5QyCrrBFViElZ8jnKrPC1roSznrd1fIB/0vR/zawdECCRJtTuqIXaJw==" + "resolved" "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.4.0.tgz" + "version" "2.4.0" + dependencies: + "commander" "^7.2.0" + "handlebars" "^4.7.7" + "node-fetch" "^2.6.1" + "parse-github-url" "^1.0.2" + "semver" "^7.3.5" + +"autoprefixer@^10.4": + "integrity" "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==" + "resolved" "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz" + "version" "10.4.14" + dependencies: + "browserslist" "^4.21.5" + "caniuse-lite" "^1.0.30001464" + "fraction.js" "^4.2.0" + "normalize-range" "^0.1.2" + "picocolors" "^1.0.0" + "postcss-value-parser" "^4.2.0" + +"babel-plugin-polyfill-corejs2@^0.4.5": + "integrity" "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==" + "resolved" "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz" + "version" "0.4.5" + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.4.2" + "semver" "^6.3.1" + +"babel-plugin-polyfill-corejs3@^0.8.3": + "integrity" "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==" + "resolved" "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz" + "version" "0.8.3" + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.2" + "core-js-compat" "^3.31.0" + +"babel-plugin-polyfill-regenerator@^0.5.2": + "integrity" "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==" + "resolved" "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz" + "version" "0.5.2" + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.2" + +"balanced-match@^1.0.0": + "integrity" "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "resolved" "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + "version" "1.0.2" + +"balanced-match@^2.0.0": + "integrity" "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==" + "resolved" "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz" + "version" "2.0.0" + +"base64-js@^1.3.1": + "integrity" "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + "resolved" "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + "version" "1.5.1" + +"binary-extensions@^2.0.0": + "integrity" "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + "resolved" "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" + "version" "2.2.0" + +"bl@^1.0.0": + "integrity" "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==" + "resolved" "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz" + "version" "1.2.3" + dependencies: + "readable-stream" "^2.3.5" + "safe-buffer" "^5.1.1" + +"bootstrap@^5.2.0-beta1": + "integrity" "sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g==" + "resolved" "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.1.tgz" + "version" "5.3.1" + +"brace-expansion@^1.1.7": + "integrity" "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==" + "resolved" "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + "version" "1.1.11" + dependencies: + "balanced-match" "^1.0.0" + "concat-map" "0.0.1" + +"braces@^3.0.1", "braces@^3.0.2", "braces@~3.0.2": + "integrity" "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==" + "resolved" "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" + "version" "3.0.2" + dependencies: + "fill-range" "^7.0.1" + +"browserslist@^4.21.5", "browserslist@^4.21.9", "browserslist@>= 4.21.0": + "integrity" "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==" + "resolved" "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz" + "version" "4.21.10" + dependencies: + "caniuse-lite" "^1.0.30001517" + "electron-to-chromium" "^1.4.477" + "node-releases" "^2.0.13" + "update-browserslist-db" "^1.0.11" + +"buffer-alloc-unsafe@^1.1.0": + "integrity" "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + "resolved" "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz" + "version" "1.1.0" + +"buffer-alloc@^1.2.0": + "integrity" "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==" + "resolved" "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz" + "version" "1.2.0" + dependencies: + "buffer-alloc-unsafe" "^1.1.0" + "buffer-fill" "^1.0.0" + +"buffer-crc32@~0.2.3": + "integrity" "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" + "resolved" "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz" + "version" "0.2.13" + +"buffer-fill@^1.0.0": + "integrity" "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==" + "resolved" "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz" + "version" "1.0.0" + +"buffer@^5.2.1": + "integrity" "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==" + "resolved" "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" + "version" "5.7.1" + dependencies: + "base64-js" "^1.3.1" + "ieee754" "^1.1.13" + +"cacheable-lookup@^6.0.4": + "integrity" "sha512-KJ/Dmo1lDDhmW2XDPMo+9oiy/CeqosPguPCrgcVzKyZrL6pM1gU2GmPY/xo6OQPTUaA/c0kwHuywB4E6nmT9ww==" + "resolved" "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.1.0.tgz" + "version" "6.1.0" + +"cacheable-request@^7.0.2": + "integrity" "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==" + "resolved" "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz" + "version" "7.0.4" + dependencies: + "clone-response" "^1.0.2" + "get-stream" "^5.1.0" + "http-cache-semantics" "^4.0.0" + "keyv" "^4.0.0" + "lowercase-keys" "^2.0.0" + "normalize-url" "^6.0.1" + "responselike" "^2.0.0" + +"callsites@^3.0.0": + "integrity" "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + "resolved" "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + "version" "3.1.0" + +"camelcase-keys@^6.2.2": + "integrity" "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==" + "resolved" "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz" + "version" "6.2.2" + dependencies: + "camelcase" "^5.3.1" + "map-obj" "^4.0.0" + "quick-lru" "^4.0.1" + +"camelcase@^5.0.0", "camelcase@^5.3.1": + "integrity" "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "resolved" "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + "version" "5.3.1" + +"caniuse-lite@^1.0.30001464", "caniuse-lite@^1.0.30001517": + "integrity" "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==" + "resolved" "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz" + "version" "1.0.30001519" + +"chalk@^2.4.2": + "integrity" "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==" + "resolved" "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + "version" "2.4.2" + dependencies: + "ansi-styles" "^3.2.1" + "escape-string-regexp" "^1.0.5" + "supports-color" "^5.3.0" + +"chalk@^4.0.0": + "integrity" "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==" + "resolved" "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + "version" "4.1.2" + dependencies: + "ansi-styles" "^4.1.0" + "supports-color" "^7.1.0" + +"character-entities@^2.0.0": + "integrity" "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==" + "resolved" "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz" + "version" "2.0.2" + +"chokidar@^3.3.0", "chokidar@^3.4.0": + "integrity" "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==" + "resolved" "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" + "version" "3.5.3" + dependencies: + "anymatch" "~3.1.2" + "braces" "~3.0.2" + "glob-parent" "~5.1.2" + "is-binary-path" "~2.1.0" + "is-glob" "~4.0.1" + "normalize-path" "~3.0.0" + "readdirp" "~3.6.0" + optionalDependencies: + "fsevents" "~2.3.2" + +"clean-stack@^4.0.0": + "integrity" "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==" + "resolved" "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz" + "version" "4.2.0" + dependencies: + "escape-string-regexp" "5.0.0" + +"clipboard@^2.0": + "integrity" "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==" + "resolved" "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz" + "version" "2.0.11" + dependencies: + "good-listener" "^1.2.2" + "select" "^1.1.2" + "tiny-emitter" "^2.0.0" + +"cliui@^6.0.0": + "integrity" "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==" + "resolved" "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz" + "version" "6.0.0" + dependencies: + "string-width" "^4.2.0" + "strip-ansi" "^6.0.0" + "wrap-ansi" "^6.2.0" + +"cliui@^7.0.2": + "integrity" "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==" + "resolved" "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" + "version" "7.0.4" + dependencies: + "string-width" "^4.2.0" + "strip-ansi" "^6.0.0" + "wrap-ansi" "^7.0.0" + +"clone-response@^1.0.2": + "integrity" "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==" + "resolved" "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz" + "version" "1.0.3" + dependencies: + "mimic-response" "^1.0.0" + +"color-convert@^1.9.0": + "integrity" "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==" + "resolved" "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + "version" "1.9.3" + dependencies: + "color-name" "1.1.3" + +"color-convert@^2.0.1": + "integrity" "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==" + "resolved" "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "color-name" "~1.1.4" + +"color-name@~1.1.4": + "integrity" "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "resolved" "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + "version" "1.1.4" + +"color-name@1.1.3": + "integrity" "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "resolved" "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + "version" "1.1.3" + +"colord@^2.9.3": + "integrity" "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + "resolved" "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz" + "version" "2.9.3" + +"commander@^2.8.1": + "integrity" "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "resolved" "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + "version" "2.20.3" + +"commander@^4.0.1": + "integrity" "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + "resolved" "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" + "version" "4.1.1" + +"commander@^7.2.0": + "integrity" "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + "resolved" "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" + "version" "7.2.0" + +"commander@^8.0.0": + "integrity" "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" + "resolved" "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" + "version" "8.3.0" + +"commander@^8.3.0": + "integrity" "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" + "resolved" "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" + "version" "8.3.0" + +"commander@7": + "integrity" "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + "resolved" "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" + "version" "7.2.0" + +"concat-map@0.0.1": + "integrity" "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "resolved" "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + "version" "0.0.1" + +"convert-source-map@^1.1.0", "convert-source-map@^1.7.0": + "integrity" "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "resolved" "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" + "version" "1.9.0" + +"core-js-compat@^3.31.0": + "integrity" "sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw==" + "resolved" "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.0.tgz" + "version" "3.32.0" + dependencies: + "browserslist" "^4.21.9" + +"core-util-is@~1.0.0": + "integrity" "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + "resolved" "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" + "version" "1.0.3" + +"cose-base@^1.0.0": + "integrity" "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==" + "resolved" "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz" + "version" "1.0.3" + dependencies: + "layout-base" "^1.0.0" + +"cosmiconfig@^7.1.0": + "integrity" "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==" + "resolved" "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz" + "version" "7.1.0" + dependencies: + "@types/parse-json" "^4.0.0" + "import-fresh" "^3.2.1" + "parse-json" "^5.0.0" + "path-type" "^4.0.0" + "yaml" "^1.10.0" + +"cross-spawn@^7.0.2": + "integrity" "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==" + "resolved" "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + "version" "7.0.3" + dependencies: + "path-key" "^3.1.0" + "shebang-command" "^2.0.0" + "which" "^2.0.1" + +"css-functions-list@^3.1.0": + "integrity" "sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg==" + "resolved" "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.0.tgz" + "version" "3.2.0" + +"cssesc@^3.0.0": + "integrity" "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + "resolved" "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" + "version" "3.0.0" + +"cytoscape-cose-bilkent@^4.1.0": + "integrity" "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==" + "resolved" "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "cose-base" "^1.0.0" + +"cytoscape@^3.2.0", "cytoscape@^3.28.1": + "integrity" "sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==" + "resolved" "https://registry.npmjs.org/cytoscape/-/cytoscape-3.28.1.tgz" + "version" "3.28.1" + dependencies: + "heap" "^0.2.6" + "lodash" "^4.17.21" + +"d3-array@^3.2.0", "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", "d3-array@3": + "integrity" "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==" + "resolved" "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz" + "version" "3.2.4" + dependencies: + "internmap" "1 - 2" + +"d3-array@1 - 2": + "integrity" "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==" + "resolved" "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz" + "version" "2.12.1" + dependencies: + "internmap" "^1.0.0" + +"d3-axis@3": + "integrity" "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" + "resolved" "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz" + "version" "3.0.0" + +"d3-brush@3": + "integrity" "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==" + "resolved" "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "d3-dispatch" "1 - 3" + "d3-drag" "2 - 3" + "d3-interpolate" "1 - 3" + "d3-selection" "3" + "d3-transition" "3" + +"d3-chord@3": + "integrity" "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==" + "resolved" "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "d3-path" "1 - 3" + +"d3-color@1 - 3", "d3-color@3": + "integrity" "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + "resolved" "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz" + "version" "3.1.0" + +"d3-contour@4": + "integrity" "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==" + "resolved" "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz" + "version" "4.0.2" + dependencies: + "d3-array" "^3.2.0" + +"d3-delaunay@6": + "integrity" "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==" + "resolved" "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz" + "version" "6.0.4" + dependencies: + "delaunator" "5" + +"d3-dispatch@1 - 3", "d3-dispatch@3": + "integrity" "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + "resolved" "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz" + "version" "3.0.1" + +"d3-drag@2 - 3", "d3-drag@3": + "integrity" "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==" + "resolved" "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "d3-dispatch" "1 - 3" + "d3-selection" "3" + +"d3-dsv@1 - 3", "d3-dsv@3": + "integrity" "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==" + "resolved" "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "commander" "7" + "iconv-lite" "0.6" + "rw" "1" + +"d3-ease@1 - 3", "d3-ease@3": + "integrity" "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + "resolved" "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz" + "version" "3.0.1" + +"d3-fetch@3": + "integrity" "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==" + "resolved" "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "d3-dsv" "1 - 3" + +"d3-force@3": + "integrity" "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==" + "resolved" "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "d3-dispatch" "1 - 3" + "d3-quadtree" "1 - 3" + "d3-timer" "1 - 3" + +"d3-format@1 - 3", "d3-format@3": + "integrity" "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + "resolved" "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz" + "version" "3.1.0" + +"d3-geo@3": + "integrity" "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==" + "resolved" "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "d3-array" "2.5.0 - 3" + +"d3-hierarchy@3": + "integrity" "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" + "resolved" "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz" + "version" "3.1.2" + +"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", "d3-interpolate@3": + "integrity" "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==" + "resolved" "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "d3-color" "1 - 3" + +"d3-path@^3.1.0", "d3-path@1 - 3", "d3-path@3": + "integrity" "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" + "resolved" "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz" + "version" "3.1.0" + +"d3-path@1": + "integrity" "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + "resolved" "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz" + "version" "1.0.9" + +"d3-polygon@3": + "integrity" "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" + "resolved" "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz" + "version" "3.0.1" + +"d3-quadtree@1 - 3", "d3-quadtree@3": + "integrity" "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" + "resolved" "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz" + "version" "3.0.1" + +"d3-random@3": + "integrity" "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" + "resolved" "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz" + "version" "3.0.1" + +"d3-sankey@^0.12.3": + "integrity" "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==" + "resolved" "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz" + "version" "0.12.3" + dependencies: + "d3-array" "1 - 2" + "d3-shape" "^1.2.0" + +"d3-scale-chromatic@3": + "integrity" "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==" + "resolved" "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "d3-color" "1 - 3" + "d3-interpolate" "1 - 3" + +"d3-scale@4": + "integrity" "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==" + "resolved" "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz" + "version" "4.0.2" + dependencies: + "d3-array" "2.10.0 - 3" + "d3-format" "1 - 3" + "d3-interpolate" "1.2.0 - 3" + "d3-time" "2.1.1 - 3" + "d3-time-format" "2 - 4" + +"d3-selection@2 - 3", "d3-selection@3": + "integrity" "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + "resolved" "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz" + "version" "3.0.0" + +"d3-shape@^1.2.0": + "integrity" "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==" + "resolved" "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz" + "version" "1.3.7" + dependencies: + "d3-path" "1" + +"d3-shape@3": + "integrity" "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==" + "resolved" "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz" + "version" "3.2.0" + dependencies: + "d3-path" "^3.1.0" + +"d3-time-format@2 - 4", "d3-time-format@4": + "integrity" "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==" + "resolved" "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "d3-time" "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", "d3-time@3": + "integrity" "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==" + "resolved" "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "d3-array" "2 - 3" + +"d3-timer@1 - 3", "d3-timer@3": + "integrity" "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + "resolved" "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz" + "version" "3.0.1" + +"d3-transition@2 - 3", "d3-transition@3": + "integrity" "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==" + "resolved" "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "d3-color" "1 - 3" + "d3-dispatch" "1 - 3" + "d3-ease" "1 - 3" + "d3-interpolate" "1 - 3" + "d3-timer" "1 - 3" + +"d3-zoom@3": + "integrity" "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==" + "resolved" "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "d3-dispatch" "1 - 3" + "d3-drag" "2 - 3" + "d3-interpolate" "1 - 3" + "d3-selection" "2 - 3" + "d3-transition" "2 - 3" + +"d3@^7.4.0", "d3@^7.8.2": + "integrity" "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==" + "resolved" "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz" + "version" "7.8.5" + dependencies: + "d3-array" "3" + "d3-axis" "3" + "d3-brush" "3" + "d3-chord" "3" + "d3-color" "3" + "d3-contour" "4" + "d3-delaunay" "6" + "d3-dispatch" "3" + "d3-drag" "3" + "d3-dsv" "3" + "d3-ease" "3" + "d3-fetch" "3" + "d3-force" "3" + "d3-format" "3" + "d3-geo" "3" + "d3-hierarchy" "3" + "d3-interpolate" "3" + "d3-path" "3" + "d3-polygon" "3" + "d3-quadtree" "3" + "d3-random" "3" + "d3-scale" "4" + "d3-scale-chromatic" "3" + "d3-selection" "3" + "d3-shape" "3" + "d3-time" "3" + "d3-time-format" "4" + "d3-timer" "3" + "d3-transition" "3" + "d3-zoom" "3" + +"dagre-d3-es@7.0.10": + "integrity" "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==" + "resolved" "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz" + "version" "7.0.10" + dependencies: + "d3" "^7.8.2" + "lodash-es" "^4.17.21" + +"data-uri-to-buffer@^4.0.0": + "integrity" "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" + "resolved" "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz" + "version" "4.0.1" + +"dayjs@^1.11.7": + "integrity" "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + "resolved" "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz" + "version" "1.11.9" + +"debug@^4.0.0", "debug@^4.1.0", "debug@^4.1.1", "debug@^4.3.2", "debug@^4.3.4": + "integrity" "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==" + "resolved" "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + "version" "4.3.4" + dependencies: + "ms" "2.1.2" + +"decamelize-keys@^1.1.0": + "integrity" "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==" + "resolved" "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz" + "version" "1.1.1" + dependencies: + "decamelize" "^1.1.0" + "map-obj" "^1.0.0" + +"decamelize@^1.1.0", "decamelize@^1.2.0": + "integrity" "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + "resolved" "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" + "version" "1.2.0" + +"decode-named-character-reference@^1.0.0": + "integrity" "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==" + "resolved" "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "character-entities" "^2.0.0" + +"decompress-response@^6.0.0": + "integrity" "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==" + "resolved" "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" + "version" "6.0.0" + dependencies: + "mimic-response" "^3.1.0" + +"decompress-tar@^4.0.0", "decompress-tar@^4.1.0", "decompress-tar@^4.1.1": + "integrity" "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==" + "resolved" "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz" + "version" "4.1.1" + dependencies: + "file-type" "^5.2.0" + "is-stream" "^1.1.0" + "tar-stream" "^1.5.2" + +"decompress-tarbz2@^4.0.0": + "integrity" "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==" + "resolved" "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz" + "version" "4.1.1" + dependencies: + "decompress-tar" "^4.1.0" + "file-type" "^6.1.0" + "is-stream" "^1.1.0" + "seek-bzip" "^1.0.5" + "unbzip2-stream" "^1.0.9" + +"decompress-targz@^4.0.0": + "integrity" "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==" + "resolved" "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz" + "version" "4.1.1" + dependencies: + "decompress-tar" "^4.1.1" + "file-type" "^5.2.0" + "is-stream" "^1.1.0" + +"decompress-unzip@^4.0.1": + "integrity" "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==" + "resolved" "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "file-type" "^3.8.0" + "get-stream" "^2.2.0" + "pify" "^2.3.0" + "yauzl" "^2.4.2" + +"decompress@4.2.x": + "integrity" "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==" + "resolved" "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz" + "version" "4.2.1" + dependencies: + "decompress-tar" "^4.0.0" + "decompress-tarbz2" "^4.0.0" + "decompress-targz" "^4.0.0" + "decompress-unzip" "^4.0.1" + "graceful-fs" "^4.1.10" + "make-dir" "^1.0.0" + "pify" "^2.3.0" + "strip-dirs" "^2.0.0" + +"deep-is@^0.1.3": + "integrity" "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "resolved" "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + "version" "0.1.4" + +"defer-to-connect@^2.0.1": + "integrity" "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" + "resolved" "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz" + "version" "2.0.1" + +"del@7.0.x": + "integrity" "sha512-tQbV/4u5WVB8HMJr08pgw0b6nG4RGt/tj+7Numvq+zqcvUFeMaIWWOUFltiU+6go8BSO2/ogsB4EasDaj0y68Q==" + "resolved" "https://registry.npmjs.org/del/-/del-7.0.0.tgz" + "version" "7.0.0" + dependencies: + "globby" "^13.1.2" + "graceful-fs" "^4.2.10" + "is-glob" "^4.0.3" + "is-path-cwd" "^3.0.0" + "is-path-inside" "^4.0.0" + "p-map" "^5.5.0" + "rimraf" "^3.0.2" + "slash" "^4.0.0" + +"delaunator@5": + "integrity" "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==" + "resolved" "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "robust-predicates" "^3.0.0" + +"delegate@^3.1.2": + "integrity" "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + "resolved" "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz" + "version" "3.2.0" + +"dependency-graph@^0.11.0": + "integrity" "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==" + "resolved" "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz" + "version" "0.11.0" + +"dequal@^2.0.0": + "integrity" "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" + "resolved" "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" + "version" "2.0.3" + +"diff@^5.0.0": + "integrity" "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==" + "resolved" "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz" + "version" "5.1.0" + +"dir-glob@^3.0.1": + "integrity" "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==" + "resolved" "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "path-type" "^4.0.0" + +"doctrine@^3.0.0": + "integrity" "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==" + "resolved" "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "esutils" "^2.0.2" + +"dompurify@^3.0.5": + "integrity" "sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A==" + "resolved" "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz" + "version" "3.0.5" + +"electron-to-chromium@^1.4.477": + "integrity" "sha512-9zn9/2lbMGY/mFhoUymD9ODYdLY3zjUW/IW9ihU/sJVeIlD70m2aAb86S35aRGF+iwqLuQP25epruayZjKNjBw==" + "resolved" "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.486.tgz" + "version" "1.4.486" + +"elkjs@^0.9.0": + "integrity" "sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw==" + "resolved" "https://registry.npmjs.org/elkjs/-/elkjs-0.9.2.tgz" + "version" "0.9.2" + +"emoji-regex@^8.0.0": + "integrity" "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "resolved" "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + "version" "8.0.0" + +"end-of-stream@^1.0.0", "end-of-stream@^1.1.0": + "integrity" "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==" + "resolved" "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" + "version" "1.4.4" + dependencies: + "once" "^1.4.0" + +"entities@~2.1.0": + "integrity" "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" + "resolved" "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz" + "version" "2.1.0" + +"error-ex@^1.3.1": + "integrity" "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==" + "resolved" "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" + "version" "1.3.2" + dependencies: + "is-arrayish" "^0.2.1" + +"escalade@^3.1.1": + "integrity" "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + "resolved" "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + "version" "3.1.1" + +"escape-string-regexp@^1.0.5": + "integrity" "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + "resolved" "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + "version" "1.0.5" + +"escape-string-regexp@^4.0.0": + "integrity" "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + "resolved" "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + "version" "4.0.0" + +"escape-string-regexp@5.0.0": + "integrity" "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==" + "resolved" "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz" + "version" "5.0.0" + +"eslint-scope@^7.2.2": + "integrity" "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==" + "resolved" "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz" + "version" "7.2.2" + dependencies: + "esrecurse" "^4.3.0" + "estraverse" "^5.2.0" + +"eslint-visitor-keys@^3.3.0", "eslint-visitor-keys@^3.4.1", "eslint-visitor-keys@^3.4.2": + "integrity" "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==" + "resolved" "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz" + "version" "3.4.2" + +"eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.19": + "integrity" "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==" + "resolved" "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz" + "version" "8.46.0" + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.1" + "@eslint/js" "^8.46.0" + "@humanwhocodes/config-array" "^0.11.10" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "ajv" "^6.12.4" + "chalk" "^4.0.0" + "cross-spawn" "^7.0.2" + "debug" "^4.3.2" + "doctrine" "^3.0.0" + "escape-string-regexp" "^4.0.0" + "eslint-scope" "^7.2.2" + "eslint-visitor-keys" "^3.4.2" + "espree" "^9.6.1" + "esquery" "^1.4.2" + "esutils" "^2.0.2" + "fast-deep-equal" "^3.1.3" + "file-entry-cache" "^6.0.1" + "find-up" "^5.0.0" + "glob-parent" "^6.0.2" + "globals" "^13.19.0" + "graphemer" "^1.4.0" + "ignore" "^5.2.0" + "imurmurhash" "^0.1.4" + "is-glob" "^4.0.0" + "is-path-inside" "^3.0.3" + "js-yaml" "^4.1.0" + "json-stable-stringify-without-jsonify" "^1.0.1" + "levn" "^0.4.1" + "lodash.merge" "^4.6.2" + "minimatch" "^3.1.2" + "natural-compare" "^1.4.0" + "optionator" "^0.9.3" + "strip-ansi" "^6.0.1" + "text-table" "^0.2.0" + +"espree@^9.6.0", "espree@^9.6.1": + "integrity" "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==" + "resolved" "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" + "version" "9.6.1" + dependencies: + "acorn" "^8.9.0" + "acorn-jsx" "^5.3.2" + "eslint-visitor-keys" "^3.4.1" + +"esquery@^1.4.2": + "integrity" "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==" + "resolved" "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz" + "version" "1.5.0" + dependencies: + "estraverse" "^5.1.0" + +"esrecurse@^4.3.0": + "integrity" "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==" + "resolved" "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + "version" "4.3.0" + dependencies: + "estraverse" "^5.2.0" + +"estraverse@^5.1.0", "estraverse@^5.2.0": + "integrity" "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + "resolved" "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + "version" "5.3.0" + +"esutils@^2.0.2": + "integrity" "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + "resolved" "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + "version" "2.0.3" + +"exec-bin@^1.0.0": + "integrity" "sha512-p8f8h8b6op2nR7U5rsd+zACUMfsfB+jW8HNIBD2njOQ/gF2WvBfQRo/OU6Q6f/b34WLAyePZcwMJyrDdEjB/fw==" + "resolved" "https://registry.npmjs.org/exec-bin/-/exec-bin-1.0.0.tgz" + "version" "1.0.0" + +"fast-deep-equal@^3.1.1", "fast-deep-equal@^3.1.3": + "integrity" "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "resolved" "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + "version" "3.1.3" + +"fast-glob@^3.2.12", "fast-glob@^3.2.7", "fast-glob@^3.2.9", "fast-glob@^3.3.0": + "integrity" "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==" + "resolved" "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz" + "version" "3.3.1" + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + "glob-parent" "^5.1.2" + "merge2" "^1.3.0" + "micromatch" "^4.0.4" + +"fast-json-stable-stringify@^2.0.0": + "integrity" "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "resolved" "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + "version" "2.1.0" + +"fast-levenshtein@^2.0.6": + "integrity" "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "resolved" "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + "version" "2.0.6" + +"fastest-levenshtein@^1.0.16": + "integrity" "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==" + "resolved" "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz" + "version" "1.0.16" + +"fastq@^1.6.0": + "integrity" "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==" + "resolved" "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz" + "version" "1.15.0" + dependencies: + "reusify" "^1.0.4" + +"fd-slicer@~1.1.0": + "integrity" "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==" + "resolved" "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "pend" "~1.2.0" + +"fetch-blob@^3.1.2", "fetch-blob@^3.1.4": + "integrity" "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==" + "resolved" "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz" + "version" "3.2.0" + dependencies: + "node-domexception" "^1.0.0" + "web-streams-polyfill" "^3.0.3" + +"file-entry-cache@^6.0.1": + "integrity" "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==" + "resolved" "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" + "version" "6.0.1" + dependencies: + "flat-cache" "^3.0.4" + +"file-type@^3.8.0": + "integrity" "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==" + "resolved" "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz" + "version" "3.9.0" + +"file-type@^5.2.0": + "integrity" "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==" + "resolved" "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz" + "version" "5.2.0" + +"file-type@^6.1.0": + "integrity" "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==" + "resolved" "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz" + "version" "6.2.0" + +"fill-range@^7.0.1": + "integrity" "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==" + "resolved" "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" + "version" "7.0.1" + dependencies: + "to-regex-range" "^5.0.1" + +"find-up@^4.1.0": + "integrity" "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==" + "resolved" "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "locate-path" "^5.0.0" + "path-exists" "^4.0.0" + +"find-up@^5.0.0": + "integrity" "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==" + "resolved" "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "locate-path" "^6.0.0" + "path-exists" "^4.0.0" + +"flat-cache@^3.0.4": + "integrity" "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==" + "resolved" "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" + "version" "3.0.4" + dependencies: + "flatted" "^3.1.0" + "rimraf" "^3.0.2" + +"flatted@^3.1.0": + "integrity" "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + "resolved" "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz" + "version" "3.2.7" + +"flexsearch@^0.7.21": + "integrity" "sha512-XGozTsMPYkm+6b5QL3Z9wQcJjNYxp0CYn3U1gO7dwD6PAqU1SVWZxI9CCg3z+ml3YfqdPnrBehaBrnH2AGKbNA==" + "resolved" "https://registry.npmjs.org/flexsearch/-/flexsearch-0.7.31.tgz" + "version" "0.7.31" + +"form-data-encoder@^2.1.0": + "integrity" "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==" + "resolved" "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz" + "version" "2.1.4" + +"formdata-polyfill@^4.0.10": + "integrity" "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==" + "resolved" "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz" + "version" "4.0.10" + dependencies: + "fetch-blob" "^3.1.2" + +"fraction.js@^4.2.0": + "integrity" "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==" + "resolved" "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" + "version" "4.2.0" + +"fs-constants@^1.0.0": + "integrity" "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + "resolved" "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" + "version" "1.0.0" + +"fs-extra@^11.0.0": + "integrity" "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==" + "resolved" "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz" + "version" "11.1.1" + dependencies: + "graceful-fs" "^4.2.0" + "jsonfile" "^6.0.1" + "universalify" "^2.0.0" + +"fs-readdir-recursive@^1.1.0": + "integrity" "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==" + "resolved" "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz" + "version" "1.1.0" + +"fs.realpath@^1.0.0": + "integrity" "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + "version" "1.0.0" + +"fsevents@~2.3.2": + "integrity" "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==" + "resolved" "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + "version" "2.3.2" + +"function-bind@^1.1.1": + "integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" + "version" "1.1.1" + +"gensync@^1.0.0-beta.2": + "integrity" "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + "resolved" "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + "version" "1.0.0-beta.2" + +"get-caller-file@^2.0.1", "get-caller-file@^2.0.5": + "integrity" "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + "resolved" "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + "version" "2.0.5" + +"get-stdin@^9.0.0": + "integrity" "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==" + "resolved" "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz" + "version" "9.0.0" + +"get-stream@^2.2.0": + "integrity" "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==" + "resolved" "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz" + "version" "2.3.1" + dependencies: + "object-assign" "^4.0.1" + "pinkie-promise" "^2.0.0" + +"get-stream@^5.1.0": + "integrity" "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==" + "resolved" "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz" + "version" "5.2.0" + dependencies: + "pump" "^3.0.0" + +"get-stream@^6.0.1": + "integrity" "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + "resolved" "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" + "version" "6.0.1" + +"glob-all@^3.1.0": + "integrity" "sha512-Y+ESjdI7ZgMwfzanHZYQ87C59jOO0i+Hd+QYtVt9PhLi6d8wlOpzQnfBxWUlaTuAoR3TkybLqqbIoWveU4Ji7Q==" + "resolved" "https://registry.npmjs.org/glob-all/-/glob-all-3.3.1.tgz" + "version" "3.3.1" + dependencies: + "glob" "^7.2.3" + "yargs" "^15.3.1" + +"glob-parent@^5.1.2", "glob-parent@~5.1.2": + "integrity" "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==" + "resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + "version" "5.1.2" + dependencies: + "is-glob" "^4.0.1" + +"glob-parent@^6.0.2": + "integrity" "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==" + "resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + "version" "6.0.2" + dependencies: + "is-glob" "^4.0.3" + +"glob@^7.0.0", "glob@^7.1.3", "glob@^7.1.7", "glob@^7.2.0", "glob@^7.2.3": + "integrity" "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==" + "resolved" "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + "version" "7.2.3" + dependencies: + "fs.realpath" "^1.0.0" + "inflight" "^1.0.4" + "inherits" "2" + "minimatch" "^3.1.1" + "once" "^1.3.0" + "path-is-absolute" "^1.0.0" + +"global-modules@^2.0.0": + "integrity" "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==" + "resolved" "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "global-prefix" "^3.0.0" + +"global-prefix@^3.0.0": + "integrity" "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==" + "resolved" "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "ini" "^1.3.5" + "kind-of" "^6.0.2" + "which" "^1.3.1" + +"globals@^11.1.0": + "integrity" "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + "resolved" "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" + "version" "11.12.0" + +"globals@^13.19.0": + "integrity" "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==" + "resolved" "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz" + "version" "13.20.0" + dependencies: + "type-fest" "^0.20.2" + +"globby@^11.1.0": + "integrity" "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==" + "resolved" "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" + "version" "11.1.0" + dependencies: + "array-union" "^2.1.0" + "dir-glob" "^3.0.1" + "fast-glob" "^3.2.9" + "ignore" "^5.2.0" + "merge2" "^1.4.1" + "slash" "^3.0.0" + +"globby@^13.0.0", "globby@^13.1.2": + "integrity" "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==" + "resolved" "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz" + "version" "13.2.2" + dependencies: + "dir-glob" "^3.0.1" + "fast-glob" "^3.3.0" + "ignore" "^5.2.4" + "merge2" "^1.4.1" + "slash" "^4.0.0" + +"globby@12.1.0": + "integrity" "sha512-YULDaNwsoUZkRy9TWSY/M7Obh0abamTKoKzTfOI3uU+hfpX2FZqOq8LFDxsjYheF1RH7ITdArgbQnsNBFgcdBA==" + "resolved" "https://registry.npmjs.org/globby/-/globby-12.1.0.tgz" + "version" "12.1.0" + dependencies: + "array-union" "^3.0.1" + "dir-glob" "^3.0.1" + "fast-glob" "^3.2.7" + "ignore" "^5.1.9" + "merge2" "^1.4.1" + "slash" "^4.0.0" + +"globjoin@^0.1.4": + "integrity" "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==" + "resolved" "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz" + "version" "0.1.4" + +"gonzales-pe@^4.2.4": + "integrity" "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==" + "resolved" "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz" + "version" "4.3.0" + dependencies: + "minimist" "^1.2.5" + +"good-listener@^1.2.2": + "integrity" "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==" + "resolved" "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz" + "version" "1.2.2" + dependencies: + "delegate" "^3.1.2" + +"got@12.4.x": + "integrity" "sha512-Sz1ojLt4zGNkcftIyJKnulZT/yEDvifhUjccHA8QzOuTgPs/+njXYNMFE3jR4/2OODQSSbH8SdnoLCkbh41ieA==" + "resolved" "https://registry.npmjs.org/got/-/got-12.4.1.tgz" + "version" "12.4.1" + dependencies: + "@sindresorhus/is" "^5.2.0" + "@szmarczak/http-timer" "^5.0.1" + "@types/cacheable-request" "^6.0.2" + "cacheable-lookup" "^6.0.4" + "cacheable-request" "^7.0.2" + "decompress-response" "^6.0.0" + "form-data-encoder" "^2.1.0" + "get-stream" "^6.0.1" + "http2-wrapper" "^2.1.10" + "lowercase-keys" "^3.0.0" + "p-cancelable" "^3.0.0" + "responselike" "^3.0.0" + +"graceful-fs@^4.1.10", "graceful-fs@^4.1.6", "graceful-fs@^4.2.0", "graceful-fs@^4.2.10": + "integrity" "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "resolved" "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + "version" "4.2.11" + +"graphemer@^1.4.0": + "integrity" "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + "resolved" "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" + "version" "1.4.0" + +"handlebars@^4.7.7": + "integrity" "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==" + "resolved" "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz" + "version" "4.7.8" + dependencies: + "minimist" "^1.2.5" + "neo-async" "^2.6.2" + "source-map" "^0.6.1" + "wordwrap" "^1.0.0" + optionalDependencies: + "uglify-js" "^3.1.4" + +"hard-rejection@^2.1.0": + "integrity" "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==" + "resolved" "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz" + "version" "2.1.0" + +"has-flag@^3.0.0": + "integrity" "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + "resolved" "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + "version" "3.0.0" + +"has-flag@^4.0.0": + "integrity" "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "resolved" "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + "version" "4.0.0" + +"has@^1.0.3": + "integrity" "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==" + "resolved" "https://registry.npmjs.org/has/-/has-1.0.3.tgz" + "version" "1.0.3" + dependencies: + "function-bind" "^1.1.1" + +"heap@^0.2.6": + "integrity" "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==" + "resolved" "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz" + "version" "0.2.7" + +"highlight.js@^11.5": + "integrity" "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==" + "resolved" "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz" + "version" "11.8.0" + +"hosted-git-info@^2.1.4": + "integrity" "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + "resolved" "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" + "version" "2.8.9" + +"hosted-git-info@^4.0.1": + "integrity" "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==" + "resolved" "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "lru-cache" "^6.0.0" + +"hpagent@1.0.x": + "integrity" "sha512-SCleE2Uc1bM752ymxg8QXYGW0TWtAV4ZW3TqH1aOnyi6T6YW2xadCcclm5qeVjvMvfQ2RKNtZxO7uVb9CTPt1A==" + "resolved" "https://registry.npmjs.org/hpagent/-/hpagent-1.0.0.tgz" + "version" "1.0.0" + +"html-tags@^3.2.0": + "integrity" "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==" + "resolved" "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz" + "version" "3.3.1" + +"http-cache-semantics@^4.0.0": + "integrity" "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + "resolved" "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz" + "version" "4.1.1" + +"http2-wrapper@^2.1.10": + "integrity" "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==" + "resolved" "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz" + "version" "2.2.0" + dependencies: + "quick-lru" "^5.1.1" + "resolve-alpn" "^1.2.0" + +"hugo-installer@>=4.0.1": + "integrity" "sha512-pkp1RO7+ekQ0vw1aqgBMK+dD2dqioIWVbwWKsJsKLOpzfFc78gK68Cweoi/g+CftoiMFO7cyGx/2MgkHCMqaLQ==" + "resolved" "https://registry.npmjs.org/hugo-installer/-/hugo-installer-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "decompress" "4.2.x" + "del" "7.0.x" + "got" "12.4.x" + "hpagent" "1.0.x" + "object-path" "0.11.x" + "semver" "7.3.x" + "yargs" "17.5.x" + +"iconv-lite@0.6": + "integrity" "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==" + "resolved" "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + "version" "0.6.3" + dependencies: + "safer-buffer" ">= 2.1.2 < 3.0.0" + +"ieee754@^1.1.13": + "integrity" "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + "resolved" "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + "version" "1.2.1" + +"ignore@^5.1.9", "ignore@^5.2.0", "ignore@^5.2.1", "ignore@^5.2.4": + "integrity" "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" + "resolved" "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" + "version" "5.2.4" + +"import-fresh@^3.2.1": + "integrity" "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==" + "resolved" "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + "version" "3.3.0" + dependencies: + "parent-module" "^1.0.0" + "resolve-from" "^4.0.0" + +"import-lazy@^4.0.0": + "integrity" "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==" + "resolved" "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz" + "version" "4.0.0" + +"imurmurhash@^0.1.4": + "integrity" "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + "resolved" "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + "version" "0.1.4" + +"indent-string@^4.0.0": + "integrity" "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + "resolved" "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" + "version" "4.0.0" + +"indent-string@^5.0.0": + "integrity" "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==" + "resolved" "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz" + "version" "5.0.0" + +"inflight@^1.0.4": + "integrity" "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==" + "resolved" "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + "version" "1.0.6" + dependencies: + "once" "^1.3.0" + "wrappy" "1" + +"inherits@~2.0.3", "inherits@2": + "integrity" "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "resolved" "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + "version" "2.0.4" + +"ini@^1.3.5": + "integrity" "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "resolved" "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + "version" "1.3.8" + +"instant.page@^5.1": + "integrity" "sha512-DUSwWyoHFOQnmEwJtg9vzDx8Ef8uNNvTxTmHjd0vN9/XEIb5EQkm/itpZMypoH3dJLJvtkrD97WOCKuMqDdMHQ==" + "resolved" "https://registry.npmjs.org/instant.page/-/instant.page-5.2.0.tgz" + "version" "5.2.0" + +"internmap@^1.0.0": + "integrity" "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" + "resolved" "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz" + "version" "1.0.1" + +"internmap@1 - 2": + "integrity" "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + "resolved" "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz" + "version" "2.0.3" + +"interpret@^1.0.0": + "integrity" "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" + "resolved" "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz" + "version" "1.4.0" + +"invariant@2.2.2": + "integrity" "sha512-FUiAFCOgp7bBzHfa/fK+Uc/vqywvdN9Wg3CiTprLcE630mrhxjDS5MlBkHzeI6+bC/6bq9VX/hxBt05fPAT5WA==" + "resolved" "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz" + "version" "2.2.2" + dependencies: + "loose-envify" "^1.0.0" + +"is-arrayish@^0.2.1": + "integrity" "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "resolved" "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + "version" "0.2.1" + +"is-binary-path@~2.1.0": + "integrity" "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==" + "resolved" "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + "version" "2.1.0" + dependencies: + "binary-extensions" "^2.0.0" + +"is-core-module@^2.13.0", "is-core-module@^2.5.0": + "integrity" "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==" + "resolved" "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz" + "version" "2.13.0" + dependencies: + "has" "^1.0.3" + +"is-extglob@^2.1.1": + "integrity" "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + "resolved" "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + "version" "2.1.1" + +"is-fullwidth-code-point@^3.0.0": + "integrity" "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "resolved" "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + "version" "3.0.0" + +"is-glob@^4.0.0", "is-glob@^4.0.1", "is-glob@^4.0.3", "is-glob@~4.0.1": + "integrity" "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==" + "resolved" "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + "version" "4.0.3" + dependencies: + "is-extglob" "^2.1.1" + +"is-natural-number@^4.0.1": + "integrity" "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==" + "resolved" "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz" + "version" "4.0.1" + +"is-number@^7.0.0": + "integrity" "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "resolved" "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + "version" "7.0.0" + +"is-path-cwd@^3.0.0": + "integrity" "sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==" + "resolved" "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz" + "version" "3.0.0" + +"is-path-inside@^3.0.3": + "integrity" "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + "resolved" "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" + "version" "3.0.3" + +"is-path-inside@^4.0.0": + "integrity" "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==" + "resolved" "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz" + "version" "4.0.0" + +"is-plain-obj@^1.1.0": + "integrity" "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==" + "resolved" "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" + "version" "1.1.0" + +"is-plain-object@^5.0.0": + "integrity" "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + "resolved" "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz" + "version" "5.0.0" + +"is-stream@^1.1.0": + "integrity" "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==" + "resolved" "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" + "version" "1.1.0" + +"isarray@~1.0.0": + "integrity" "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "resolved" "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + "version" "1.0.0" + +"isexe@^2.0.0": + "integrity" "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "resolved" "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + "version" "2.0.0" + +"js-tokens@^3.0.0 || ^4.0.0", "js-tokens@^4.0.0": + "integrity" "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "resolved" "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + "version" "4.0.0" + +"js-yaml@^4.1.0": + "integrity" "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==" + "resolved" "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "argparse" "^2.0.1" + +"jsesc@^2.5.1": + "integrity" "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + "resolved" "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" + "version" "2.5.2" + +"jsesc@~0.5.0": + "integrity" "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" + "resolved" "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" + "version" "0.5.0" + +"json-buffer@3.0.1": + "integrity" "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "resolved" "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" + "version" "3.0.1" + +"json-parse-even-better-errors@^2.3.0": + "integrity" "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "resolved" "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + "version" "2.3.1" + +"json-schema-traverse@^0.4.1": + "integrity" "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "resolved" "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + "version" "0.4.1" + +"json-schema-traverse@^1.0.0": + "integrity" "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "resolved" "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" + "version" "1.0.0" + +"json-stable-stringify-without-jsonify@^1.0.1": + "integrity" "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "resolved" "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + "version" "1.0.1" + +"json5@^2.2.2": + "integrity" "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + "resolved" "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + "version" "2.2.3" + +"jsonfile@^6.0.1": + "integrity" "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==" + "resolved" "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" + "version" "6.1.0" + dependencies: + "universalify" "^2.0.0" + optionalDependencies: + "graceful-fs" "^4.1.6" + +"katex@^0.16", "katex@^0.16.9": + "integrity" "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==" + "resolved" "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz" + "version" "0.16.10" + dependencies: + "commander" "^8.3.0" + +"keyv@^4.0.0": + "integrity" "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==" + "resolved" "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz" + "version" "4.5.3" + dependencies: + "json-buffer" "3.0.1" + +"khroma@^2.0.0": + "integrity" "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==" + "resolved" "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz" + "version" "2.0.0" + +"kind-of@^6.0.2", "kind-of@^6.0.3": + "integrity" "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + "resolved" "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" + "version" "6.0.3" + +"kleur@^4.0.3": + "integrity" "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" + "resolved" "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz" + "version" "4.1.5" + +"known-css-properties@^0.26.0": + "integrity" "sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg==" + "resolved" "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.26.0.tgz" + "version" "0.26.0" + +"layout-base@^1.0.0": + "integrity" "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==" + "resolved" "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz" + "version" "1.0.2" + +"lazysizes@^5.3": + "integrity" "sha512-22UzWP+Vedi/sMeOr8O7FWimRVtiNJV2HCa+V8+peZOw6QbswN9k58VUhd7i6iK5bw5QkYrF01LJbeJe0PV8jg==" + "resolved" "https://registry.npmjs.org/lazysizes/-/lazysizes-5.3.2.tgz" + "version" "5.3.2" + +"levn@^0.4.1": + "integrity" "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==" + "resolved" "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + "version" "0.4.1" + dependencies: + "prelude-ls" "^1.2.1" + "type-check" "~0.4.0" + +"lilconfig@^2.0.5": + "integrity" "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==" + "resolved" "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz" + "version" "2.1.0" + +"lines-and-columns@^1.1.6": + "integrity" "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "resolved" "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" + "version" "1.2.4" + +"linkify-it@^3.0.1": + "integrity" "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==" + "resolved" "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz" + "version" "3.0.3" + dependencies: + "uc.micro" "^1.0.1" + +"locate-path@^5.0.0": + "integrity" "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==" + "resolved" "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "p-locate" "^4.1.0" + +"locate-path@^6.0.0": + "integrity" "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==" + "resolved" "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + "version" "6.0.0" + dependencies: + "p-locate" "^5.0.0" + +"lodash-es@^4.17.21": + "integrity" "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + "resolved" "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" + "version" "4.17.21" + +"lodash.debounce@^4.0.8": + "integrity" "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "resolved" "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" + "version" "4.0.8" + +"lodash.merge@^4.6.2": + "integrity" "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "resolved" "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + "version" "4.6.2" + +"lodash.truncate@^4.4.2": + "integrity" "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==" + "resolved" "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz" + "version" "4.4.2" + +"lodash@^4.17.21", "lodash@^4.17.4": + "integrity" "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "resolved" "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + "version" "4.17.21" + +"loose-envify@^1.0.0": + "integrity" "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==" + "resolved" "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" + "version" "1.4.0" + dependencies: + "js-tokens" "^3.0.0 || ^4.0.0" + +"lowercase-keys@^2.0.0": + "integrity" "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + "resolved" "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz" + "version" "2.0.0" + +"lowercase-keys@^3.0.0": + "integrity" "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==" + "resolved" "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz" + "version" "3.0.0" + +"lru-cache@^5.1.1": + "integrity" "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==" + "resolved" "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" + "version" "5.1.1" + dependencies: + "yallist" "^3.0.2" + +"lru-cache@^6.0.0": + "integrity" "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==" + "resolved" "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + "version" "6.0.0" + dependencies: + "yallist" "^4.0.0" + +"make-dir@^1.0.0": + "integrity" "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==" + "resolved" "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz" + "version" "1.3.0" + dependencies: + "pify" "^3.0.0" + +"make-dir@^2.1.0": + "integrity" "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==" + "resolved" "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz" + "version" "2.1.0" + dependencies: + "pify" "^4.0.1" + "semver" "^5.6.0" + +"map-obj@^1.0.0": + "integrity" "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==" + "resolved" "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz" + "version" "1.0.1" + +"map-obj@^4.0.0": + "integrity" "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==" + "resolved" "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz" + "version" "4.3.0" + +"markdown-it@12.3.2": + "integrity" "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==" + "resolved" "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz" + "version" "12.3.2" + dependencies: + "argparse" "^2.0.1" + "entities" "~2.1.0" + "linkify-it" "^3.0.1" + "mdurl" "^1.0.1" + "uc.micro" "^1.0.5" + +"markdownlint-cli2-formatter-default@0.0.3": + "integrity" "sha512-QEAJitT5eqX1SNboOD+SO/LNBpu4P4je8JlR02ug2cLQAqmIhh8IJnSK7AcaHBHhNADqdGydnPpQOpsNcEEqCw==" + "resolved" "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.3.tgz" + "version" "0.0.3" + +"markdownlint-cli2@^0.4.0", "markdownlint-cli2@>=0.0.4": + "integrity" "sha512-EcwP5tAbyzzL3ACI0L16LqbNctmh8wNX56T+aVvIxWyTAkwbYNx2V7IheRkXS3mE7R/pnaApZ/RSXcXuzRVPjg==" + "resolved" "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.4.0.tgz" + "version" "0.4.0" + dependencies: + "globby" "12.1.0" + "markdownlint" "0.25.1" + "markdownlint-cli2-formatter-default" "0.0.3" + "markdownlint-rule-helpers" "0.16.0" + "micromatch" "4.0.4" + "strip-json-comments" "4.0.0" + "yaml" "1.10.2" + +"markdownlint-rule-helpers@0.16.0": + "integrity" "sha512-oEacRUVeTJ5D5hW1UYd2qExYI0oELdYK72k1TKGvIeYJIbqQWAz476NAc7LNixSySUhcNl++d02DvX0ccDk9/w==" + "resolved" "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.16.0.tgz" + "version" "0.16.0" + +"markdownlint@0.25.1": + "integrity" "sha512-AG7UkLzNa1fxiOv5B+owPsPhtM4D6DoODhsJgiaNg1xowXovrYgOnLqAgOOFQpWOlHFVQUzjMY5ypNNTeov92g==" + "resolved" "https://registry.npmjs.org/markdownlint/-/markdownlint-0.25.1.tgz" + "version" "0.25.1" + dependencies: + "markdown-it" "12.3.2" + +"mathml-tag-names@^2.1.3": + "integrity" "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==" + "resolved" "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz" + "version" "2.1.3" + +"mdast-util-from-markdown@^1.3.0": + "integrity" "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==" + "resolved" "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz" + "version" "1.3.1" + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + "decode-named-character-reference" "^1.0.0" + "mdast-util-to-string" "^3.1.0" + "micromark" "^3.0.0" + "micromark-util-decode-numeric-character-reference" "^1.0.0" + "micromark-util-decode-string" "^1.0.0" + "micromark-util-normalize-identifier" "^1.0.0" + "micromark-util-symbol" "^1.0.0" + "micromark-util-types" "^1.0.0" + "unist-util-stringify-position" "^3.0.0" + "uvu" "^0.5.0" + +"mdast-util-to-string@^3.1.0": + "integrity" "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==" + "resolved" "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz" + "version" "3.2.0" + dependencies: + "@types/mdast" "^3.0.0" + +"mdurl@^1.0.1": + "integrity" "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + "resolved" "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz" + "version" "1.0.1" + +"meow@^9.0.0": + "integrity" "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==" + "resolved" "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz" + "version" "9.0.0" + dependencies: + "@types/minimist" "^1.2.0" + "camelcase-keys" "^6.2.2" + "decamelize" "^1.2.0" + "decamelize-keys" "^1.1.0" + "hard-rejection" "^2.1.0" + "minimist-options" "4.1.0" + "normalize-package-data" "^3.0.0" + "read-pkg-up" "^7.0.1" + "redent" "^3.0.0" + "trim-newlines" "^3.0.0" + "type-fest" "^0.18.0" + "yargs-parser" "^20.2.3" + +"merge2@^1.3.0", "merge2@^1.4.1": + "integrity" "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + "resolved" "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + "version" "1.4.1" + +"mermaid@^10.5.0": + "integrity" "sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==" + "resolved" "https://registry.npmjs.org/mermaid/-/mermaid-10.9.0.tgz" + "version" "10.9.0" + dependencies: + "@braintree/sanitize-url" "^6.0.1" + "@types/d3-scale" "^4.0.3" + "@types/d3-scale-chromatic" "^3.0.0" + "cytoscape" "^3.28.1" + "cytoscape-cose-bilkent" "^4.1.0" + "d3" "^7.4.0" + "d3-sankey" "^0.12.3" + "dagre-d3-es" "7.0.10" + "dayjs" "^1.11.7" + "dompurify" "^3.0.5" + "elkjs" "^0.9.0" + "katex" "^0.16.9" + "khroma" "^2.0.0" + "lodash-es" "^4.17.21" + "mdast-util-from-markdown" "^1.3.0" + "non-layered-tidy-tree-layout" "^2.0.2" + "stylis" "^4.1.3" + "ts-dedent" "^2.2.0" + "uuid" "^9.0.0" + "web-worker" "^1.2.0" + +"micromark-core-commonmark@^1.0.1": + "integrity" "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==" + "resolved" "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "decode-named-character-reference" "^1.0.0" + "micromark-factory-destination" "^1.0.0" + "micromark-factory-label" "^1.0.0" + "micromark-factory-space" "^1.0.0" + "micromark-factory-title" "^1.0.0" + "micromark-factory-whitespace" "^1.0.0" + "micromark-util-character" "^1.0.0" + "micromark-util-chunked" "^1.0.0" + "micromark-util-classify-character" "^1.0.0" + "micromark-util-html-tag-name" "^1.0.0" + "micromark-util-normalize-identifier" "^1.0.0" + "micromark-util-resolve-all" "^1.0.0" + "micromark-util-subtokenize" "^1.0.0" + "micromark-util-symbol" "^1.0.0" + "micromark-util-types" "^1.0.1" + "uvu" "^0.5.0" + +"micromark-factory-destination@^1.0.0": + "integrity" "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==" + "resolved" "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "micromark-util-character" "^1.0.0" + "micromark-util-symbol" "^1.0.0" + "micromark-util-types" "^1.0.0" + +"micromark-factory-label@^1.0.0": + "integrity" "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==" + "resolved" "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "micromark-util-character" "^1.0.0" + "micromark-util-symbol" "^1.0.0" + "micromark-util-types" "^1.0.0" + "uvu" "^0.5.0" + +"micromark-factory-space@^1.0.0": + "integrity" "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==" + "resolved" "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "micromark-util-character" "^1.0.0" + "micromark-util-types" "^1.0.0" + +"micromark-factory-title@^1.0.0": + "integrity" "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==" + "resolved" "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "micromark-factory-space" "^1.0.0" + "micromark-util-character" "^1.0.0" + "micromark-util-symbol" "^1.0.0" + "micromark-util-types" "^1.0.0" + +"micromark-factory-whitespace@^1.0.0": + "integrity" "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==" + "resolved" "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "micromark-factory-space" "^1.0.0" + "micromark-util-character" "^1.0.0" + "micromark-util-symbol" "^1.0.0" + "micromark-util-types" "^1.0.0" + +"micromark-util-character@^1.0.0": + "integrity" "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==" + "resolved" "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz" + "version" "1.2.0" + dependencies: + "micromark-util-symbol" "^1.0.0" + "micromark-util-types" "^1.0.0" + +"micromark-util-chunked@^1.0.0": + "integrity" "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==" + "resolved" "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "micromark-util-symbol" "^1.0.0" + +"micromark-util-classify-character@^1.0.0": + "integrity" "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==" + "resolved" "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "micromark-util-character" "^1.0.0" + "micromark-util-symbol" "^1.0.0" + "micromark-util-types" "^1.0.0" + +"micromark-util-combine-extensions@^1.0.0": + "integrity" "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==" + "resolved" "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "micromark-util-chunked" "^1.0.0" + "micromark-util-types" "^1.0.0" + +"micromark-util-decode-numeric-character-reference@^1.0.0": + "integrity" "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==" + "resolved" "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "micromark-util-symbol" "^1.0.0" + +"micromark-util-decode-string@^1.0.0": + "integrity" "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==" + "resolved" "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "decode-named-character-reference" "^1.0.0" + "micromark-util-character" "^1.0.0" + "micromark-util-decode-numeric-character-reference" "^1.0.0" + "micromark-util-symbol" "^1.0.0" + +"micromark-util-encode@^1.0.0": + "integrity" "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==" + "resolved" "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz" + "version" "1.1.0" + +"micromark-util-html-tag-name@^1.0.0": + "integrity" "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==" + "resolved" "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz" + "version" "1.2.0" + +"micromark-util-normalize-identifier@^1.0.0": + "integrity" "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==" + "resolved" "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "micromark-util-symbol" "^1.0.0" + +"micromark-util-resolve-all@^1.0.0": + "integrity" "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==" + "resolved" "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "micromark-util-types" "^1.0.0" + +"micromark-util-sanitize-uri@^1.0.0": + "integrity" "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==" + "resolved" "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz" + "version" "1.2.0" + dependencies: + "micromark-util-character" "^1.0.0" + "micromark-util-encode" "^1.0.0" + "micromark-util-symbol" "^1.0.0" + +"micromark-util-subtokenize@^1.0.0": + "integrity" "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==" + "resolved" "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "micromark-util-chunked" "^1.0.0" + "micromark-util-symbol" "^1.0.0" + "micromark-util-types" "^1.0.0" + "uvu" "^0.5.0" + +"micromark-util-symbol@^1.0.0": + "integrity" "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==" + "resolved" "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz" + "version" "1.1.0" + +"micromark-util-types@^1.0.0", "micromark-util-types@^1.0.1": + "integrity" "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==" + "resolved" "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz" + "version" "1.1.0" + +"micromark@^3.0.0": + "integrity" "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==" + "resolved" "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz" + "version" "3.2.0" + dependencies: + "@types/debug" "^4.0.0" + "debug" "^4.0.0" + "decode-named-character-reference" "^1.0.0" + "micromark-core-commonmark" "^1.0.1" + "micromark-factory-space" "^1.0.0" + "micromark-util-character" "^1.0.0" + "micromark-util-chunked" "^1.0.0" + "micromark-util-combine-extensions" "^1.0.0" + "micromark-util-decode-numeric-character-reference" "^1.0.0" + "micromark-util-encode" "^1.0.0" + "micromark-util-normalize-identifier" "^1.0.0" + "micromark-util-resolve-all" "^1.0.0" + "micromark-util-sanitize-uri" "^1.0.0" + "micromark-util-subtokenize" "^1.0.0" + "micromark-util-symbol" "^1.0.0" + "micromark-util-types" "^1.0.1" + "uvu" "^0.5.0" + +"micromatch@^4.0.4", "micromatch@^4.0.5": + "integrity" "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==" + "resolved" "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" + "version" "4.0.5" + dependencies: + "braces" "^3.0.2" + "picomatch" "^2.3.1" + +"micromatch@4.0.4": + "integrity" "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==" + "resolved" "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz" + "version" "4.0.4" + dependencies: + "braces" "^3.0.1" + "picomatch" "^2.2.3" + +"mimic-response@^1.0.0": + "integrity" "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + "resolved" "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" + "version" "1.0.1" + +"mimic-response@^3.1.0": + "integrity" "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + "resolved" "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" + "version" "3.1.0" + +"min-indent@^1.0.0": + "integrity" "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" + "resolved" "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" + "version" "1.0.1" + +"minimatch@^3.0.5", "minimatch@^3.1.1", "minimatch@^3.1.2": + "integrity" "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==" + "resolved" "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + "version" "3.1.2" + dependencies: + "brace-expansion" "^1.1.7" + +"minimist-options@4.1.0": + "integrity" "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==" + "resolved" "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "arrify" "^1.0.1" + "is-plain-obj" "^1.1.0" + "kind-of" "^6.0.3" + +"minimist@^1.2.3", "minimist@^1.2.5": + "integrity" "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + "resolved" "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" + "version" "1.2.8" + +"mri@^1.1.0": + "integrity" "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" + "resolved" "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz" + "version" "1.2.0" + +"ms@2.1.2": + "integrity" "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "resolved" "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + "version" "2.1.2" + +"nanoid@^3.3.6": + "integrity" "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + "resolved" "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz" + "version" "3.3.6" + +"natural-compare@^1.4.0": + "integrity" "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "resolved" "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + "version" "1.4.0" + +"neo-async@^2.6.2": + "integrity" "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "resolved" "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" + "version" "2.6.2" + +"netlify-plugin-submit-sitemap@^0.4.0": + "integrity" "sha512-5ntDtSKZRHaCDrDXh4sH4V7lNEEsoi01lsmSUuqJ/ikPHf0XEErjsKba8TsM3iaZRYEHI9bQse3BWgguwuwIIQ==" + "resolved" "https://registry.npmjs.org/netlify-plugin-submit-sitemap/-/netlify-plugin-submit-sitemap-0.4.0.tgz" + "version" "0.4.0" + dependencies: + "node-fetch" "^3.2.3" + +"node-domexception@^1.0.0": + "integrity" "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + "resolved" "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" + "version" "1.0.0" + +"node-fetch@^2.6.1": + "integrity" "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==" + "resolved" "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz" + "version" "2.6.12" + dependencies: + "whatwg-url" "^5.0.0" + +"node-fetch@^3.2", "node-fetch@^3.2.3": + "integrity" "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==" + "resolved" "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz" + "version" "3.3.2" + dependencies: + "data-uri-to-buffer" "^4.0.0" + "fetch-blob" "^3.1.4" + "formdata-polyfill" "^4.0.10" + +"node-releases@^2.0.13": + "integrity" "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "resolved" "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz" + "version" "2.0.13" + +"non-layered-tidy-tree-layout@^2.0.2": + "integrity" "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==" + "resolved" "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz" + "version" "2.0.2" + +"normalize-package-data@^2.5.0": + "integrity" "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==" + "resolved" "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz" + "version" "2.5.0" + dependencies: + "hosted-git-info" "^2.1.4" + "resolve" "^1.10.0" + "semver" "2 || 3 || 4 || 5" + "validate-npm-package-license" "^3.0.1" + +"normalize-package-data@^3.0.0": + "integrity" "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==" + "resolved" "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz" + "version" "3.0.3" + dependencies: + "hosted-git-info" "^4.0.1" + "is-core-module" "^2.5.0" + "semver" "^7.3.4" + "validate-npm-package-license" "^3.0.1" + +"normalize-path@^3.0.0", "normalize-path@~3.0.0": + "integrity" "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + "resolved" "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + "version" "3.0.0" + +"normalize-range@^0.1.2": + "integrity" "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" + "resolved" "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" + "version" "0.1.2" + +"normalize-url@^6.0.1": + "integrity" "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + "resolved" "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz" + "version" "6.1.0" + +"object-assign@^4.0.1": + "integrity" "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + "resolved" "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + "version" "4.1.1" + +"object-path@0.11.x": + "integrity" "sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==" + "resolved" "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz" + "version" "0.11.8" + +"once@^1.3.0", "once@^1.3.1", "once@^1.4.0": + "integrity" "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==" + "resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + "version" "1.4.0" + dependencies: + "wrappy" "1" + +"optionator@^0.9.3": + "integrity" "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==" + "resolved" "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz" + "version" "0.9.3" + dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" + "deep-is" "^0.1.3" + "fast-levenshtein" "^2.0.6" + "levn" "^0.4.1" + "prelude-ls" "^1.2.1" + "type-check" "^0.4.0" + +"p-cancelable@^3.0.0": + "integrity" "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==" + "resolved" "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz" + "version" "3.0.0" + +"p-limit@^2.2.0": + "integrity" "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==" + "resolved" "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + "version" "2.3.0" + dependencies: + "p-try" "^2.0.0" + +"p-limit@^3.0.2": + "integrity" "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==" + "resolved" "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "yocto-queue" "^0.1.0" + +"p-locate@^4.1.0": + "integrity" "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==" + "resolved" "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "p-limit" "^2.2.0" + +"p-locate@^5.0.0": + "integrity" "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==" + "resolved" "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "p-limit" "^3.0.2" + +"p-map@^5.5.0": + "integrity" "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==" + "resolved" "https://registry.npmjs.org/p-map/-/p-map-5.5.0.tgz" + "version" "5.5.0" + dependencies: + "aggregate-error" "^4.0.0" + +"p-try@^2.0.0": + "integrity" "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + "resolved" "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" + "version" "2.2.0" + +"parent-module@^1.0.0": + "integrity" "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==" + "resolved" "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "callsites" "^3.0.0" + +"parse-github-url@^1.0.2": + "integrity" "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==" + "resolved" "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz" + "version" "1.0.2" + +"parse-json@^5.0.0": + "integrity" "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==" + "resolved" "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + "version" "5.2.0" + dependencies: + "@babel/code-frame" "^7.0.0" + "error-ex" "^1.3.1" + "json-parse-even-better-errors" "^2.3.0" + "lines-and-columns" "^1.1.6" + +"path-exists@^4.0.0": + "integrity" "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + "resolved" "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + "version" "4.0.0" + +"path-is-absolute@^1.0.0": + "integrity" "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + "resolved" "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + "version" "1.0.1" + +"path-key@^3.1.0": + "integrity" "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + "resolved" "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + "version" "3.1.1" + +"path-parse@^1.0.7": + "integrity" "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "resolved" "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + "version" "1.0.7" + +"path-type@^4.0.0": + "integrity" "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + "resolved" "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + "version" "4.0.0" + +"pend@~1.2.0": + "integrity" "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + "resolved" "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" + "version" "1.2.0" + +"picocolors@^1.0.0": + "integrity" "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "resolved" "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" + "version" "1.0.0" + +"picomatch@^2.0.4", "picomatch@^2.2.1", "picomatch@^2.2.3", "picomatch@^2.3.1": + "integrity" "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + "resolved" "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + "version" "2.3.1" + +"pify@^2.3.0": + "integrity" "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + "resolved" "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" + "version" "2.3.0" + +"pify@^3.0.0": + "integrity" "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==" + "resolved" "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" + "version" "3.0.0" + +"pify@^4.0.1": + "integrity" "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + "resolved" "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" + "version" "4.0.1" + +"pinkie-promise@^2.0.0": + "integrity" "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==" + "resolved" "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "pinkie" "^2.0.0" + +"pinkie@^2.0.0": + "integrity" "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==" + "resolved" "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" + "version" "2.0.4" + +"postcss-cli@^10.0": + "integrity" "sha512-Zu7PLORkE9YwNdvOeOVKPmWghprOtjFQU3srMUGbdz3pHJiFh7yZ4geiZFMkjMfB0mtTFR3h8RemR62rPkbOPA==" + "resolved" "https://registry.npmjs.org/postcss-cli/-/postcss-cli-10.1.0.tgz" + "version" "10.1.0" + dependencies: + "chokidar" "^3.3.0" + "dependency-graph" "^0.11.0" + "fs-extra" "^11.0.0" + "get-stdin" "^9.0.0" + "globby" "^13.0.0" + "picocolors" "^1.0.0" + "postcss-load-config" "^4.0.0" + "postcss-reporter" "^7.0.0" + "pretty-hrtime" "^1.0.3" + "read-cache" "^1.0.0" + "slash" "^5.0.0" + "yargs" "^17.0.0" + +"postcss-load-config@^4.0.0": + "integrity" "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==" + "resolved" "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "lilconfig" "^2.0.5" + "yaml" "^2.1.1" + +"postcss-media-query-parser@^0.2.3": + "integrity" "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==" + "resolved" "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz" + "version" "0.2.3" + +"postcss-reporter@^7.0.0": + "integrity" "sha512-glWg7VZBilooZGOFPhN9msJ3FQs19Hie7l5a/eE6WglzYqVeH3ong3ShFcp9kDWJT1g2Y/wd59cocf9XxBtkWA==" + "resolved" "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.0.5.tgz" + "version" "7.0.5" + dependencies: + "picocolors" "^1.0.0" + "thenby" "^1.3.4" + +"postcss-resolve-nested-selector@^0.1.1": + "integrity" "sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==" + "resolved" "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz" + "version" "0.1.1" + +"postcss-safe-parser@^6.0.0": + "integrity" "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==" + "resolved" "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz" + "version" "6.0.0" + +"postcss-scss@^4.0.2": + "integrity" "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==" + "resolved" "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz" + "version" "4.0.6" + +"postcss-selector-parser@^6.0.10", "postcss-selector-parser@^6.0.11", "postcss-selector-parser@^6.0.6": + "integrity" "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==" + "resolved" "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz" + "version" "6.0.13" + dependencies: + "cssesc" "^3.0.0" + "util-deprecate" "^1.0.2" + +"postcss-value-parser@^4.2.0": + "integrity" "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "resolved" "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" + "version" "4.2.0" + +"postcss@^8.0.0", "postcss@^8.1.0", "postcss@^8.3.3", "postcss@^8.3.5", "postcss@^8.4", "postcss@^8.4.19", "postcss@>=8.0.9": + "integrity" "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==" + "resolved" "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz" + "version" "8.4.27" + dependencies: + "nanoid" "^3.3.6" + "picocolors" "^1.0.0" + "source-map-js" "^1.0.2" + +"prelude-ls@^1.2.1": + "integrity" "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + "resolved" "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + "version" "1.2.1" + +"pretty-hrtime@^1.0.3": + "integrity" "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==" + "resolved" "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz" + "version" "1.0.3" + +"process-nextick-args@~2.0.0": + "integrity" "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "resolved" "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" + "version" "2.0.1" + +"pump@^3.0.0": + "integrity" "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==" + "resolved" "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "end-of-stream" "^1.1.0" + "once" "^1.3.1" + +"punycode@^2.1.0": + "integrity" "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + "resolved" "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" + "version" "2.3.0" + +"purgecss-whitelister@^2.4": + "integrity" "sha512-O0jBUDtY9dU9tUT0vA1FvwFdkKDerxzteYaBV49JCbm+QJLFKMlIsf5Kp5cdbLatHQNjJtV8VB8eXtISoZL2Dg==" + "resolved" "https://registry.npmjs.org/purgecss-whitelister/-/purgecss-whitelister-2.4.0.tgz" + "version" "2.4.0" + dependencies: + "glob-all" "^3.1.0" + "gonzales-pe" "^4.2.4" + "scss-parser" "1.0.3" + +"purgecss@^4.1.3": + "integrity" "sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==" + "resolved" "https://registry.npmjs.org/purgecss/-/purgecss-4.1.3.tgz" + "version" "4.1.3" + dependencies: + "commander" "^8.0.0" + "glob" "^7.1.7" + "postcss" "^8.3.5" + "postcss-selector-parser" "^6.0.6" + +"queue-microtask@^1.2.2": + "integrity" "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + "resolved" "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + "version" "1.2.3" + +"quick-lru@^4.0.1": + "integrity" "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==" + "resolved" "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz" + "version" "4.0.1" + +"quick-lru@^5.1.1": + "integrity" "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + "resolved" "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" + "version" "5.1.1" + +"read-cache@^1.0.0": + "integrity" "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==" + "resolved" "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "pify" "^2.3.0" + +"read-pkg-up@^7.0.1": + "integrity" "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==" + "resolved" "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz" + "version" "7.0.1" + dependencies: + "find-up" "^4.1.0" + "read-pkg" "^5.2.0" + "type-fest" "^0.8.1" + +"read-pkg@^5.2.0": + "integrity" "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==" + "resolved" "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz" + "version" "5.2.0" + dependencies: + "@types/normalize-package-data" "^2.4.0" + "normalize-package-data" "^2.5.0" + "parse-json" "^5.0.0" + "type-fest" "^0.6.0" + +"readable-stream@^2.3.0", "readable-stream@^2.3.5": + "integrity" "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==" + "resolved" "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" + "version" "2.3.8" + dependencies: + "core-util-is" "~1.0.0" + "inherits" "~2.0.3" + "isarray" "~1.0.0" + "process-nextick-args" "~2.0.0" + "safe-buffer" "~5.1.1" + "string_decoder" "~1.1.1" + "util-deprecate" "~1.0.1" + +"readdirp@~3.6.0": + "integrity" "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==" + "resolved" "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + "version" "3.6.0" + dependencies: + "picomatch" "^2.2.1" + +"rechoir@^0.6.2": + "integrity" "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==" + "resolved" "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz" + "version" "0.6.2" + dependencies: + "resolve" "^1.1.6" + +"redent@^3.0.0": + "integrity" "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==" + "resolved" "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "indent-string" "^4.0.0" + "strip-indent" "^3.0.0" + +"regenerate-unicode-properties@^10.1.0": + "integrity" "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==" + "resolved" "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz" + "version" "10.1.0" + dependencies: + "regenerate" "^1.4.2" + +"regenerate@^1.4.2": + "integrity" "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + "resolved" "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" + "version" "1.4.2" + +"regenerator-runtime@^0.14.0": + "integrity" "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz" + "version" "0.14.0" + +"regenerator-transform@^0.15.2": + "integrity" "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==" + "resolved" "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz" + "version" "0.15.2" + dependencies: + "@babel/runtime" "^7.8.4" + +"regexpu-core@^5.3.1": + "integrity" "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==" + "resolved" "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz" + "version" "5.3.2" + dependencies: + "@babel/regjsgen" "^0.8.0" + "regenerate" "^1.4.2" + "regenerate-unicode-properties" "^10.1.0" + "regjsparser" "^0.9.1" + "unicode-match-property-ecmascript" "^2.0.0" + "unicode-match-property-value-ecmascript" "^2.1.0" + +"regjsparser@^0.9.1": + "integrity" "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==" + "resolved" "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz" + "version" "0.9.1" + dependencies: + "jsesc" "~0.5.0" + +"require-directory@^2.1.1": + "integrity" "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + "resolved" "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + "version" "2.1.1" + +"require-from-string@^2.0.2": + "integrity" "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + "resolved" "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + "version" "2.0.2" + +"require-main-filename@^2.0.0": + "integrity" "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + "resolved" "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz" + "version" "2.0.0" + +"resolve-alpn@^1.2.0": + "integrity" "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + "resolved" "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz" + "version" "1.2.1" + +"resolve-from@^4.0.0": + "integrity" "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "resolved" "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + "version" "4.0.0" + +"resolve-from@^5.0.0": + "integrity" "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + "resolved" "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + "version" "5.0.0" + +"resolve@^1.1.6", "resolve@^1.10.0", "resolve@^1.14.2": + "integrity" "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==" + "resolved" "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz" + "version" "1.22.4" + dependencies: + "is-core-module" "^2.13.0" + "path-parse" "^1.0.7" + "supports-preserve-symlinks-flag" "^1.0.0" + +"responselike@^2.0.0": + "integrity" "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==" + "resolved" "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "lowercase-keys" "^2.0.0" + +"responselike@^3.0.0": + "integrity" "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==" + "resolved" "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "lowercase-keys" "^3.0.0" + +"reusify@^1.0.4": + "integrity" "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + "resolved" "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + "version" "1.0.4" + +"rimraf@^3.0.2": + "integrity" "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==" + "resolved" "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + "version" "3.0.2" + dependencies: + "glob" "^7.1.3" + +"robust-predicates@^3.0.0": + "integrity" "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + "resolved" "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz" + "version" "3.0.2" + +"run-parallel@^1.1.9": + "integrity" "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==" + "resolved" "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + "version" "1.2.0" + dependencies: + "queue-microtask" "^1.2.2" + +"rw@1": + "integrity" "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + "resolved" "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz" + "version" "1.3.3" + +"sade@^1.7.3": + "integrity" "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==" + "resolved" "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz" + "version" "1.8.1" + dependencies: + "mri" "^1.1.0" + +"safe-buffer@^5.1.1": + "integrity" "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "resolved" "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + "version" "5.2.1" + +"safe-buffer@~5.1.0": + "integrity" "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "resolved" "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + "version" "5.1.2" + +"safe-buffer@~5.1.1": + "integrity" "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "resolved" "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + "version" "5.1.2" + +"safer-buffer@>= 2.1.2 < 3.0.0": + "integrity" "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "resolved" "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + "version" "2.1.2" + +"scss-parser@1.0.3": + "integrity" "sha512-XQKCfOJERmhn1yoNRUyxv9wgkf4DIv29Jk0m4FiZforeiCmGxrby8K3not7tQ8GK1yvtd9N0OnNimNetJ8V+zQ==" + "resolved" "https://registry.npmjs.org/scss-parser/-/scss-parser-1.0.3.tgz" + "version" "1.0.3" + dependencies: + "invariant" "2.2.2" + "lodash" "^4.17.4" + +"seek-bzip@^1.0.5": + "integrity" "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==" + "resolved" "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz" + "version" "1.0.6" + dependencies: + "commander" "^2.8.1" + +"select@^1.1.2": + "integrity" "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" + "resolved" "https://registry.npmjs.org/select/-/select-1.1.2.tgz" + "version" "1.1.2" + +"semver@^5.6.0": + "integrity" "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" + "resolved" "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" + "version" "5.7.2" + +"semver@^6.3.1": + "integrity" "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "resolved" "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + "version" "6.3.1" + +"semver@^7.3.4": + "integrity" "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==" + "resolved" "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz" + "version" "7.5.4" + dependencies: + "lru-cache" "^6.0.0" + +"semver@^7.3.5": + "integrity" "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==" + "resolved" "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz" + "version" "7.5.4" + dependencies: + "lru-cache" "^6.0.0" + +"semver@2 || 3 || 4 || 5": + "integrity" "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" + "resolved" "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" + "version" "5.7.2" + +"semver@7.3.x": + "integrity" "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==" + "resolved" "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" + "version" "7.3.8" + dependencies: + "lru-cache" "^6.0.0" + +"set-blocking@^2.0.0": + "integrity" "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + "resolved" "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + "version" "2.0.0" + +"shebang-command@^2.0.0": + "integrity" "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==" + "resolved" "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "shebang-regex" "^3.0.0" + +"shebang-regex@^3.0.0": + "integrity" "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + "resolved" "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + "version" "3.0.0" + +"shelljs@^0.8.5": + "integrity" "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==" + "resolved" "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz" + "version" "0.8.5" + dependencies: + "glob" "^7.0.0" + "interpret" "^1.0.0" + "rechoir" "^0.6.2" + +"shx@^0.3.4": + "integrity" "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==" + "resolved" "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz" + "version" "0.3.4" + dependencies: + "minimist" "^1.2.3" + "shelljs" "^0.8.5" + +"signal-exit@^3.0.7": + "integrity" "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "resolved" "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + "version" "3.0.7" + +"slash@^2.0.0": + "integrity" "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" + "resolved" "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz" + "version" "2.0.0" + +"slash@^3.0.0": + "integrity" "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + "resolved" "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + "version" "3.0.0" + +"slash@^4.0.0": + "integrity" "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==" + "resolved" "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" + "version" "4.0.0" + +"slash@^5.0.0": + "integrity" "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==" + "resolved" "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz" + "version" "5.1.0" + +"slice-ansi@^4.0.0": + "integrity" "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==" + "resolved" "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "ansi-styles" "^4.0.0" + "astral-regex" "^2.0.0" + "is-fullwidth-code-point" "^3.0.0" + +"source-map-js@^1.0.2": + "integrity" "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + "resolved" "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" + "version" "1.0.2" + +"source-map@^0.6.1": + "integrity" "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "resolved" "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + "version" "0.6.1" + +"spdx-correct@^3.0.0": + "integrity" "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==" + "resolved" "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz" + "version" "3.2.0" + dependencies: + "spdx-expression-parse" "^3.0.0" + "spdx-license-ids" "^3.0.0" + +"spdx-exceptions@^2.1.0": + "integrity" "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + "resolved" "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz" + "version" "2.3.0" + +"spdx-expression-parse@^3.0.0": + "integrity" "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==" + "resolved" "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "spdx-exceptions" "^2.1.0" + "spdx-license-ids" "^3.0.0" + +"spdx-license-ids@^3.0.0": + "integrity" "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==" + "resolved" "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz" + "version" "3.0.13" + +"string_decoder@~1.1.1": + "integrity" "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==" + "resolved" "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + "version" "1.1.1" + dependencies: + "safe-buffer" "~5.1.0" + +"string-width@^4.1.0", "string-width@^4.2.0", "string-width@^4.2.3": + "integrity" "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==" + "resolved" "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + "version" "4.2.3" + dependencies: + "emoji-regex" "^8.0.0" + "is-fullwidth-code-point" "^3.0.0" + "strip-ansi" "^6.0.1" + +"strip-ansi@^6.0.0", "strip-ansi@^6.0.1": + "integrity" "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==" + "resolved" "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + "version" "6.0.1" + dependencies: + "ansi-regex" "^5.0.1" + +"strip-dirs@^2.0.0": + "integrity" "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==" + "resolved" "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz" + "version" "2.1.0" + dependencies: + "is-natural-number" "^4.0.1" + +"strip-indent@^3.0.0": + "integrity" "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==" + "resolved" "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "min-indent" "^1.0.0" + +"strip-json-comments@^3.1.1": + "integrity" "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + "resolved" "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + "version" "3.1.1" + +"strip-json-comments@4.0.0": + "integrity" "sha512-LzWcbfMbAsEDTRmhjWIioe8GcDRl0fa35YMXFoJKDdiD/quGFmjJjdgPjFJJNwCMaLyQqFIDqCdHD2V4HfLgYA==" + "resolved" "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-4.0.0.tgz" + "version" "4.0.0" + +"style-search@^0.1.0": + "integrity" "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==" + "resolved" "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz" + "version" "0.1.0" + +"stylelint-config-recommended-scss@^6.0.0": + "integrity" "sha512-6QOe2/OzXV2AP5FE12A7+qtKdZik7Saf42SMMl84ksVBBPpTdrV+9HaCbPYiRMiwELY9hXCVdH4wlJ+YJb5eig==" + "resolved" "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-6.0.0.tgz" + "version" "6.0.0" + dependencies: + "postcss-scss" "^4.0.2" + "stylelint-config-recommended" "^7.0.0" + "stylelint-scss" "^4.0.0" + +"stylelint-config-recommended@^7.0.0": + "integrity" "sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==" + "resolved" "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz" + "version" "7.0.0" + +"stylelint-config-standard-scss@^4.0": + "integrity" "sha512-xizu8PTEyB6zYXBiVg6VtvUYn9m57x+6ZtaOdaxsfpbe5eagLPGNlbYnKfm/CfN69ArUpnwR6LjgsTHzlGbtXQ==" + "resolved" "https://registry.npmjs.org/stylelint-config-standard-scss/-/stylelint-config-standard-scss-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "stylelint-config-recommended-scss" "^6.0.0" + "stylelint-config-standard" "^25.0.0" + +"stylelint-config-standard@^25.0.0": + "integrity" "sha512-21HnP3VSpaT1wFjFvv9VjvOGDtAviv47uTp3uFmzcN+3Lt+RYRv6oAplLaV51Kf792JSxJ6svCJh/G18E9VnCA==" + "resolved" "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-25.0.0.tgz" + "version" "25.0.0" + dependencies: + "stylelint-config-recommended" "^7.0.0" + +"stylelint-scss@^4.0.0": + "integrity" "sha512-TSUgIeS0H3jqDZnby1UO1Qv3poi1N8wUYIJY6D1tuUq2MN3lwp/rITVo0wD+1SWTmRm0tNmGO0b7nKInnqF6Hg==" + "resolved" "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-4.7.0.tgz" + "version" "4.7.0" + dependencies: + "postcss-media-query-parser" "^0.2.3" + "postcss-resolve-nested-selector" "^0.1.1" + "postcss-selector-parser" "^6.0.11" + "postcss-value-parser" "^4.2.0" + +"stylelint@^14.4.0", "stylelint@^14.5.1 || ^15.0.0", "stylelint@^14.9": + "integrity" "sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A==" + "resolved" "https://registry.npmjs.org/stylelint/-/stylelint-14.16.1.tgz" + "version" "14.16.1" + dependencies: + "@csstools/selector-specificity" "^2.0.2" + "balanced-match" "^2.0.0" + "colord" "^2.9.3" + "cosmiconfig" "^7.1.0" + "css-functions-list" "^3.1.0" + "debug" "^4.3.4" + "fast-glob" "^3.2.12" + "fastest-levenshtein" "^1.0.16" + "file-entry-cache" "^6.0.1" + "global-modules" "^2.0.0" + "globby" "^11.1.0" + "globjoin" "^0.1.4" + "html-tags" "^3.2.0" + "ignore" "^5.2.1" + "import-lazy" "^4.0.0" + "imurmurhash" "^0.1.4" + "is-plain-object" "^5.0.0" + "known-css-properties" "^0.26.0" + "mathml-tag-names" "^2.1.3" + "meow" "^9.0.0" + "micromatch" "^4.0.5" + "normalize-path" "^3.0.0" + "picocolors" "^1.0.0" + "postcss" "^8.4.19" + "postcss-media-query-parser" "^0.2.3" + "postcss-resolve-nested-selector" "^0.1.1" + "postcss-safe-parser" "^6.0.0" + "postcss-selector-parser" "^6.0.11" + "postcss-value-parser" "^4.2.0" + "resolve-from" "^5.0.0" + "string-width" "^4.2.3" + "strip-ansi" "^6.0.1" + "style-search" "^0.1.0" + "supports-hyperlinks" "^2.3.0" + "svg-tags" "^1.0.0" + "table" "^6.8.1" + "v8-compile-cache" "^2.3.0" + "write-file-atomic" "^4.0.2" + +"stylis@^4.1.3": + "integrity" "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" + "resolved" "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz" + "version" "4.3.0" + +"supports-color@^5.3.0": + "integrity" "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==" + "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + "version" "5.5.0" + dependencies: + "has-flag" "^3.0.0" + +"supports-color@^7.0.0": + "integrity" "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" + "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + "version" "7.2.0" + dependencies: + "has-flag" "^4.0.0" + +"supports-color@^7.1.0": + "integrity" "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" + "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + "version" "7.2.0" + dependencies: + "has-flag" "^4.0.0" + +"supports-hyperlinks@^2.3.0": + "integrity" "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==" + "resolved" "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz" + "version" "2.3.0" + dependencies: + "has-flag" "^4.0.0" + "supports-color" "^7.0.0" + +"supports-preserve-symlinks-flag@^1.0.0": + "integrity" "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + "resolved" "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + "version" "1.0.0" + +"svg-tags@^1.0.0": + "integrity" "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==" + "resolved" "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz" + "version" "1.0.0" + +"table@^6.8.1": + "integrity" "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==" + "resolved" "https://registry.npmjs.org/table/-/table-6.8.1.tgz" + "version" "6.8.1" + dependencies: + "ajv" "^8.0.1" + "lodash.truncate" "^4.4.2" + "slice-ansi" "^4.0.0" + "string-width" "^4.2.3" + "strip-ansi" "^6.0.1" + +"tar-stream@^1.5.2": + "integrity" "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==" + "resolved" "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz" + "version" "1.6.2" + dependencies: + "bl" "^1.0.0" + "buffer-alloc" "^1.2.0" + "end-of-stream" "^1.0.0" + "fs-constants" "^1.0.0" + "readable-stream" "^2.3.0" + "to-buffer" "^1.1.1" + "xtend" "^4.0.0" + +"text-table@^0.2.0": + "integrity" "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "resolved" "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + "version" "0.2.0" + +"thenby@^1.3.4": + "integrity" "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==" + "resolved" "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz" + "version" "1.3.4" + +"through@^2.3.8": + "integrity" "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + "resolved" "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + "version" "2.3.8" + +"tiny-emitter@^2.0.0": + "integrity" "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + "resolved" "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz" + "version" "2.1.0" + +"to-buffer@^1.1.1": + "integrity" "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + "resolved" "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz" + "version" "1.1.1" + +"to-fast-properties@^2.0.0": + "integrity" "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + "resolved" "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" + "version" "2.0.0" + +"to-regex-range@^5.0.1": + "integrity" "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==" + "resolved" "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "is-number" "^7.0.0" + +"tr46@~0.0.3": + "integrity" "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "resolved" "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" + "version" "0.0.3" + +"trim-newlines@^3.0.0": + "integrity" "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==" + "resolved" "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz" + "version" "3.0.1" + +"ts-dedent@^2.2.0": + "integrity" "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==" + "resolved" "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz" + "version" "2.2.0" + +"type-check@^0.4.0", "type-check@~0.4.0": + "integrity" "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==" + "resolved" "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + "version" "0.4.0" + dependencies: + "prelude-ls" "^1.2.1" + +"type-fest@^0.18.0": + "integrity" "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==" + "resolved" "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz" + "version" "0.18.1" + +"type-fest@^0.20.2": + "integrity" "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + "resolved" "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + "version" "0.20.2" + +"type-fest@^0.6.0": + "integrity" "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + "resolved" "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz" + "version" "0.6.0" + +"type-fest@^0.8.1": + "integrity" "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + "resolved" "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" + "version" "0.8.1" + +"uc.micro@^1.0.1", "uc.micro@^1.0.5": + "integrity" "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + "resolved" "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz" + "version" "1.0.6" + +"uglify-js@^3.1.4": + "integrity" "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==" + "resolved" "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz" + "version" "3.17.4" + +"unbzip2-stream@^1.0.9": + "integrity" "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==" + "resolved" "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz" + "version" "1.4.3" + dependencies: + "buffer" "^5.2.1" + "through" "^2.3.8" + +"unicode-canonical-property-names-ecmascript@^2.0.0": + "integrity" "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==" + "resolved" "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz" + "version" "2.0.0" + +"unicode-match-property-ecmascript@^2.0.0": + "integrity" "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==" + "resolved" "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "unicode-canonical-property-names-ecmascript" "^2.0.0" + "unicode-property-aliases-ecmascript" "^2.0.0" + +"unicode-match-property-value-ecmascript@^2.1.0": + "integrity" "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==" + "resolved" "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz" + "version" "2.1.0" + +"unicode-property-aliases-ecmascript@^2.0.0": + "integrity" "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" + "resolved" "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" + "version" "2.1.0" + +"unist-util-stringify-position@^3.0.0": + "integrity" "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==" + "resolved" "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz" + "version" "3.0.3" + dependencies: + "@types/unist" "^2.0.0" + +"universalify@^2.0.0": + "integrity" "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + "resolved" "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" + "version" "2.0.0" + +"update-browserslist-db@^1.0.11": + "integrity" "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==" + "resolved" "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz" + "version" "1.0.11" + dependencies: + "escalade" "^3.1.1" + "picocolors" "^1.0.0" + +"uri-js@^4.2.2": + "integrity" "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==" + "resolved" "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + "version" "4.4.1" + dependencies: + "punycode" "^2.1.0" + +"util-deprecate@^1.0.2", "util-deprecate@~1.0.1": + "integrity" "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "resolved" "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + "version" "1.0.2" + +"uuid@^9.0.0": + "integrity" "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + "resolved" "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz" + "version" "9.0.0" + +"uvu@^0.5.0": + "integrity" "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==" + "resolved" "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz" + "version" "0.5.6" + dependencies: + "dequal" "^2.0.0" + "diff" "^5.0.0" + "kleur" "^4.0.3" + "sade" "^1.7.3" + +"v8-compile-cache@^2.3.0": + "integrity" "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==" + "resolved" "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" + "version" "2.3.0" + +"validate-npm-package-license@^3.0.1": + "integrity" "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==" + "resolved" "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" + "version" "3.0.4" + dependencies: + "spdx-correct" "^3.0.0" + "spdx-expression-parse" "^3.0.0" + +"web-streams-polyfill@^3.0.3": + "integrity" "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" + "resolved" "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz" + "version" "3.2.1" + +"web-worker@^1.2.0": + "integrity" "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==" + "resolved" "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz" + "version" "1.2.0" + +"webidl-conversions@^3.0.0": + "integrity" "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "resolved" "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" + "version" "3.0.1" + +"whatwg-url@^5.0.0": + "integrity" "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==" + "resolved" "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "tr46" "~0.0.3" + "webidl-conversions" "^3.0.0" + +"which-module@^2.0.0": + "integrity" "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + "resolved" "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz" + "version" "2.0.1" + +"which@^1.3.1": + "integrity" "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==" + "resolved" "https://registry.npmjs.org/which/-/which-1.3.1.tgz" + "version" "1.3.1" + dependencies: + "isexe" "^2.0.0" + +"which@^2.0.1": + "integrity" "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==" + "resolved" "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + "version" "2.0.2" + dependencies: + "isexe" "^2.0.0" + +"wordwrap@^1.0.0": + "integrity" "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + "resolved" "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" + "version" "1.0.0" + +"wrap-ansi@^6.2.0": + "integrity" "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==" + "resolved" "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" + "version" "6.2.0" + dependencies: + "ansi-styles" "^4.0.0" + "string-width" "^4.1.0" + "strip-ansi" "^6.0.0" + +"wrap-ansi@^7.0.0": + "integrity" "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==" + "resolved" "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + "version" "7.0.0" + dependencies: + "ansi-styles" "^4.0.0" + "string-width" "^4.1.0" + "strip-ansi" "^6.0.0" + +"wrappy@1": + "integrity" "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "resolved" "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + "version" "1.0.2" + +"write-file-atomic@^4.0.2": + "integrity" "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==" + "resolved" "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz" + "version" "4.0.2" + dependencies: + "imurmurhash" "^0.1.4" + "signal-exit" "^3.0.7" + +"xtend@^4.0.0": + "integrity" "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "resolved" "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" + "version" "4.0.2" + +"y18n@^4.0.0": + "integrity" "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + "resolved" "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz" + "version" "4.0.3" + +"y18n@^5.0.5": + "integrity" "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + "resolved" "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + "version" "5.0.8" + +"yallist@^3.0.2": + "integrity" "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "resolved" "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" + "version" "3.1.1" + +"yallist@^4.0.0": + "integrity" "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "resolved" "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" + "version" "4.0.0" + +"yaml@^1.10.0", "yaml@1.10.2": + "integrity" "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + "resolved" "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" + "version" "1.10.2" + +"yaml@^2.1.1": + "integrity" "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==" + "resolved" "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz" + "version" "2.3.1" + +"yargs-parser@^18.1.2": + "integrity" "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==" + "resolved" "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz" + "version" "18.1.3" + dependencies: + "camelcase" "^5.0.0" + "decamelize" "^1.2.0" + +"yargs-parser@^20.2.3": + "integrity" "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + "resolved" "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" + "version" "20.2.9" + +"yargs-parser@^21.0.0": + "integrity" "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + "resolved" "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" + "version" "21.1.1" + +"yargs@^15.3.1": + "integrity" "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==" + "resolved" "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz" + "version" "15.4.1" + dependencies: + "cliui" "^6.0.0" + "decamelize" "^1.2.0" + "find-up" "^4.1.0" + "get-caller-file" "^2.0.1" + "require-directory" "^2.1.1" + "require-main-filename" "^2.0.0" + "set-blocking" "^2.0.0" + "string-width" "^4.2.0" + "which-module" "^2.0.0" + "y18n" "^4.0.0" + "yargs-parser" "^18.1.2" + +"yargs@^17.0.0", "yargs@17.5.x": + "integrity" "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==" + "resolved" "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz" + "version" "17.5.1" + dependencies: + "cliui" "^7.0.2" + "escalade" "^3.1.1" + "get-caller-file" "^2.0.5" + "require-directory" "^2.1.1" + "string-width" "^4.2.3" + "y18n" "^5.0.5" + "yargs-parser" "^21.0.0" + +"yauzl@^2.4.2": + "integrity" "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==" + "resolved" "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz" + "version" "2.10.0" + dependencies: + "buffer-crc32" "~0.2.3" + "fd-slicer" "~1.1.0" + +"yocto-queue@^0.1.0": + "integrity" "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + "resolved" "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + "version" "0.1.0" diff --git a/examples/README.rst b/examples/README.rst deleted file mode 100644 index df5b39e4a..000000000 --- a/examples/README.rst +++ /dev/null @@ -1,23 +0,0 @@ -Running -======= - -.. code-block:: bash - - $ cd docker/ - $ docker compose up - -If the Druid data doesn't load, you need to fix the permissions: - -.. code-block:: bash - - $ docker exec -u root -it coordinator sh - $ chmod 777 /opt/shared - $ docker-compose restart druid_ingest - -Install the required SQLAlchemy dialects: - -.. code-block:: bash - - $ pip install 'shillelagh[gsheetsapi]' pydruid psycopg2-binary - -And run: diff --git a/examples/configs/databases/druid.yaml b/examples/configs/databases/druid.yaml deleted file mode 100644 index 09dfa20aa..000000000 --- a/examples/configs/databases/druid.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: An Apache Druid database -URI: druid://localhost:8082/druid/v2/sql/ -read_only: true diff --git a/examples/configs/databases/gsheets.yaml b/examples/configs/databases/gsheets.yaml deleted file mode 100644 index e617caeb2..000000000 --- a/examples/configs/databases/gsheets.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: A Google Sheets connector -URI: gsheets:// -read_only: true diff --git a/examples/configs/databases/postgres.yaml b/examples/configs/databases/postgres.yaml deleted file mode 100644 index 78e15e298..000000000 --- a/examples/configs/databases/postgres.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: A Postgres database -URI: postgresql://username:FoolishPassword@localhost:5433/examples -read_only: false diff --git a/examples/configs/dj.yaml b/examples/configs/dj.yaml deleted file mode 100644 index 3da45f540..000000000 --- a/examples/configs/dj.yaml +++ /dev/null @@ -1 +0,0 @@ -index: sqlite:///dj.db diff --git a/examples/configs/nodes/core/comments.yaml b/examples/configs/nodes/core/comments.yaml deleted file mode 100644 index 02a553cca..000000000 --- a/examples/configs/nodes/core/comments.yaml +++ /dev/null @@ -1,17 +0,0 @@ -description: A fact table with comments -representations: - gsheets: - catalog: null - schema: null - table: "https://docs.google.com/spreadsheets/d/1SkEZOipqjXQnxHLMr2kZ7Tbn7OiHSgO99gOCS5jTQJs/edit#gid=1811447072" - cost: 100 - postgres: - catalog: null - schema: public - table: comments - cost: 10 - druid: - catalog: null - schema: druid - table: comments - cost: 1 diff --git a/examples/configs/nodes/core/users.yaml b/examples/configs/nodes/core/users.yaml deleted file mode 100644 index ab6acbf95..000000000 --- a/examples/configs/nodes/core/users.yaml +++ /dev/null @@ -1,12 +0,0 @@ -description: A user dimension table -representations: - gsheets: - catalog: null - schema: null - table: "https://docs.google.com/spreadsheets/d/1SkEZOipqjXQnxHLMr2kZ7Tbn7OiHSgO99gOCS5jTQJs/edit#gid=0" - cost: 100 - postgres: - catalog: null - schema: public - table: dim_users - cost: 10 diff --git a/examples/docker/docker-compose.yml b/examples/docker/docker-compose.yml deleted file mode 100644 index 78a30c8c1..000000000 --- a/examples/docker/docker-compose.yml +++ /dev/null @@ -1,157 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# -version: "2.2" - -volumes: - metadata_data: {} - middle_var: {} - historical_var: {} - broker_var: {} - coordinator_var: {} - router_var: {} - druid_shared: {} - postgres_data: {} - - -services: - postgres_examples: - container_name: postgres_examples - image: postgres:latest - volumes: - - ./postgres_init.sql:/docker-entrypoint-initdb.d/init.sql - - postgres_data:/var/lib/postgresql/data - environment: - - POSTGRES_PASSWORD=FoolishPassword - - POSTGRES_USER=username - - POSTGRES_DB=examples - ports: - - "5433:5432" - - postgres: - container_name: postgres - image: postgres:latest - volumes: - - metadata_data:/var/lib/postgresql/data - environment: - - POSTGRES_PASSWORD=FoolishPassword - - POSTGRES_USER=druid - - POSTGRES_DB=druid - - # Need 3.5 or later for container nodes - zookeeper: - container_name: zookeeper - image: zookeeper:3.5 - ports: - - "2181:2181" - environment: - - ZOO_MY_ID=1 - - coordinator: - image: apache/druid:0.20.0 - container_name: coordinator - volumes: - - druid_shared:/opt/shared - - coordinator_var:/opt/druid/var - depends_on: - - zookeeper - - postgres - ports: - - "8081:8081" - command: - - coordinator - env_file: - - environment - - broker: - image: apache/druid:0.20.0 - container_name: broker - volumes: - - broker_var:/opt/druid/var - depends_on: - - zookeeper - - postgres - - coordinator - ports: - - "8082:8082" - command: - - broker - env_file: - - environment - - historical: - image: apache/druid:0.20.0 - container_name: historical - volumes: - - druid_shared:/opt/shared - - historical_var:/opt/druid/var - depends_on: - - zookeeper - - postgres - - coordinator - ports: - - "8083:8083" - command: - - historical - env_file: - - environment - - middlemanager: - image: apache/druid:0.20.0 - container_name: middlemanager - volumes: - - druid_shared:/opt/shared - - middle_var:/opt/druid/var - depends_on: - - zookeeper - - postgres - - coordinator - ports: - - "8091:8091" - - "8100-8105:8100-8105" - command: - - middleManager - env_file: - - environment - - router: - image: apache/druid:0.20.0 - container_name: router - volumes: - - router_var:/opt/druid/var - depends_on: - - zookeeper - - postgres - - coordinator - ports: - - "8888:8888" - command: - - router - env_file: - - environment - - druid_ingest: - image: curlimages/curl - depends_on: - - router - container_name: druid_ingest - volumes: - - ./druid_init.sh:/druid_init.sh - - ./druid_spec.json:/druid_spec.json - - ./wait-for:/wait-for - entrypoint: sh -c './wait-for coordinator:8081 -- /druid_init.sh' diff --git a/examples/docker/postgres_init.sql b/examples/docker/postgres_init.sql deleted file mode 100644 index c5c7c5bdd..000000000 --- a/examples/docker/postgres_init.sql +++ /dev/null @@ -1,26 +0,0 @@ -DROP TABLE IF EXISTS dim_users; - -DROP TABLE IF EXISTS comments; - -CREATE TABLE dim_users ( - id integer PRIMARY KEY, - full_name text, - age integer, - country text, - gender text, - preferred_language text -); - -INSERT INTO dim_users (id, full_name, age, country, gender, preferred_language) - VALUES (1, 'Alice One', 10, 'Argentina', 'female', 'Spanish'), (2, 'Bob Two', 15, 'Brazil', 'male', 'Portuguese'), (3, 'Charlie Three', 20, 'Chile', 'non-binary', 'Spanish'), (4, 'Denise Four', 25, 'Denmark', 'female', 'Danish'), (5, 'Ernie Five', 27, 'Equator', 'male', 'Spanish'), (6, 'Fabian Six', 29, 'France', 'non-binary', 'French'); - -CREATE TABLE comments ( - id integer PRIMARY KEY, - user_id integer, - "timestamp" timestamp with time zone, - "text" text, - CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES dim_users (id) -); - -INSERT INTO comments (id, user_id, "timestamp", "text") - VALUES (1, 1, '2021-01-01 01:00:00', 'Hola!'), (2, 2, '2021-01-01 02:00:00', 'Oi, tudo bom?'), (3, 3, '2021-01-01 03:00:00', 'Que pasa?'), (4, 4, '2021-01-01 04:00:00', 'Også mig'), (5, 5, '2021-01-01 05:00:00', 'Bueno'), (6, 6, '2021-01-01 06:00:00', 'Bonjour!'), (7, 2, '2021-01-01 07:00:00', 'Prazer em conhecer'), (8, 3, '2021-01-01 08:00:00', 'Si, si'), (9, 4, '2021-01-01 09:00:00', 'Hej'), (10, 5, '2021-01-01 10:00:00', 'Por supuesto'), (11, 6, '2021-01-01 11:00:00', 'Oui, oui'), (12, 3, '2021-01-01 12:00:00', 'Como no?'), (13, 4, '2021-01-01 13:00:00', 'Farvel'), (14, 5, '2021-01-01 14:00:00', 'Hola, amigo!'), (15, 6, '2021-01-01 15:00:00', 'Très bien'), (16, 4, '2021-01-01 16:00:00', 'Dejligt at møde dig'), (17, 5, '2021-01-01 17:00:00', 'Dale!'), (18, 6, '2021-01-01 18:00:00', 'Bien sûr!'), (19, 5, '2021-01-01 19:00:00', 'Hasta luego!'), (20, 6, '2021-01-01 20:00:00', 'À toute à l'' heure ! '), (21, 6, '2021-01-01 21:00:00', 'Peut être'); diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 000000000..72ae80299 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,9 @@ +# Helpful gist to see all available values +# https://gist.github.com/DavidWells/43884f15aed7e4dcb3a6dad06430b756 +[build] + base = "docs/" + command = "./build-docs.sh 0.1.0 true $DEPLOY_URL" + publish = "public" + +[build.environment] + HUGO_VERSION = "0.113.0" diff --git a/notebooks/DJ Projects Tutorial.ipynb b/notebooks/DJ Projects Tutorial.ipynb new file mode 100644 index 000000000..631a4d521 --- /dev/null +++ b/notebooks/DJ Projects Tutorial.ipynb @@ -0,0 +1,175 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9dc8658c-d6f5-4e97-9112-ee5ab006e585", + "metadata": {}, + "source": [ + "# DataJunction Projects\n", + "\n", + "A DataJunction project is a collection of local YAML files that represent a particular namespace on the DataJunction server. Projects can be developed locally and then deployed. An existing namespace can also be pulled down to a local project. This notebook walks through how to work with DataJunction projects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d74fec0-8ef7-4bd6-bb93-7d56ef1f7974", + "metadata": {}, + "outputs": [], + "source": [ + "from datajunction import DJBuilder, Project\n", + "from datajunction._internal import RequestsSessionWithEndpoint" + ] + }, + { + "cell_type": "markdown", + "id": "8c92ccd3-c0f2-4584-a372-71373793e187", + "metadata": {}, + "source": [ + "# Create a \"dj\" User" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ca381e9-41a1-49c2-aba7-f0bf4577a8ce", + "metadata": {}, + "outputs": [], + "source": [ + "session = RequestsSessionWithEndpoint(endpoint=\"http://dj:8000\")\n", + "session.post(\"/basic/user/\", data={\"email\": \"dj@dj.com\", \"username\": \"dj\", \"password\": \"dj\"})" + ] + }, + { + "cell_type": "markdown", + "id": "112050ce-1b18-4ea7-9b02-831b427776b5", + "metadata": {}, + "source": [ + "# Login as the User \"dj\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abca4c31-eae8-4d89-9f51-fa50fca2a0d6", + "metadata": {}, + "outputs": [], + "source": [ + "session.post(\"/basic/login/\", data={\"username\": \"dj\", \"password\": \"dj\"})" + ] + }, + { + "cell_type": "markdown", + "id": "0a62b79e-27a4-4d1c-9e4f-91e9125f89ca", + "metadata": {}, + "source": [ + "# Instantiate a Client Using the Logged In Session" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc6b9aeb-3c71-4bf3-ac0f-51be8b161a50", + "metadata": {}, + "outputs": [], + "source": [ + "dj = DJBuilder(requests_session=session)" + ] + }, + { + "cell_type": "markdown", + "id": "9e8f60a0-b19c-405d-b723-5766392b780f", + "metadata": {}, + "source": [ + "# Import an Existing Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e82977fb-b973-4d17-b718-3b610233c9a7", + "metadata": {}, + "outputs": [], + "source": [ + "Project.pull(client=dj, namespace=\"default\", target_path=\"./example_project\", ignore_existing_files=True)" + ] + }, + { + "cell_type": "markdown", + "id": "c245ec81-26ba-426c-bfe3-ce8885433cba", + "metadata": {}, + "source": [ + "# Load and Compile the Project in the Current Directory" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff102299-5f09-4e5d-9fb4-e7f7c9075a16", + "metadata": {}, + "outputs": [], + "source": [ + "project = Project.load(\"./example_project\")\n", + "compiled_project = project.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "ad1b350d-3185-483f-a80f-a58015563da9", + "metadata": {}, + "source": [ + "# Optionally Validate the Project\n", + "### (This automatically happens during during deployment)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88cb4ca7-d0fb-470e-b642-a2ac7dfcf5e0", + "metadata": {}, + "outputs": [], + "source": [ + "# compiled_project.validate(client=dj)" + ] + }, + { + "cell_type": "markdown", + "id": "c57adadd-c794-41f5-9d08-28184915de77", + "metadata": {}, + "source": [ + "# Deploy the Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b1ee788-afa2-46b9-b6d4-610633c2f1eb", + "metadata": {}, + "outputs": [], + "source": [ + "compiled_project.deploy(client=dj)" + ] + } + ], + "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.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/Modeling the Roads Example Database.ipynb b/notebooks/Modeling the Roads Example Database.ipynb new file mode 100644 index 000000000..ccda54f59 --- /dev/null +++ b/notebooks/Modeling the Roads Example Database.ipynb @@ -0,0 +1,1684 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1e59e4df", + "metadata": {}, + "source": [ + "# Modeling the Roads Example Database" + ] + }, + { + "cell_type": "markdown", + "id": "0df819ea", + "metadata": {}, + "source": [ + "![dj-roads-erd](./images/dj-roads-erd.jpg)\n", + "*note: open in a new tab to see at full-resolution*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba3ff960", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import requests\n", + "from datajunction._internal import RequestsSessionWithEndpoint\n", + "\n", + "DJ_URL = \"http://dj:8000\"\n", + "DJQS_URL = \"http://djqs:8001\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d050af96-a9da-42b6-af4a-3e93bd6d8748", + "metadata": {}, + "outputs": [], + "source": [ + "session = RequestsSessionWithEndpoint(endpoint=\"http://dj:8000\")" + ] + }, + { + "cell_type": "markdown", + "id": "07ba9613-eb07-454a-91aa-9af8dba96d4a", + "metadata": {}, + "source": [ + "# Login as the User \"dj\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6cc01b37-7637-4406-970d-8c86a9c0aa5e", + "metadata": {}, + "outputs": [], + "source": [ + "session.post(\"/basic/login/\", data={\"username\": \"dj\", \"password\": \"dj\"})" + ] + }, + { + "cell_type": "markdown", + "id": "9b9ee60b", + "metadata": {}, + "source": [ + "# Add a Catalog and an Engine\n", + "\n", + "In DJ, all nodes used in a query must share a common catalog. Before creating source nodes, add a `dj` catalog and a `duckdb` engine to the system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da86a1ec", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/catalogs/\",\n", + " json={\"name\": \"warehouse\"},\n", + ")\n", + "print(response.json())\n", + "\n", + "response = session.post(\n", + " f\"{DJ_URL}/engines/\",\n", + " json={\n", + " \"name\": \"duckdb\",\n", + " \"version\": \"0.7.1\",\n", + " },\n", + ")\n", + "print(response.json())\n", + "\n", + "response = session.post(\n", + " f\"{DJ_URL}/catalogs/warehouse/engines/\",\n", + " json=[{\"name\": \"duckdb\", \"version\": \"0.7.1\"}],\n", + ")\n", + "print(response.json())\n" + ] + }, + { + "cell_type": "markdown", + "id": "c51432bf-6400-4035-aab1-03248175610f", + "metadata": {}, + "source": [ + "# Create a `default` Namespace" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b65c245e-757b-4367-886a-2fa766156543", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(f\"{DJ_URL}/namespaces/default/\")\n", + "response.json()" + ] + }, + { + "cell_type": "markdown", + "id": "eebb213d", + "metadata": {}, + "source": [ + "# Create Source Nodes\n", + "Create twelve source nodes for each of the tables in the DJ roads example database." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b6e7674", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"repair_order_id\", \"type\": \"int\"},\n", + " {\"name\": \"municipality_id\", \"type\": \"string\"},\n", + " {\"name\": \"hard_hat_id\", \"type\": \"int\"},\n", + " {\"name\": \"order_date\", \"type\": \"date\"},\n", + " {\"name\": \"required_date\", \"type\": \"date\"},\n", + " {\"name\": \"dispatched_date\", \"type\": \"date\"},\n", + " {\"name\": \"dispatcher_id\", \"type\": \"int\"},\n", + " ],\n", + " \"description\": \"Repair orders\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.repair_orders\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"repair_orders\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99812125", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"repair_order_id\", \"type\": \"int\"},\n", + " {\"name\": \"repair_type_id\", \"type\": \"int\"},\n", + " {\"name\": \"price\", \"type\": \"float\"},\n", + " {\"name\": \"quantity\", \"type\": \"int\"},\n", + " {\"name\": \"discount\", \"type\": \"float\"},\n", + " ],\n", + " \"description\": \"Details on repair orders\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.repair_order_details\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"repair_order_details\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "122440f9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"repair_type_id\", \"type\": \"int\"},\n", + " {\"name\": \"repair_type_name\", \"type\": \"string\"},\n", + " {\"name\": \"contractor_id\", \"type\": \"int\"},\n", + " ],\n", + " \"description\": \"Information on different types of repairs\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.repair_type\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"repair_type\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4092b42", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"contractor_id\", \"type\": \"int\"},\n", + " {\"name\": \"company_name\", \"type\": \"string\"},\n", + " {\"name\": \"contact_name\", \"type\": \"string\"},\n", + " {\"name\": \"contact_title\", \"type\": \"string\"},\n", + " {\"name\": \"address\", \"type\": \"string\"},\n", + " {\"name\": \"city\", \"type\": \"string\"},\n", + " {\"name\": \"state\", \"type\": \"string\"},\n", + " {\"name\": \"postal_code\", \"type\": \"string\"},\n", + " {\"name\": \"country\", \"type\": \"string\"},\n", + " ],\n", + " \"description\": \"Information on different types of repairs\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.contractors\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"contractors\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05cc34e6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"municipality_id\", \"type\": \"string\"},\n", + " {\"name\": \"municipality_type_id\", \"type\": \"string\"},\n", + " ],\n", + " \"description\": \"Information on different types of repairs\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.municipality_municipality_type\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"municipality_municipality_type\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9dc5d78b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"municipality_type_id\", \"type\": \"string\"},\n", + " {\"name\": \"municipality_type_desc\", \"type\": \"string\"},\n", + " ],\n", + " \"description\": \"Information on different types of repairs\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.municipality_type\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"municipality_type\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c20728c0", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"municipality_id\", \"type\": \"string\"},\n", + " {\"name\": \"contact_name\", \"type\": \"string\"},\n", + " {\"name\": \"contact_title\", \"type\": \"string\"},\n", + " {\"name\": \"local_region\", \"type\": \"string\"},\n", + " {\"name\": \"phone\", \"type\": \"string\"},\n", + " {\"name\": \"state_id\", \"type\": \"int\"},\n", + " ],\n", + " \"description\": \"Information on different types of repairs\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.municipality\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"municipality\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b876fc24", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"dispatcher_id\", \"type\": \"int\"},\n", + " {\"name\": \"company_name\", \"type\": \"string\"},\n", + " {\"name\": \"phone\", \"type\": \"string\"},\n", + " ],\n", + " \"description\": \"Information on different types of repairs\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.dispatchers\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"dispatchers\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1cc47895", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"hard_hat_id\", \"type\": \"int\"},\n", + " {\"name\": \"last_name\", \"type\": \"string\"},\n", + " {\"name\": \"first_name\", \"type\": \"string\"},\n", + " {\"name\": \"title\", \"type\": \"string\"},\n", + " {\"name\": \"birth_date\", \"type\": \"date\"},\n", + " {\"name\": \"hire_date\", \"type\": \"date\"},\n", + " {\"name\": \"address\", \"type\": \"string\"},\n", + " {\"name\": \"city\", \"type\": \"string\"},\n", + " {\"name\": \"state\", \"type\": \"string\"},\n", + " {\"name\": \"postal_code\", \"type\": \"string\"},\n", + " {\"name\": \"country\", \"type\": \"string\"},\n", + " {\"name\": \"manager\", \"type\": \"int\"},\n", + " {\"name\": \"contractor_id\", \"type\": \"int\"},\n", + " ],\n", + " \"description\": \"Information on different types of repairs\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.hard_hats\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"hard_hats\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87aeb9b6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"hard_hat_id\", \"type\": \"int\"},\n", + " {\"name\": \"state_id\", \"type\": \"string\"},\n", + " ],\n", + " \"description\": \"Information on different types of repairs\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.hard_hat_state\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"hard_hat_state\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "310c9ef4", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"state_id\", \"type\": \"int\"},\n", + " {\"name\": \"state_name\", \"type\": \"string\"},\n", + " {\"name\": \"state_abbr\", \"type\": \"string\"},\n", + " {\"name\": \"state_region\", \"type\": \"int\"},\n", + " ],\n", + " \"description\": \"Information on different types of repairs\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.us_states\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"us_states\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7ce8d9d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"us_region_id\", \"type\": \"int\"},\n", + " {\"name\": \"us_region_description\", \"type\": \"string\"},\n", + " ],\n", + " \"description\": \"Information on different types of repairs\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.us_region\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"us_region\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7959c670-23c4-41fc-9bc2-8ae4531a69bd", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/source/\",\n", + " json={\n", + " \"columns\": [\n", + " {\"name\": \"dateint\", \"type\": \"timestamp\"},\n", + " {\"name\": \"year\", \"type\": \"int\"},\n", + " {\"name\": \"month\", \"type\": \"int\"},\n", + " {\"name\": \"day\", \"type\": \"int\"},\n", + " ],\n", + " \"description\": \"Date dimension\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.date\",\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"date\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "markdown", + "id": "7a1c79a5", + "metadata": {}, + "source": [ + "# Create Dimension Nodes\n", + "\n", + "Dimension nodes are how you represent dimensions in the data model and can be defined using any SQL, including filters as well as joins to source nodes, transform nodes, and other dimension nodes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff5a3eb0-01e8-4858-89ef-126dddc17f10", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/dimension/\",\n", + " json={\n", + " \"description\": \"Date dimension\",\n", + " \"query\": \"\"\"\n", + " SELECT\n", + " dateint,\n", + " month,\n", + " year,\n", + " day\n", + " FROM default.date\n", + " \"\"\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.date_dim\",\n", + " \"type\": \"dimension\",\n", + " \"primary_key\": [\"dateint\"],\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dffcf40a", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/transform/\",\n", + " json={\n", + " \"description\": \"Repair order dimension\",\n", + " \"query\": \"\"\"\n", + " SELECT\n", + " repair_order_id,\n", + " municipality_id,\n", + " hard_hat_id,\n", + " dispatcher_id\n", + " FROM default.repair_orders\n", + " \"\"\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.repair_order_transform\",\n", + " \"type\": \"transform\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41d414c3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/dimension/\",\n", + " json={\n", + " \"description\": \"Repair order dimension\",\n", + " \"query\": \"\"\"\n", + " SELECT\n", + " repair_order_id,\n", + " municipality_id,\n", + " hard_hat_id,\n", + " dispatcher_id\n", + " FROM default.repair_orders\n", + " \"\"\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.repair_order\",\n", + " \"primary_key\": [\"repair_order_id\"],\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b61a229", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/dimension/\",\n", + " json={\n", + " \"description\": \"Contractor dimension\",\n", + " \"query\": \"\"\"\n", + " SELECT\n", + " contractor_id,\n", + " company_name,\n", + " contact_name,\n", + " contact_title,\n", + " address,\n", + " city,\n", + " state,\n", + " postal_code,\n", + " country\n", + " FROM default.contractors\n", + " \"\"\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.contractor\",\n", + " \"primary_key\": [\"contractor_id\"],\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a866d4a", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/dimension/\",\n", + " json={\n", + " \"description\": \"Hard hat dimension\",\n", + " \"query\": \"\"\"\n", + " SELECT\n", + " hard_hat_id,\n", + " last_name,\n", + " first_name,\n", + " title,\n", + " birth_date,\n", + " hire_date,\n", + " address,\n", + " city,\n", + " state,\n", + " postal_code,\n", + " country,\n", + " manager,\n", + " contractor_id\n", + " FROM default.hard_hats\n", + " \"\"\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.hard_hat\",\n", + " \"primary_key\": [\"hard_hat_id\"],\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d995d032", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/dimension/\",\n", + " json={\n", + " \"description\": \"Hard hat dimension\",\n", + " \"query\": \"\"\"\n", + " SELECT\n", + " hh.hard_hat_id,\n", + " last_name,\n", + " first_name,\n", + " title,\n", + " birth_date,\n", + " hire_date,\n", + " address,\n", + " city,\n", + " state,\n", + " postal_code,\n", + " country,\n", + " manager,\n", + " contractor_id,\n", + " hhs.state_id AS state_id\n", + " FROM default.hard_hats hh\n", + " LEFT JOIN default.hard_hat_state hhs\n", + " ON hh.hard_hat_id = hhs.hard_hat_id\n", + " WHERE hh.state_id = 'NY'\n", + " \"\"\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.local_hard_hats\",\n", + " \"primary_key\": [\"hard_hat_id\"],\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ebaa645", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/dimension/\",\n", + " json={\n", + " \"description\": \"US state dimension\",\n", + " \"query\": \"\"\"\n", + " SELECT\n", + " state_id,\n", + " state_name,\n", + " state_abbr,\n", + " state_region,\n", + " r.us_region_description AS state_region_description\n", + " FROM default.us_states s\n", + " LEFT JOIN default.us_region r\n", + " ON s.state_region = r.us_region_description\n", + " \"\"\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.us_state\",\n", + " \"primary_key\": [\"state_id\"],\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2105f66", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/dimension/\",\n", + " json={\n", + " \"description\": \"Dispatcher dimension\",\n", + " \"query\": \"\"\"\n", + " SELECT\n", + " dispatcher_id,\n", + " company_name,\n", + " phone\n", + " FROM default.dispatchers\n", + " \"\"\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.dispatcher\",\n", + " \"primary_key\": [\"dispatcher_id\"],\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e5b3210", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/dimension/\",\n", + " json={\n", + " \"description\": \"Municipality dimension\",\n", + " \"query\": \"\"\"\n", + " SELECT\n", + " m.municipality_id AS municipality_id,\n", + " contact_name,\n", + " contact_title,\n", + " local_region,\n", + " state_id,\n", + " mmt.municipality_type_id AS municipality_type_id,\n", + " mt.municipality_type_desc AS municipality_type_desc\n", + " FROM default.municipality AS m\n", + " LEFT JOIN default.municipality_municipality_type AS mmt\n", + " ON m.municipality_id = mmt.municipality_id\n", + " LEFT JOIN default.municipality_type AS mt\n", + " ON mmt.municipality_type_id = mt.municipality_type_desc\n", + " \"\"\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.municipality_dim\",\n", + " \"primary_key\": [\"municipality_id\"],\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "markdown", + "id": "d405291d", + "metadata": {}, + "source": [ + "# Add Metrics\n", + "\n", + "Metrics are defined by writing a SQL query that performs an aggregation function on an expression using columns from any **single** source node, dimension node, or transform node. Here are some metrics that can be added to the DJ server.\n", + "\n", + "- `num_repair_orders` - Number of repair orders\n", + "- `avg_repair_price` - Avg price of a repair order\n", + "- `total_repair_cost` - Total price of a repair order\n", + "- `avg_length_of_employment` - Avg length of employment\n", + "- `total_repair_order_discounts` - Total discounts on repair orders\n", + "- `avg_repair_order_discounts` - Avg discount on repair orders\n", + "- `avg_time_to_dispatch` - Avg time to dispatch\n", + "- `avg_time_to_dispatch_local` - Avg time to dispatch in NYC\n", + "\n", + "Each of these metrics can be grouped by any dimensions that are discoverable through dimension labels, such as `municipality`, `state`, `region`, `contractor`, `dispatcher`, or `hard_hat`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f87bc290", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/metric/\",\n", + " json={\n", + " \"description\": \"Number of repair orders\",\n", + " \"query\": \"SELECT count(repair_order_id) FROM default.repair_orders\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.num_repair_orders\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be519eac", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/metric/\",\n", + " json={\n", + " \"description\": \"Average repair price\",\n", + " \"query\": \"SELECT avg(price) FROM default.repair_order_details\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.avg_repair_price\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a8d229c3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/metric/\",\n", + " json={\n", + " \"description\": \"Total repair cost\",\n", + " \"query\": \"SELECT sum(price) FROM default.repair_order_details\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.total_repair_cost\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebaa0482", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/metric/\",\n", + " json={\n", + " \"description\": \"Average length of employment\",\n", + " \"query\": \"SELECT avg(NOW() - hire_date) FROM default.hard_hats\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.avg_length_of_employment\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0144baf7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/metric/\",\n", + " json={\n", + " \"description\": \"Total repair order discounts\",\n", + " \"query\": \"SELECT sum(price * discount) FROM default.repair_order_details\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.total_repair_order_discounts\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fce8e1f7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/metric/\",\n", + " json={\n", + " \"description\": \"Total repair order discounts\",\n", + " \"query\": \"SELECT avg(price * discount) FROM default.repair_order_details\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.avg_repair_order_discounts\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbba1585", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/metric/\",\n", + " json={\n", + " \"description\": \"Average time to dispatch a repair order\",\n", + " \"query\": \"SELECT avg(dispatched_date - order_date) FROM default.repair_orders\",\n", + " \"mode\": \"published\",\n", + " \"name\": \"default.avg_time_to_dispatch\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "markdown", + "id": "e22b99fa-9030-405f-b272-0d71ef986922", + "metadata": {}, + "source": [ + "## Complex Transforms and Metrics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dae37cbb-88bc-4133-ad62-25fd9e2fcc2a", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/transform/\",\n", + " json={\n", + " \"description\": \"National level aggregates\",\n", + " \"name\": \"default.national_level_agg\",\n", + " \"mode\": \"published\",\n", + " \"query\": \"SELECT SUM(rd.price * rd.quantity) AS total_amount_nationwide FROM default.repair_order_details rd\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37d46769-7de5-47b1-ac71-9969cca559c6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/transform/\",\n", + " json={\n", + " \"name\": \"default.regional_level_agg\",\n", + " \"description\": \"Regional-level aggregates\",\n", + " \"mode\": \"published\",\n", + " \"primary_key\": [\"us_region_id\", \"state_name\", \"order_year\", \"order_month\", \"order_day\"],\n", + " \"query\": \"\"\"SELECT\n", + " usr.us_region_id,\n", + " us.state_name,\n", + " CONCAT(us.state_name, '-', usr.us_region_description) AS location_hierarchy,\n", + " EXTRACT(YEAR FROM ro.order_date) AS order_year,\n", + " EXTRACT(MONTH FROM ro.order_date) AS order_month,\n", + " EXTRACT(DAY FROM ro.order_date) AS order_day,\n", + " COUNT(DISTINCT CASE WHEN ro.dispatched_date IS NOT NULL THEN ro.repair_order_id ELSE NULL END) AS completed_repairs,\n", + " COUNT(DISTINCT ro.repair_order_id) AS total_repairs_dispatched,\n", + " SUM(rd.price * rd.quantity) AS total_amount_in_region,\n", + " AVG(rd.price * rd.quantity) AS avg_repair_amount_in_region,\n", + " -- ELEMENT_AT(ARRAY_SORT(COLLECT_LIST(STRUCT(COUNT(*) AS cnt, rt.repair_type_name AS repair_type_name)), (left, right) -> case when left.cnt < right.cnt then 1 when left.cnt > right.cnt then -1 else 0 end), 0).repair_type_name AS most_common_repair_type,\n", + " AVG(DATEDIFF(ro.dispatched_date, ro.order_date)) AS avg_dispatch_delay,\n", + " COUNT(DISTINCT c.contractor_id) AS unique_contractors\n", + "FROM\n", + " (SELECT\n", + " repair_order_id,\n", + " municipality_id,\n", + " hard_hat_id,\n", + " order_date,\n", + " required_date,\n", + " dispatched_date,\n", + " dispatcher_id\n", + " FROM default.repair_orders) ro\n", + "JOIN\n", + " default.municipality m ON ro.municipality_id = m.municipality_id\n", + "JOIN\n", + " default.us_states us ON m.state_id = us.state_id\n", + " AND AVG(rd.price * rd.quantity) >\n", + " (SELECT AVG(price * quantity) FROM default.repair_order_details WHERE repair_order_id = ro.repair_order_id)\n", + "JOIN\n", + " default.us_states us ON m.state_id = us.state_id\n", + "JOIN\n", + " default.us_region usr ON us.state_region = usr.us_region_id\n", + "JOIN\n", + " default.repair_order_details rd ON ro.repair_order_id = rd.repair_order_id\n", + "JOIN\n", + " default.repair_type rt ON rd.repair_type_id = rt.repair_type_id\n", + "JOIN\n", + " default.contractors c ON rt.contractor_id = c.contractor_id\n", + "GROUP BY\n", + " usr.us_region_id,\n", + " EXTRACT(YEAR FROM ro.order_date),\n", + " EXTRACT(MONTH FROM ro.order_date),\n", + " EXTRACT(DAY FROM ro.order_date)\"\"\"\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6a921e4d-f9db-4750-b2d3-4b7ad6929fa9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/metric/\",\n", + " json={\n", + " \"description\": \"\"\"For each US region (as defined in the us_region table), we want to calculate:\n", + " Regional Repair Efficiency = (Number of Completed Repairs / Total Repairs Dispatched) × \n", + " (Total Repair Amount in Region / Total Repair Amount Nationwide) × 100\n", + " Here:\n", + " A \"Completed Repair\" is one where the dispatched_date is not null.\n", + " \"Total Repair Amount in Region\" is the total amount spent on repairs in a given region.\n", + " \"Total Repair Amount Nationwide\" is the total amount spent on all repairs nationwide.\"\"\",\n", + " \"name\": \"default.regional_repair_efficiency\",\n", + " \"query\": \"\"\"SELECT \n", + "(SUM(rm.completed_repairs) * 1.0 / SUM(rm.total_repairs_dispatched)) * \n", + "(SUM(rm.total_amount_in_region) * 1.0 / SUM(na.total_amount_nationwide)) * 100\n", + "FROM \n", + "default.regional_level_agg rm\n", + "CROSS JOIN \n", + "default.national_level_agg na\"\"\",\n", + " \"mode\": \"published\",\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "markdown", + "id": "a2edbf18", + "metadata": {}, + "source": [ + "# Link Columns to Dimension Nodes\n", + "\n", + "Dimensions are discovered through labels on node columns throughout the DJ DAG. These labels link these columns to the primary key(s) of the dimension node. Create links to all columns in the DJ DAG to their corresponding dimension nodes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2bf208cc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.repair_order_details/columns/repair_order_id/?dimension=default.repair_order&dimension_column=repair_order_id\"\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5237f6f3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.repair_type/columns/contractor_id/?dimension=default.contractor&dimension_column=contractor_id\"\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab23f28c-3a97-4159-bcee-589b03953d4b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.repair_orders/columns/repair_order_id/?dimension=default.repair_order&dimension_column=repair_order_id\"\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab3ffde2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.local_hard_hats/columns/state_id/?dimension=default.us_state&dimension_column=state_abbr\"\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bad74cd5-d65d-406a-b458-ac6c8bd8b3d2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.repair_order_details/columns/repair_order_id/?dimension=default.repair_order&dimension_column=repair_order_id\"\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2da6f2e-2317-40b5-be29-b05dd9c09652", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.repair_order/columns/dispatcher_id/?dimension=default.dispatcher&dimension_column=dispatcher_id\"\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6bcbfc56-c53c-4ecf-aff5-38c8833b2a4c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.repair_order/columns/hard_hat_id/?dimension=default.hard_hat&dimension_column=hard_hat_id\"\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db13b4f6-2966-4fdb-a716-4688d5e4ec81", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.repair_order/columns/municipality_id/?dimension=default.municipality_dim&dimension_column=municipality_id\"\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b57c9e38-4bf4-492f-8c52-dec7d7191784", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.hard_hat/columns/state/?dimension=default.us_state&dimension_column=state_abbr\"\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46e12891-6f53-4a27-a446-4ca103018a5c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.hard_hat/columns/birth_date/?dimension=default.date_dim&dimension_column=dateint\"\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6808fdae-eadf-4da3-bc61-43b0271b0af7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.hard_hat/columns/hire_date/?dimension=default.date_dim&dimension_column=dateint\"\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "markdown", + "id": "c7c11045", + "metadata": {}, + "source": [ + "# View All Existing Nodes\n", + "\n", + "Let's look at the full list of nodes that are now in the DJ system and can be used to generate queries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3f61edb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "nodes = session.get(f\"{DJ_URL}/nodes/\").json()\n", + "for node in nodes:\n", + " print(node)" + ] + }, + { + "cell_type": "markdown", + "id": "b94aede1", + "metadata": {}, + "source": [ + "# Generate SQL Using Metrics & Dimensions\n", + "\n", + "You can now generate SQL queries for any metric and include any of the discoverable dimensions that you'd like to group it by. Let's list out the dimensions that are available for each metric in DJ." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccb90f88", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "for metric in [\n", + " \"default.avg_length_of_employment\",\n", + " \"default.avg_repair_order_discounts\",\n", + " \"default.avg_repair_price\",\n", + " \"default.avg_time_to_dispatch\",\n", + " \"default.num_repair_orders\",\n", + " \"default.total_repair_cost\",\n", + " \"default.total_repair_order_discounts\",\n", + "]:\n", + " response = session.get(\n", + " f\"{DJ_URL}/metrics/{metric}/\",\n", + " )\n", + " metric_metadata = response.json()\n", + " print(metric)\n", + " print(\"---\")\n", + " for dimension in metric_metadata[\"dimensions\"]:\n", + " print(dimension)\n", + " print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3911d09-ddf1-4965-8142-f8c957817288", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from datajunction import DJBuilder\n", + "dj = DJBuilder(DJ_URL, requests_session=session)\n", + "\n", + "repair_orders_cube = dj.create_cube(\n", + " name=\"default.repair_orders_cube\",\n", + " description=\"Repair Orders Cube\",\n", + " metrics=[\n", + " \"default.num_repair_orders\",\n", + " \"default.avg_repair_price\",\n", + " \"default.total_repair_cost\",\n", + " \"default.total_repair_order_discounts\",\n", + " ],\n", + " dimensions=[\n", + " \"default.hard_hat.state\",\n", + " \"default.dispatcher.company_name\",\n", + " ],\n", + " filters=[],\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "markdown", + "id": "f949edf8", + "metadata": {}, + "source": [ + "### Using the metric SQL endpoint, generate some SQL for a few metrics, grouped by dimensions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22f78085", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.get(\n", + " f\"{DJ_URL}/sql/default.num_repair_orders/?dimensions=default.hard_hat.first_name&dimensions=default.hard_hat.last_name\",\n", + ")\n", + "print(response.json()[\"sql\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "960b072c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.get(\n", + " f\"{DJ_URL}/sql/default.total_repair_order_discounts/?dimensions=default.repair_order.hard_hat_id\",\n", + ")\n", + "print(response.json()[\"sql\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8fea581", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.get(\n", + " f\"{DJ_URL}/sql/default.avg_repair_price/?dimensions=default.hard_hat.city\",\n", + ")\n", + "print(response.json()[\"sql\"])" + ] + }, + { + "cell_type": "markdown", + "id": "376099e1-5e5f-42c6-8173-0d20365c8515", + "metadata": {}, + "source": [ + "# Add Materialization" + ] + }, + { + "cell_type": "raw", + "id": "0d8ea258-f188-41e8-adad-07d781445384", + "metadata": { + "tags": [] + }, + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.repair_order_transform/materialization/\",\n", + " json={\n", + " \"engine\": {\n", + " \"name\": \"duckdb\",\n", + " \"version\": \"0.7.1\"\n", + " },\n", + " \"schedule\": \"0 * * * *\",\n", + " \"config\": {\n", + " \"partitions\": [{\n", + " \"name\": \"municipality_id\",\n", + " \"type_\": \"categorical\",\n", + " }]\n", + " }\n", + " }\n", + ")\n", + "print(response.json())\n" + ] + }, + { + "cell_type": "raw", + "id": "77b3739f-3450-46c0-a871-ac8f23df78d3", + "metadata": {}, + "source": [ + "response = session.post(\n", + " f\"{DJ_URL}/nodes/default.repair_order_transform/materialization/\",\n", + " json={\n", + " \"engine\": {\n", + " \"name\": \"duckdb\",\n", + " \"version\": \"0.7.1\"\n", + " },\n", + " \"schedule\": \"0 * * * *\",\n", + " \"config\": {\n", + " \"partitions\": [{\n", + " \"name\": \"municipality_id\",\n", + " \"type_\": \"categorical\",\n", + " }]\n", + " }\n", + " }\n", + ")\n", + "print(response.json())\n" + ] + }, + { + "cell_type": "markdown", + "id": "9d79622a", + "metadata": {}, + "source": [ + "# Report Completed Materializations by Setting Availability on a Node" + ] + }, + { + "cell_type": "markdown", + "id": "d65c0337", + "metadata": {}, + "source": [ + "Materializations can be reported by adding an `availability` to a node. When DJ builds the query, it will use any availability states it finds. Let's add an availability to the `repair_order` dimension node." + ] + }, + { + "cell_type": "markdown", + "id": "df39c4dd-3b0d-42e5-be93-b0ff4489ac24", + "metadata": { + "tags": [] + }, + "source": [ + "## Add an availability for the contractor dimension node\n", + "```py\n", + "response = session.post(\n", + " f\"{DJ_URL}/data/default.repair_order/availability/\",\n", + " json={\n", + " \"catalog\": \"warehouse\",\n", + " \"schema_\": \"roads\",\n", + " \"table\": \"repair_order_materialization_123\",\n", + " \"valid_through_ts\": 20230306,\n", + " \"max_partition\": [\"20230305\"],\n", + " \"min_partition\": [\"20230304\"],\n", + " },\n", + ")\n", + "response.json()\n", + "```\n", + "## Now when you generate the SQL, the availability will be used instead of building the query to the source node\n", + "```py\n", + "response = session.get(\n", + " f\"{DJ_URL}/sql/default.avg_repair_price/?dimensions=default.repair_order.municipality_id\",\n", + ")\n", + "print(response.json()[\"sql\"])\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "5760cfc8-967f-4a8a-8539-cc06e21aa042", + "metadata": { + "tags": [] + }, + "source": [ + "# Requesting Data for Metrics and Dimensions" + ] + }, + { + "cell_type": "markdown", + "id": "09cca0fe-1653-4fe8-b7c2-29727842911e", + "metadata": { + "tags": [] + }, + "source": [ + "### In addition to requesting SQL, you can also request data for a specific set of metrics and dimensions. This will generate and execute the query and return the results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d28d942-e41f-4da4-89d7-a6eb62d0d145", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import pandas\n", + "\n", + "def to_dataframe(results):\n", + " results = results[\"results\"][0]\n", + " return pandas.DataFrame(\n", + " results[\"rows\"],\n", + " columns=[\n", + " col[\"semantic_entity\"] if col[\"semantic_type\"] == \"dimension\" else col[\"node\"]\n", + " for col in results[\"columns\"]\n", + " ],\n", + " )\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14a5d5ac-1417-4e59-9438-ddad3b2d4245", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.get(\n", + " f\"{DJ_URL}/data/default.num_repair_orders/?dimensions=default.hard_hat.first_name&dimensions=default.hard_hat.last_name\",\n", + ")\n", + "to_dataframe(response.json())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1978c7c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.get(\n", + " f\"{DJ_URL}/data/default.total_repair_order_discounts/?dimensions=default.repair_order.hard_hat_id\",\n", + ")\n", + "to_dataframe(response.json())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f141414", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.get(\n", + " f\"{DJ_URL}/data/default.avg_time_to_dispatch/?dimensions=default.hard_hat.state\",\n", + ")\n", + "to_dataframe(response.json())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dafd0bf7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "response = session.get(\n", + " f\"{DJ_URL}/data/default.avg_repair_price/?dimensions=default.repair_order.municipality_id\",\n", + ")\n", + "to_dataframe(response.json())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f447632d-0f3b-43ff-b4f6-2cd35328a232", + "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.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/example_project/avg_length_of_employment.metric.yaml b/notebooks/example_project/avg_length_of_employment.metric.yaml new file mode 100644 index 000000000..a3ac3dbca --- /dev/null +++ b/notebooks/example_project/avg_length_of_employment.metric.yaml @@ -0,0 +1,3 @@ +description: Average length of employment +display_name: 'Default: Avg Length Of Employment' +query: SELECT avg(NOW() - hire_date) FROM ${prefix}hard_hats diff --git a/notebooks/example_project/avg_repair_order_discounts.metric.yaml b/notebooks/example_project/avg_repair_order_discounts.metric.yaml new file mode 100644 index 000000000..6c022557a --- /dev/null +++ b/notebooks/example_project/avg_repair_order_discounts.metric.yaml @@ -0,0 +1,3 @@ +description: Total repair order discounts +display_name: 'Default: Avg Repair Order Discounts' +query: SELECT avg(price * discount) FROM ${prefix}repair_order_details diff --git a/notebooks/example_project/avg_repair_price.metric.yaml b/notebooks/example_project/avg_repair_price.metric.yaml new file mode 100644 index 000000000..30728ec93 --- /dev/null +++ b/notebooks/example_project/avg_repair_price.metric.yaml @@ -0,0 +1,3 @@ +description: Average repair price +display_name: 'Default: Avg Repair Price' +query: SELECT avg(price) FROM ${prefix}repair_order_details diff --git a/notebooks/example_project/avg_time_to_dispatch.metric.yaml b/notebooks/example_project/avg_time_to_dispatch.metric.yaml new file mode 100644 index 000000000..2feb9ead0 --- /dev/null +++ b/notebooks/example_project/avg_time_to_dispatch.metric.yaml @@ -0,0 +1,3 @@ +description: Average time to dispatch a repair order +display_name: 'Default: Avg Time To Dispatch' +query: SELECT avg(dispatched_date - order_date) FROM ${prefix}repair_orders diff --git a/notebooks/example_project/contractor.dimension.yaml b/notebooks/example_project/contractor.dimension.yaml new file mode 100644 index 000000000..6da2ec867 --- /dev/null +++ b/notebooks/example_project/contractor.dimension.yaml @@ -0,0 +1,18 @@ +description: Contractor dimension +dimension_links: {} +display_name: 'Default: Contractor' +primary_key: +- contractor_id +query: |2- + + SELECT + contractor_id, + company_name, + contact_name, + contact_title, + address, + city, + state, + postal_code, + country + FROM ${prefix}contractors diff --git a/notebooks/example_project/contractors.source.yaml b/notebooks/example_project/contractors.source.yaml new file mode 100644 index 000000000..4f7f27820 --- /dev/null +++ b/notebooks/example_project/contractors.source.yaml @@ -0,0 +1,23 @@ +columns: +- name: contact_title + type: string +- name: city + type: string +- name: postal_code + type: string +- name: contractor_id + type: int +- name: country + type: string +- name: address + type: string +- name: company_name + type: string +- name: state + type: string +- name: contact_name + type: string +description: Information on different types of repairs +dimension_links: {} +display_name: warehouse.roads.contractors +table: warehouse.roads.contractors diff --git a/notebooks/example_project/date.source.yaml b/notebooks/example_project/date.source.yaml new file mode 100644 index 000000000..1fa6abf93 --- /dev/null +++ b/notebooks/example_project/date.source.yaml @@ -0,0 +1,13 @@ +columns: +- name: day + type: int +- name: dateint + type: timestamp +- name: year + type: int +- name: month + type: int +description: Date dimension +dimension_links: {} +display_name: warehouse.roads.date +table: warehouse.roads.date diff --git a/notebooks/example_project/date_dim.dimension.yaml b/notebooks/example_project/date_dim.dimension.yaml new file mode 100644 index 000000000..168b7fae3 --- /dev/null +++ b/notebooks/example_project/date_dim.dimension.yaml @@ -0,0 +1,13 @@ +description: Date dimension +dimension_links: {} +display_name: 'Default: Date Dim' +primary_key: +- dateint +query: |2- + + SELECT + dateint, + month, + year, + day + FROM ${prefix}date diff --git a/notebooks/example_project/dispatcher.dimension.yaml b/notebooks/example_project/dispatcher.dimension.yaml new file mode 100644 index 000000000..3b9d838e8 --- /dev/null +++ b/notebooks/example_project/dispatcher.dimension.yaml @@ -0,0 +1,12 @@ +description: Dispatcher dimension +dimension_links: {} +display_name: 'Default: Dispatcher' +primary_key: +- dispatcher_id +query: |2- + + SELECT + dispatcher_id, + company_name, + phone + FROM ${prefix}dispatchers diff --git a/notebooks/example_project/dispatchers.source.yaml b/notebooks/example_project/dispatchers.source.yaml new file mode 100644 index 000000000..e265ddef2 --- /dev/null +++ b/notebooks/example_project/dispatchers.source.yaml @@ -0,0 +1,11 @@ +columns: +- name: dispatcher_id + type: int +- name: phone + type: string +- name: company_name + type: string +description: Information on different types of repairs +dimension_links: {} +display_name: warehouse.roads.dispatchers +table: warehouse.roads.dispatchers diff --git a/notebooks/example_project/dj.yaml b/notebooks/example_project/dj.yaml new file mode 100644 index 000000000..d56ead76a --- /dev/null +++ b/notebooks/example_project/dj.yaml @@ -0,0 +1,3 @@ +description: This is an autogenerated project for namespace default +name: Project default (Autogenerated) +prefix: default diff --git a/notebooks/example_project/hard_hat.dimension.yaml b/notebooks/example_project/hard_hat.dimension.yaml new file mode 100644 index 000000000..622fbb800 --- /dev/null +++ b/notebooks/example_project/hard_hat.dimension.yaml @@ -0,0 +1,28 @@ +description: Hard hat dimension +dimension_links: + birth_date: + dimension: ${prefix}date_dim + hire_date: + dimension: ${prefix}date_dim + state: + dimension: ${prefix}us_state +display_name: 'Default: Hard Hat' +primary_key: +- hard_hat_id +query: |2- + + SELECT + hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id + FROM ${prefix}hard_hats diff --git a/notebooks/example_project/hard_hat_state.source.yaml b/notebooks/example_project/hard_hat_state.source.yaml new file mode 100644 index 000000000..ab0228092 --- /dev/null +++ b/notebooks/example_project/hard_hat_state.source.yaml @@ -0,0 +1,9 @@ +columns: +- name: hard_hat_id + type: int +- name: state_id + type: string +description: Information on different types of repairs +dimension_links: {} +display_name: warehouse.roads.hard_hat_state +table: warehouse.roads.hard_hat_state diff --git a/notebooks/example_project/hard_hats.source.yaml b/notebooks/example_project/hard_hats.source.yaml new file mode 100644 index 000000000..b7c4f1d9f --- /dev/null +++ b/notebooks/example_project/hard_hats.source.yaml @@ -0,0 +1,31 @@ +columns: +- name: title + type: string +- name: address + type: string +- name: postal_code + type: string +- name: state + type: string +- name: city + type: string +- name: country + type: string +- name: hard_hat_id + type: int +- name: last_name + type: string +- name: birth_date + type: date +- name: first_name + type: string +- name: contractor_id + type: int +- name: manager + type: int +- name: hire_date + type: date +description: Information on different types of repairs +dimension_links: {} +display_name: warehouse.roads.hard_hats +table: warehouse.roads.hard_hats diff --git a/notebooks/example_project/local_hard_hats.dimension.yaml b/notebooks/example_project/local_hard_hats.dimension.yaml new file mode 100644 index 000000000..2d5442344 --- /dev/null +++ b/notebooks/example_project/local_hard_hats.dimension.yaml @@ -0,0 +1,28 @@ +description: Hard hat dimension +dimension_links: + state_id: + dimension: ${prefix}us_state +display_name: 'Default: Local Hard Hats' +primary_key: +- hard_hat_id +query: |2- + + SELECT + hh.hard_hat_id, + last_name, + first_name, + title, + birth_date, + hire_date, + address, + city, + state, + postal_code, + country, + manager, + contractor_id, + hhs.state_id AS state_id + FROM ${prefix}hard_hats hh + LEFT JOIN ${prefix}hard_hat_state hhs + ON hh.hard_hat_id = hhs.hard_hat_id + WHERE hh.state_id = 'NY' diff --git a/notebooks/example_project/municipality.source.yaml b/notebooks/example_project/municipality.source.yaml new file mode 100644 index 000000000..bbbbd4e31 --- /dev/null +++ b/notebooks/example_project/municipality.source.yaml @@ -0,0 +1,17 @@ +columns: +- name: local_region + type: string +- name: municipality_id + type: string +- name: state_id + type: int +- name: contact_name + type: string +- name: phone + type: string +- name: contact_title + type: string +description: Information on different types of repairs +dimension_links: {} +display_name: warehouse.roads.municipality +table: warehouse.roads.municipality diff --git a/notebooks/example_project/municipality_dim.dimension.yaml b/notebooks/example_project/municipality_dim.dimension.yaml new file mode 100644 index 000000000..717d3384e --- /dev/null +++ b/notebooks/example_project/municipality_dim.dimension.yaml @@ -0,0 +1,20 @@ +description: Municipality dimension +dimension_links: {} +display_name: 'Default: Municipality Dim' +primary_key: +- municipality_id +query: |2- + + SELECT + m.municipality_id AS municipality_id, + contact_name, + contact_title, + local_region, + state_id, + mmt.municipality_type_id AS municipality_type_id, + mt.municipality_type_desc AS municipality_type_desc + FROM ${prefix}municipality AS m + LEFT JOIN ${prefix}municipality_municipality_type AS mmt + ON m.municipality_id = mmt.municipality_id + LEFT JOIN ${prefix}municipality_type AS mt + ON mmt.municipality_type_id = mt.municipality_type_desc diff --git a/notebooks/example_project/municipality_municipality_type.source.yaml b/notebooks/example_project/municipality_municipality_type.source.yaml new file mode 100644 index 000000000..3d5699d0c --- /dev/null +++ b/notebooks/example_project/municipality_municipality_type.source.yaml @@ -0,0 +1,9 @@ +columns: +- name: municipality_id + type: string +- name: municipality_type_id + type: string +description: Information on different types of repairs +dimension_links: {} +display_name: warehouse.roads.municipality_municipality_type +table: warehouse.roads.municipality_municipality_type diff --git a/notebooks/example_project/municipality_type.source.yaml b/notebooks/example_project/municipality_type.source.yaml new file mode 100644 index 000000000..eac737c8e --- /dev/null +++ b/notebooks/example_project/municipality_type.source.yaml @@ -0,0 +1,9 @@ +columns: +- name: municipality_type_desc + type: string +- name: municipality_type_id + type: string +description: Information on different types of repairs +dimension_links: {} +display_name: warehouse.roads.municipality_type +table: warehouse.roads.municipality_type diff --git a/notebooks/example_project/national_level_agg.transform.yaml b/notebooks/example_project/national_level_agg.transform.yaml new file mode 100644 index 000000000..ddd33ac9c --- /dev/null +++ b/notebooks/example_project/national_level_agg.transform.yaml @@ -0,0 +1,5 @@ +description: National level aggregates +dimension_links: {} +display_name: 'Default: National Level Agg' +query: SELECT SUM(rd.price * rd.quantity) AS total_amount_nationwide FROM ${prefix}repair_order_details + rd diff --git a/notebooks/example_project/num_repair_orders.metric.yaml b/notebooks/example_project/num_repair_orders.metric.yaml new file mode 100644 index 000000000..d29841f46 --- /dev/null +++ b/notebooks/example_project/num_repair_orders.metric.yaml @@ -0,0 +1,3 @@ +description: Number of repair orders +display_name: 'Default: Num Repair Orders' +query: SELECT count(repair_order_id) FROM ${prefix}repair_orders diff --git a/notebooks/example_project/regional_level_agg.transform.yaml b/notebooks/example_project/regional_level_agg.transform.yaml new file mode 100644 index 000000000..1c1fbf214 --- /dev/null +++ b/notebooks/example_project/regional_level_agg.transform.yaml @@ -0,0 +1,49 @@ +description: Regional-level aggregates +dimension_links: {} +display_name: 'Default: Regional Level Agg' +query: |- + SELECT + usr.us_region_id, + us.state_name, + CONCAT(us.state_name, '-', usr.us_region_description) AS location_hierarchy, + EXTRACT(YEAR FROM ro.order_date) AS order_year, + EXTRACT(MONTH FROM ro.order_date) AS order_month, + EXTRACT(DAY FROM ro.order_date) AS order_day, + COUNT(DISTINCT CASE WHEN ro.dispatched_date IS NOT NULL THEN ro.repair_order_id ELSE NULL END) AS completed_repairs, + COUNT(DISTINCT ro.repair_order_id) AS total_repairs_dispatched, + SUM(rd.price * rd.quantity) AS total_amount_in_region, + AVG(rd.price * rd.quantity) AS avg_repair_amount_in_region, + -- ELEMENT_AT(ARRAY_SORT(COLLECT_LIST(STRUCT(COUNT(*) AS cnt, rt.repair_type_name AS repair_type_name)), (left, right) -> case when left.cnt < right.cnt then 1 when left.cnt > right.cnt then -1 else 0 end), 0).repair_type_name AS most_common_repair_type, + AVG(DATEDIFF(ro.dispatched_date, ro.order_date)) AS avg_dispatch_delay, + COUNT(DISTINCT c.contractor_id) AS unique_contractors + FROM + (SELECT + repair_order_id, + municipality_id, + hard_hat_id, + order_date, + required_date, + dispatched_date, + dispatcher_id + FROM ${prefix}repair_orders) ro + JOIN + ${prefix}municipality m ON ro.municipality_id = m.municipality_id + JOIN + ${prefix}us_states us ON m.state_id = us.state_id + AND AVG(rd.price * rd.quantity) > + (SELECT AVG(price * quantity) FROM ${prefix}repair_order_details WHERE repair_order_id = ro.repair_order_id) + JOIN + ${prefix}us_states us ON m.state_id = us.state_id + JOIN + ${prefix}us_region usr ON us.state_region = usr.us_region_id + JOIN + ${prefix}repair_order_details rd ON ro.repair_order_id = rd.repair_order_id + JOIN + ${prefix}repair_type rt ON rd.repair_type_id = rt.repair_type_id + JOIN + ${prefix}contractors c ON rt.contractor_id = c.contractor_id + GROUP BY + usr.us_region_id, + EXTRACT(YEAR FROM ro.order_date), + EXTRACT(MONTH FROM ro.order_date), + EXTRACT(DAY FROM ro.order_date) diff --git a/notebooks/example_project/regional_repair_efficiency.metric.yaml b/notebooks/example_project/regional_repair_efficiency.metric.yaml new file mode 100644 index 000000000..834cf9be1 --- /dev/null +++ b/notebooks/example_project/regional_repair_efficiency.metric.yaml @@ -0,0 +1,17 @@ +description: "For each US region (as defined in the us_region table), we want to calculate:\n\ + \ Regional Repair Efficiency = (Number of Completed Repairs / Total Repairs\ + \ Dispatched) \xD7\n (Total Repair Amount in\ + \ Region / Total Repair Amount Nationwide) \xD7 100\n Here:\n \ + \ A \"Completed Repair\" is one where the dispatched_date is not null.\n \ + \ \"Total Repair Amount in Region\" is the total amount spent on repairs in\ + \ a given region.\n \"Total Repair Amount Nationwide\" is the total amount\ + \ spent on all repairs nationwide." +display_name: 'Default: Regional Repair Efficiency' +query: |- + SELECT + (SUM(rm.completed_repairs) * 1.0 / SUM(rm.total_repairs_dispatched)) * + (SUM(rm.total_amount_in_region) * 1.0 / SUM(na.total_amount_nationwide)) * 100 + FROM + ${prefix}regional_level_agg rm + CROSS JOIN + ${prefix}national_level_agg na diff --git a/notebooks/example_project/repair_order.dimension.yaml b/notebooks/example_project/repair_order.dimension.yaml new file mode 100644 index 000000000..d097a4313 --- /dev/null +++ b/notebooks/example_project/repair_order.dimension.yaml @@ -0,0 +1,19 @@ +description: Repair order dimension +dimension_links: + dispatcher_id: + dimension: ${prefix}dispatcher + hard_hat_id: + dimension: ${prefix}hard_hat + municipality_id: + dimension: ${prefix}municipality_dim +display_name: 'Default: Repair Order' +primary_key: +- repair_order_id +query: |2- + + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + dispatcher_id + FROM ${prefix}repair_orders diff --git a/notebooks/example_project/repair_order_details.source.yaml b/notebooks/example_project/repair_order_details.source.yaml new file mode 100644 index 000000000..012c56af0 --- /dev/null +++ b/notebooks/example_project/repair_order_details.source.yaml @@ -0,0 +1,17 @@ +columns: +- name: repair_order_id + type: int +- name: discount + type: float +- name: price + type: float +- name: quantity + type: int +- name: repair_type_id + type: int +description: Details on repair orders +dimension_links: + repair_order_id: + dimension: ${prefix}repair_order +display_name: warehouse.roads.repair_order_details +table: warehouse.roads.repair_order_details diff --git a/notebooks/example_project/repair_order_transform.transform.yaml b/notebooks/example_project/repair_order_transform.transform.yaml new file mode 100644 index 000000000..6cf7fe2ba --- /dev/null +++ b/notebooks/example_project/repair_order_transform.transform.yaml @@ -0,0 +1,11 @@ +description: Repair order dimension +dimension_links: {} +display_name: 'Default: Repair Order Transform' +query: |2- + + SELECT + repair_order_id, + municipality_id, + hard_hat_id, + dispatcher_id + FROM ${prefix}repair_orders diff --git a/notebooks/example_project/repair_orders.source.yaml b/notebooks/example_project/repair_orders.source.yaml new file mode 100644 index 000000000..f1027af2f --- /dev/null +++ b/notebooks/example_project/repair_orders.source.yaml @@ -0,0 +1,21 @@ +columns: +- name: required_date + type: date +- name: order_date + type: date +- name: repair_order_id + type: int +- name: hard_hat_id + type: int +- name: municipality_id + type: string +- name: dispatcher_id + type: int +- name: dispatched_date + type: date +description: Repair orders +dimension_links: + repair_order_id: + dimension: ${prefix}repair_order +display_name: warehouse.roads.repair_orders +table: warehouse.roads.repair_orders diff --git a/notebooks/example_project/repair_orders_cube.cube.yaml b/notebooks/example_project/repair_orders_cube.cube.yaml new file mode 100644 index 000000000..72fd1cace --- /dev/null +++ b/notebooks/example_project/repair_orders_cube.cube.yaml @@ -0,0 +1,10 @@ +description: Repair Orders Cube +dimensions: +- ${prefix}dispatcher.company_name +- ${prefix}hard_hat.state +display_name: 'Default: Repair Orders Cube' +metrics: +- ${prefix}total_repair_cost +- ${prefix}avg_repair_price +- ${prefix}num_repair_orders +- ${prefix}total_repair_order_discounts diff --git a/notebooks/example_project/repair_type.source.yaml b/notebooks/example_project/repair_type.source.yaml new file mode 100644 index 000000000..131993d98 --- /dev/null +++ b/notebooks/example_project/repair_type.source.yaml @@ -0,0 +1,13 @@ +columns: +- name: repair_type_name + type: string +- name: repair_type_id + type: int +- name: contractor_id + type: int +description: Information on different types of repairs +dimension_links: + contractor_id: + dimension: ${prefix}contractor +display_name: warehouse.roads.repair_type +table: warehouse.roads.repair_type diff --git a/notebooks/example_project/total_repair_cost.metric.yaml b/notebooks/example_project/total_repair_cost.metric.yaml new file mode 100644 index 000000000..65ae86b7e --- /dev/null +++ b/notebooks/example_project/total_repair_cost.metric.yaml @@ -0,0 +1,3 @@ +description: Total repair cost +display_name: 'Default: Total Repair Cost' +query: SELECT sum(price) FROM ${prefix}repair_order_details diff --git a/notebooks/example_project/total_repair_order_discounts.metric.yaml b/notebooks/example_project/total_repair_order_discounts.metric.yaml new file mode 100644 index 000000000..07138a32a --- /dev/null +++ b/notebooks/example_project/total_repair_order_discounts.metric.yaml @@ -0,0 +1,3 @@ +description: Total repair order discounts +display_name: 'Default: Total Repair Order Discounts' +query: SELECT sum(price * discount) FROM ${prefix}repair_order_details diff --git a/notebooks/example_project/us_region.source.yaml b/notebooks/example_project/us_region.source.yaml new file mode 100644 index 000000000..88e00022d --- /dev/null +++ b/notebooks/example_project/us_region.source.yaml @@ -0,0 +1,9 @@ +columns: +- name: us_region_description + type: string +- name: us_region_id + type: int +description: Information on different types of repairs +dimension_links: {} +display_name: warehouse.roads.us_region +table: warehouse.roads.us_region diff --git a/notebooks/example_project/us_state.dimension.yaml b/notebooks/example_project/us_state.dimension.yaml new file mode 100644 index 000000000..bb40c9fef --- /dev/null +++ b/notebooks/example_project/us_state.dimension.yaml @@ -0,0 +1,16 @@ +description: US state dimension +dimension_links: {} +display_name: 'Default: Us State' +primary_key: +- state_id +query: |2- + + SELECT + state_id, + state_name, + state_abbr, + state_region, + r.us_region_description AS state_region_description + FROM ${prefix}us_states s + LEFT JOIN ${prefix}us_region r + ON s.state_region = r.us_region_description diff --git a/notebooks/example_project/us_states.source.yaml b/notebooks/example_project/us_states.source.yaml new file mode 100644 index 000000000..c65595ec8 --- /dev/null +++ b/notebooks/example_project/us_states.source.yaml @@ -0,0 +1,13 @@ +columns: +- name: state_id + type: int +- name: state_region + type: int +- name: state_name + type: string +- name: state_abbr + type: string +description: Information on different types of repairs +dimension_links: {} +display_name: warehouse.roads.us_states +table: warehouse.roads.us_states diff --git a/notebooks/images/dj-roads-erd.jpg b/notebooks/images/dj-roads-erd.jpg new file mode 100644 index 000000000..b4a7355c8 Binary files /dev/null and b/notebooks/images/dj-roads-erd.jpg differ diff --git a/openapi.json b/openapi.json new file mode 100644 index 000000000..f83e7a20c --- /dev/null +++ b/openapi.json @@ -0,0 +1,8557 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "DJ server", + "description": "A DataJunction metrics layer", + "license": { + "name": "MIT License", + "url": "https://mit-license.org/" + }, + "version": "0.0.1a44" + }, + "paths": { + "/catalogs": { + "get": { + "tags": [ + "catalogs" + ], + "summary": "List Catalogs", + "description": "List all available catalogs", + "operationId": "list_catalogs_catalogs_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/CatalogInfo" + }, + "type": "array", + "title": "Response List Catalogs Catalogs Get" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "post": { + "tags": [ + "catalogs" + ], + "summary": "Add A Catalog", + "description": "Add a Catalog", + "operationId": "Add_A_Catalog_catalogs_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogInfo" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogInfo" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/catalogs/{name}": { + "get": { + "tags": [ + "catalogs" + ], + "summary": "Get A Catalog", + "description": "Return a catalog by name", + "operationId": "Get_a_Catalog_catalogs__name__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogInfo" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/catalogs/{name}/engines": { + "post": { + "tags": [ + "catalogs" + ], + "summary": "Add Engines To A Catalog", + "description": "Attach one or more engines to a catalog", + "operationId": "Add_Engines_to_a_Catalog_catalogs__name__engines_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/EngineInfo" + }, + "type": "array", + "title": "Data" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogInfo" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/engines": { + "get": { + "tags": [ + "engines" + ], + "summary": "List Engines", + "description": "List all available engines", + "operationId": "list_engines_engines_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/EngineInfo" + }, + "type": "array", + "title": "Response List Engines Engines Get" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "post": { + "tags": [ + "engines" + ], + "summary": "Add An Engine", + "description": "Add a new engine", + "operationId": "Add_An_Engine_engines_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EngineInfo" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EngineInfo" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/engines/{name}/{version}": { + "get": { + "tags": [ + "engines" + ], + "summary": "Get An Engine", + "description": "Return an engine by name and version", + "operationId": "get_an_engine_engines__name___version__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Version" + }, + "name": "version", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EngineInfo" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/metrics": { + "get": { + "tags": [ + "metrics" + ], + "summary": "List Metrics", + "description": "List all available metrics.", + "operationId": "list_metrics_metrics_get", + "parameters": [ + { + "required": false, + "schema": { + "type": "string", + "title": "Prefix" + }, + "name": "prefix", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Response List Metrics Metrics Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/metrics/metadata": { + "get": { + "tags": [ + "metrics" + ], + "summary": "List Metric Metadata", + "description": "Return available metric metadata attributes", + "operationId": "list_metric_metadata_metrics_metadata_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetricMetadataOptions" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/metrics/{name}": { + "get": { + "tags": [ + "metrics" + ], + "summary": "Get A Metric", + "description": "Return a metric by name.", + "operationId": "get_a_metric_metrics__name__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Metric" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/metrics/common/dimensions": { + "get": { + "tags": [ + "metrics" + ], + "summary": "Get Common Dimensions", + "description": "Return common dimensions for a set of metrics.", + "operationId": "get_common_dimensions_metrics_common_dimensions_get", + "parameters": [ + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "List of metrics to find common dimensions for", + "default": [] + }, + "name": "metric", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/DimensionAttributeOutput" + }, + "type": "array", + "title": "Response Get Common Dimensions Metrics Common Dimensions Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/djsql/data": { + "get": { + "tags": [ + "DJSQL" + ], + "summary": "Get Data For Djsql", + "description": "Return data for a DJ SQL query", + "operationId": "get_data_for_djsql_djsql_data_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Query" + }, + "name": "query", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "boolean", + "title": "Async ", + "default": false + }, + "name": "async_", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Name" + }, + "name": "engine_name", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Version" + }, + "name": "engine_version", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryWithResults" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/djsql/stream": { + "get": { + "tags": [ + "DJSQL" + ], + "summary": "Get Data Stream For Djsql", + "description": "Return data for a DJ SQL query using server side events", + "operationId": "get_data_stream_for_djsql_djsql_stream_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Query" + }, + "name": "query", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Name" + }, + "name": "engine_name", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Version" + }, + "name": "engine_version", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryWithResults" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/validate": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Validate Node", + "description": "Determines whether the provided node is valid and returns metadata from node validation.", + "operationId": "validate_node_nodes_validate_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeRevisionBase" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeValidation" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/validate": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Revalidate", + "description": "Revalidate a single existing node and update its status appropriately", + "operationId": "revalidate_nodes__name__validate_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeValidation" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{node_name}/columns/{column_name}/attributes": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Set Column Attributes", + "description": "Set column attributes for the node.", + "operationId": "set_column_attributes_nodes__node_name__columns__column_name__attributes_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Column Name" + }, + "name": "column_name", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AttributeTypeIdentifier" + }, + "type": "array", + "title": "Attributes" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/datajunction_server__models__node__ColumnOutput" + }, + "type": "array", + "title": "Response Set Column Attributes Nodes Node Name Columns Column Name Attributes Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes": { + "get": { + "tags": [ + "nodes" + ], + "summary": "List Nodes", + "description": "List the available nodes.", + "operationId": "list_nodes_nodes_get", + "parameters": [ + { + "required": false, + "schema": { + "$ref": "#/components/schemas/NodeType" + }, + "name": "node_type", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Prefix" + }, + "name": "prefix", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Response List Nodes Nodes Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/details": { + "get": { + "tags": [ + "nodes" + ], + "summary": "List All Nodes With Details", + "description": "List the available nodes.", + "operationId": "list_all_nodes_with_details_nodes_details_get", + "parameters": [ + { + "required": false, + "schema": { + "$ref": "#/components/schemas/NodeType" + }, + "name": "node_type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NodeIndexItem" + }, + "type": "array", + "title": "Response List All Nodes With Details Nodes Details Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}": { + "get": { + "tags": [ + "nodes" + ], + "summary": "Get Node", + "description": "Show the active version of the specified node.", + "operationId": "get_node_nodes__name__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "delete": { + "tags": [ + "nodes" + ], + "summary": "Delete Node", + "description": "Delete (aka deactivate) the specified node.", + "operationId": "delete_node_nodes__name__delete", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "patch": { + "tags": [ + "nodes" + ], + "summary": "Update Node", + "description": "Update a node.", + "operationId": "update_node_nodes__name__patch", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateNode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/hard": { + "delete": { + "tags": [ + "nodes" + ], + "summary": "Hard Delete A Dj Node", + "description": "Hard delete a node, destroying all links and invalidating all downstream nodes.\nThis should be used with caution, deactivating a node is preferred.", + "operationId": "Hard_Delete_a_DJ_Node_nodes__name__hard_delete", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/restore": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Restore Node", + "description": "Restore (aka re-activate) the specified node.", + "operationId": "restore_node_nodes__name__restore_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/revisions": { + "get": { + "tags": [ + "nodes" + ], + "summary": "List Node Revisions", + "description": "List all revisions for the node.", + "operationId": "list_node_revisions_nodes__name__revisions_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NodeRevisionOutput" + }, + "type": "array", + "title": "Response List Node Revisions Nodes Name Revisions Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/source": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Create A Source Node", + "description": "Create a source node. If columns are not provided, the source node's schema\nwill be inferred using the configured query service.", + "operationId": "Create_A_Source_Node_nodes_source_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSourceNode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/metric": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Create A Metric Node", + "description": "Create a node.", + "operationId": "Create_A_Metric_Node_nodes_metric_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNode" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/dimension": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Create A Dimension Node", + "description": "Create a node.", + "operationId": "Create_A_Dimension_Node_nodes_dimension_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNode" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/transform": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Create A Transform Node", + "description": "Create a node.", + "operationId": "Create_A_Transform_Node_nodes_transform_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNode" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/cube": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Create A Cube", + "description": "Create a cube node.", + "operationId": "Create_A_Cube_nodes_cube_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCubeNode" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/register/table/{catalog}/{schema_}/{table}": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Register Table", + "description": "Register a table. This creates a source node in the SOURCE_NODE_NAMESPACE and\nthe source node's schema will be inferred using the configured query service.", + "operationId": "register_table_register_table__catalog___schema____table__post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Catalog" + }, + "name": "catalog", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Schema " + }, + "name": "schema_", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Table" + }, + "name": "table", + "in": "path" + } + ], + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/columns/{column}": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Link Dimension", + "description": "Add information to a node column", + "operationId": "link_dimension_nodes__name__columns__column__post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Column" + }, + "name": "column", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Dimension" + }, + "name": "dimension", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Dimension Column" + }, + "name": "dimension_column", + "in": "query" + } + ], + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "delete": { + "tags": [ + "nodes" + ], + "summary": "Delete Dimension Link", + "description": "Remove the link between a node column and a dimension node", + "operationId": "delete_dimension_link_nodes__name__columns__column__delete", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Column" + }, + "name": "column", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Dimension" + }, + "name": "dimension", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Dimension Column" + }, + "name": "dimension_column", + "in": "query" + } + ], + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{node_name}/link": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Add Complex Dimension Link", + "description": "Links a source, dimension, or transform node to a dimension with a custom join query.\nIf a link already exists, updates the link definition.", + "operationId": "add_complex_dimension_link_nodes__node_name__link_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkDimensionInput" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "delete": { + "tags": [ + "nodes" + ], + "summary": "Remove Complex Dimension Link", + "description": "Removes a complex dimension link based on the dimension node and its role (if any).", + "operationId": "remove_complex_dimension_link_nodes__node_name__link_delete", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkDimensionIdentifier" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/migrate_dim_link": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Migrate Dimension Link", + "description": "Migrate dimension link from column-level to node-level", + "operationId": "migrate_dimension_link_nodes__name__migrate_dim_link_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/tags": { + "post": { + "tags": [ + "nodes", + "tags" + ], + "summary": "Update Tags On Node", + "description": "Add a tag to a node", + "operationId": "Update_Tags_on_Node_nodes__name__tags_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tag Names" + }, + "name": "tag_names", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/refresh": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Refresh Source Node", + "description": "Refresh a source node with the latest columns from the query service.", + "operationId": "refresh_source_node_nodes__name__refresh_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/similarity/{node1_name}/{node2_name}": { + "get": { + "tags": [ + "nodes" + ], + "summary": "Calculate Node Similarity", + "description": "Compare two nodes by how similar their queries are", + "operationId": "calculate_node_similarity_nodes_similarity__node1_name___node2_name__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node1 Name" + }, + "name": "node1_name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Node2 Name" + }, + "name": "node2_name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/downstream": { + "get": { + "tags": [ + "nodes" + ], + "summary": "List Downstream Nodes For A Node", + "description": "List all nodes that are downstream from the given node, filterable by type.", + "operationId": "List_Downstream_Nodes_For_A_Node_nodes__name__downstream_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + }, + { + "required": false, + "schema": { + "$ref": "#/components/schemas/NodeType" + }, + "name": "node_type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/DAGNodeOutput" + }, + "type": "array", + "title": "Response List Downstream Nodes For A Node Nodes Name Downstream Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/upstream": { + "get": { + "tags": [ + "nodes" + ], + "summary": "List Upstream Nodes For A Node", + "description": "List all nodes that are upstream from the given node, filterable by type.", + "operationId": "List_Upstream_Nodes_For_A_Node_nodes__name__upstream_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + }, + { + "required": false, + "schema": { + "$ref": "#/components/schemas/NodeType" + }, + "name": "node_type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/DAGNodeOutput" + }, + "type": "array", + "title": "Response List Upstream Nodes For A Node Nodes Name Upstream Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/dag": { + "get": { + "tags": [ + "nodes" + ], + "summary": "List All Connected Nodes (Upstreams + Downstreams)", + "description": "List all nodes that are part of the DAG of the given node. This means getting all upstreams,\ndownstreams, and linked dimension nodes.", + "operationId": "List_All_Connected_Nodes__Upstreams___Downstreams__nodes__name__dag_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/DAGNodeOutput" + }, + "type": "array", + "title": "Response List All Connected Nodes Upstreams Downstreams Nodes Name Dag Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/dimensions": { + "get": { + "tags": [ + "nodes" + ], + "summary": "List All Dimension Attributes", + "description": "List all available dimension attributes for the given node.", + "operationId": "List_All_Dimension_Attributes_nodes__name__dimensions_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/DimensionAttributeOutput" + }, + "type": "array", + "title": "Response List All Dimension Attributes Nodes Name Dimensions Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{name}/lineage": { + "get": { + "tags": [ + "nodes" + ], + "summary": "List Column Level Lineage Of Node", + "description": "List column-level lineage of a node in a graph", + "operationId": "List_column_level_lineage_of_node_nodes__name__lineage_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/LineageColumn" + }, + "type": "array", + "title": "Response List Column Level Lineage Of Node Nodes Name Lineage Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{node_name}/columns/{column_name}": { + "patch": { + "tags": [ + "nodes" + ], + "summary": "Set Column Display Name", + "description": "Set column name for the node", + "operationId": "set_column_display_name_nodes__node_name__columns__column_name__patch", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Column Name" + }, + "name": "column_name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Display Name" + }, + "name": "display_name", + "in": "query" + } + ], + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/datajunction_server__models__node__ColumnOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{node_name}/columns/{column_name}/partition": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Set Node Column As Partition", + "description": "Add or update partition columns for the specified node.", + "operationId": "Set_Node_Column_as_Partition_nodes__node_name__columns__column_name__partition_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Column Name" + }, + "name": "column_name", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PartitionInput" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/datajunction_server__models__node__ColumnOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{node_name}/copy": { + "post": { + "tags": [ + "nodes" + ], + "summary": "Copy A Node", + "description": "Copy this node to a new name.", + "operationId": "Copy_A_Node_nodes__node_name__copy_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "New Name" + }, + "name": "new_name", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DAGNodeOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/namespaces/{namespace}": { + "get": { + "tags": [ + "namespaces" + ], + "summary": "List Nodes In Namespace", + "description": "List node names in namespace, filterable to a given type if desired.", + "operationId": "list_nodes_in_namespace_namespaces__namespace__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Namespace" + }, + "name": "namespace", + "in": "path" + }, + { + "description": "Filter the list of nodes to this type", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/NodeType" + } + ], + "description": "Filter the list of nodes to this type" + }, + "name": "type_", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NodeMinimumDetail" + }, + "type": "array", + "title": "Response List Nodes In Namespace Namespaces Namespace Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "post": { + "tags": [ + "namespaces" + ], + "summary": "Create Node Namespace", + "description": "Create a node namespace", + "operationId": "create_node_namespace_namespaces__namespace__post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Namespace" + }, + "name": "namespace", + "in": "path" + }, + { + "required": false, + "schema": { + "type": "boolean", + "title": "Include Parents", + "default": false + }, + "name": "include_parents", + "in": "query" + } + ], + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "delete": { + "tags": [ + "namespaces" + ], + "summary": "Deactivate A Namespace", + "description": "Deactivates a node namespace", + "operationId": "deactivate_a_namespace_namespaces__namespace__delete", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Namespace" + }, + "name": "namespace", + "in": "path" + }, + { + "description": "Cascade the deletion down to the nodes in the namespace", + "required": false, + "schema": { + "type": "boolean", + "title": "Cascade", + "description": "Cascade the deletion down to the nodes in the namespace", + "default": false + }, + "name": "cascade", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/namespaces": { + "get": { + "tags": [ + "namespaces" + ], + "summary": "List Namespaces", + "description": "List namespaces with the number of nodes contained in them", + "operationId": "list_namespaces_namespaces_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NamespaceOutput" + }, + "type": "array", + "title": "Response List Namespaces Namespaces Get" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/namespaces/{namespace}/restore": { + "post": { + "tags": [ + "namespaces" + ], + "summary": "Restore A Namespace", + "description": "Restores a node namespace", + "operationId": "restore_a_namespace_namespaces__namespace__restore_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Namespace" + }, + "name": "namespace", + "in": "path" + }, + { + "description": "Cascade the restore down to the nodes in the namespace", + "required": false, + "schema": { + "type": "boolean", + "title": "Cascade", + "description": "Cascade the restore down to the nodes in the namespace", + "default": false + }, + "name": "cascade", + "in": "query" + } + ], + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/namespaces/{namespace}/hard": { + "delete": { + "tags": [ + "namespaces" + ], + "summary": "Hard Delete A Dj Namespace", + "description": "Hard delete a namespace, which will completely remove the namespace. Additionally,\nif any nodes are saved under this namespace, we'll hard delete the nodes if cascade\nis set to true. If cascade is set to false, we'll raise an error. This should be used\nwith caution, as the impact may be large.", + "operationId": "Hard_Delete_a_DJ_Namespace_namespaces__namespace__hard_delete", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Namespace" + }, + "name": "namespace", + "in": "path" + }, + { + "required": false, + "schema": { + "type": "boolean", + "title": "Cascade", + "default": false + }, + "name": "cascade", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/namespaces/{namespace}/export": { + "get": { + "tags": [ + "namespaces" + ], + "summary": "Export A Namespace As A Single Project'S Metadata", + "description": "Generates a zip of YAML files for the contents of the given namespace\nas well as a project definition file.", + "operationId": "Export_a_namespace_as_a_single_project_s_metadata_namespaces__namespace__export_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Namespace" + }, + "name": "namespace", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "type": "object" + }, + "type": "array", + "title": "Response Export A Namespace As A Single Project S Metadata Namespaces Namespace Export Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/materialization/info": { + "get": { + "tags": [ + "materializations" + ], + "summary": "Materialization Jobs Info", + "description": "Materialization job types and strategies", + "operationId": "Materialization_Jobs_Info_materialization_info_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{node_name}/materialization": { + "post": { + "tags": [ + "materializations" + ], + "summary": "Insert Or Update A Materialization For A Node", + "description": "Add or update a materialization of the specified node. If a node_name is specified\nfor the materialization config, it will always update that named config.", + "operationId": "Insert_or_Update_a_Materialization_for_a_Node_nodes__node_name__materialization_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpsertMaterialization" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{node_name}/materializations": { + "get": { + "tags": [ + "materializations" + ], + "summary": "List Materializations For A Node", + "description": "Show all materializations configured for the node, with any associated metadata\nlike urls from the materialization service, if available.", + "operationId": "List_Materializations_for_a_Node_nodes__node_name__materializations_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + }, + { + "required": false, + "schema": { + "type": "boolean", + "title": "Show Deleted", + "default": false + }, + "name": "show_deleted", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/MaterializationConfigInfoUnified" + }, + "type": "array", + "title": "Response List Materializations For A Node Nodes Node Name Materializations Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "delete": { + "tags": [ + "materializations" + ], + "summary": "Deactivate A Materialization For A Node", + "description": "Deactivate the node materialization with the provided name.\nAlso calls the query service to deactivate the associated scheduled jobs.", + "operationId": "Deactivate_a_Materialization_for_a_Node_nodes__node_name__materializations_delete", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Materialization Name" + }, + "name": "materialization_name", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/MaterializationConfigInfoUnified" + }, + "type": "array", + "title": "Response Deactivate A Materialization For A Node Nodes Node Name Materializations Delete" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/nodes/{node_name}/materializations/{materialization_name}/backfill": { + "post": { + "tags": [ + "materializations" + ], + "summary": "Kick Off A Backfill Run For A Configured Materialization", + "description": "Start a backfill for a configured materialization.", + "operationId": "Kick_off_a_backfill_run_for_a_configured_materialization_nodes__node_name__materializations__materialization_name__backfill_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Materialization Name" + }, + "name": "materialization_name", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PartitionBackfill" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MaterializationInfo" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/measures": { + "get": { + "tags": [ + "measures" + ], + "summary": "List Measures", + "description": "List all measures.", + "operationId": "list_measures_measures_get", + "parameters": [ + { + "required": false, + "schema": { + "type": "string", + "title": "Prefix" + }, + "name": "prefix", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Response List Measures Measures Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "post": { + "tags": [ + "measures" + ], + "summary": "Add A Measure", + "description": "Add a measure", + "operationId": "Add_a_Measure_measures_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMeasure" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeasureOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/measures/{measure_name}": { + "get": { + "tags": [ + "measures" + ], + "summary": "Get Measure", + "description": "Get info on a measure.", + "operationId": "get_measure_measures__measure_name__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Measure Name" + }, + "name": "measure_name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeasureOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "patch": { + "tags": [ + "measures" + ], + "summary": "Edit A Measure", + "description": "Edit a measure", + "operationId": "Edit_a_Measure_measures__measure_name__patch", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Measure Name" + }, + "name": "measure_name", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EditMeasure" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeasureOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/data/{node_name}/availability": { + "post": { + "tags": [ + "data" + ], + "summary": "Add Availability State To Node", + "description": "Add an availability state to a node.", + "operationId": "Add_Availability_State_to_Node_data__node_name__availability_post", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AvailabilityStateBase" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/data/{node_name}": { + "get": { + "tags": [ + "data" + ], + "summary": "Get Data For A Node", + "description": "Gets data for a node", + "operationId": "Get_Data_for_a_Node_data__node_name__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + }, + { + "description": "Dimensional attributes to group by", + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions", + "description": "Dimensional attributes to group by", + "default": [] + }, + "name": "dimensions", + "in": "query" + }, + { + "description": "Filters on dimensional attributes", + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Filters", + "description": "Filters on dimensional attributes", + "default": [] + }, + "name": "filters", + "in": "query" + }, + { + "description": "Expression to order by", + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Orderby", + "description": "Expression to order by", + "default": [] + }, + "name": "orderby", + "in": "query" + }, + { + "description": "Number of rows to limit the data retrieved to", + "required": false, + "schema": { + "type": "integer", + "title": "Limit", + "description": "Number of rows to limit the data retrieved to" + }, + "name": "limit", + "in": "query" + }, + { + "description": "Whether to run the query async or wait for results from the query engine", + "required": false, + "schema": { + "type": "boolean", + "title": "Async ", + "description": "Whether to run the query async or wait for results from the query engine", + "default": false + }, + "name": "async_", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Name" + }, + "name": "engine_name", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Version" + }, + "name": "engine_version", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryWithResults" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/data/query/{query_id}": { + "get": { + "tags": [ + "data" + ], + "summary": "Get Data For Query Id", + "description": "Return data for a specific query ID.", + "operationId": "Get_Data_For_Query_ID_data_query__query_id__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Query Id" + }, + "name": "query_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryWithResults" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/data": { + "get": { + "tags": [ + "data" + ], + "summary": "Get Data For Metrics", + "description": "Return data for a set of metrics with dimensions and filters", + "operationId": "Get_Data_For_Metrics_data_get", + "parameters": [ + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Metrics", + "default": [] + }, + "name": "metrics", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions", + "default": [] + }, + "name": "dimensions", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Filters", + "default": [] + }, + "name": "filters", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Orderby", + "default": [] + }, + "name": "orderby", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "integer", + "title": "Limit" + }, + "name": "limit", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "boolean", + "title": "Async ", + "default": false + }, + "name": "async_", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Name" + }, + "name": "engine_name", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Version" + }, + "name": "engine_version", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryWithResults" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/stream": { + "get": { + "tags": [ + "data" + ], + "summary": "Get Data Stream For Metrics", + "description": "Return data for a set of metrics with dimensions and filters using server side events", + "operationId": "get_data_stream_for_metrics_stream_get", + "parameters": [ + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Metrics", + "default": [] + }, + "name": "metrics", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions", + "default": [] + }, + "name": "dimensions", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Filters", + "default": [] + }, + "name": "filters", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Orderby", + "default": [] + }, + "name": "orderby", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "integer", + "title": "Limit" + }, + "name": "limit", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Name" + }, + "name": "engine_name", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Version" + }, + "name": "engine_version", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryWithResults" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/health/": { + "get": { + "tags": [ + "health" + ], + "summary": "Health Check", + "description": "Healthcheck for services.", + "operationId": "health_check_health__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/HealthCheck" + }, + "type": "array", + "title": "Response Health Check Health Get" + } + } + } + } + } + } + }, + "/history/{entity_type}/{entity_name}": { + "get": { + "tags": [ + "history" + ], + "summary": "List History", + "description": "List history for an entity type (i.e. Node) and entity name", + "operationId": "list_history_history__entity_type___entity_name__get", + "parameters": [ + { + "required": true, + "schema": { + "$ref": "#/components/schemas/EntityType" + }, + "name": "entity_type", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Entity Name" + }, + "name": "entity_name", + "in": "path" + }, + { + "required": false, + "schema": { + "type": "integer", + "title": "Offset", + "default": 0 + }, + "name": "offset", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "integer", + "title": "Limit", + "default": 100, + "lte": 100 + }, + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/HistoryOutput" + }, + "type": "array", + "title": "Response List History History Entity Type Entity Name Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/history": { + "get": { + "tags": [ + "history" + ], + "summary": "List History By Node Context", + "description": "List all activity history for a node context", + "operationId": "list_history_by_node_context_history_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node" + }, + "name": "node", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "integer", + "title": "Offset", + "default": 0 + }, + "name": "offset", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "integer", + "title": "Limit", + "default": 100, + "lte": 100 + }, + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/HistoryOutput" + }, + "type": "array", + "title": "Response List History By Node Context History Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/cubes/{name}": { + "get": { + "tags": [ + "cubes" + ], + "summary": "Get A Cube", + "description": "Get information on a cube", + "operationId": "Get_a_Cube_cubes__name__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CubeRevisionMetadata" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/cubes/{name}/dimensions/sql": { + "get": { + "tags": [ + "cubes" + ], + "summary": "Dimensions Sql For Cube", + "description": "Generates SQL to retrieve all unique values of a dimension for the cube", + "operationId": "Dimensions_SQL_for_Cube_cubes__name__dimensions_sql_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + }, + { + "description": "Dimensions to get values for", + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions", + "description": "Dimensions to get values for", + "default": [] + }, + "name": "dimensions", + "in": "query" + }, + { + "description": "Filters on dimensional attributes", + "required": false, + "schema": { + "type": "string", + "title": "Filters", + "description": "Filters on dimensional attributes" + }, + "name": "filters", + "in": "query" + }, + { + "description": "Number of rows to limit the data retrieved to", + "required": false, + "schema": { + "type": "integer", + "title": "Limit", + "description": "Number of rows to limit the data retrieved to" + }, + "name": "limit", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "boolean", + "title": "Include Counts", + "default": false + }, + "name": "include_counts", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TranslatedSQL" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/cubes/{name}/dimensions/data": { + "get": { + "tags": [ + "cubes" + ], + "summary": "Dimensions Values For Cube", + "description": "All unique values of a dimension from the cube", + "operationId": "Dimensions_Values_for_Cube_cubes__name__dimensions_data_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + }, + { + "description": "Dimensions to get values for", + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions", + "description": "Dimensions to get values for", + "default": [] + }, + "name": "dimensions", + "in": "query" + }, + { + "description": "Filters on dimensional attributes", + "required": false, + "schema": { + "type": "string", + "title": "Filters", + "description": "Filters on dimensional attributes" + }, + "name": "filters", + "in": "query" + }, + { + "description": "Number of rows to limit the data retrieved to", + "required": false, + "schema": { + "type": "integer", + "title": "Limit", + "description": "Number of rows to limit the data retrieved to" + }, + "name": "limit", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "boolean", + "title": "Include Counts", + "default": false + }, + "name": "include_counts", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "boolean", + "title": "Async ", + "default": false + }, + "name": "async_", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DimensionValues" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/tags": { + "get": { + "tags": [ + "tags" + ], + "summary": "List Tags", + "description": "List all available tags.", + "operationId": "list_tags_tags_get", + "parameters": [ + { + "required": false, + "schema": { + "type": "string", + "title": "Tag Type" + }, + "name": "tag_type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/TagOutput" + }, + "type": "array", + "title": "Response List Tags Tags Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "post": { + "tags": [ + "tags" + ], + "summary": "Create A Tag", + "description": "Create a tag.", + "operationId": "create_a_tag_tags_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTag" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/tags/{name}": { + "get": { + "tags": [ + "tags" + ], + "summary": "Get A Tag", + "description": "Return a tag by name.", + "operationId": "get_a_tag_tags__name__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "patch": { + "tags": [ + "tags" + ], + "summary": "Update A Tag", + "description": "Update a tag.", + "operationId": "update_a_tag_tags__name__patch", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTag" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagOutput" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/tags/{name}/nodes": { + "get": { + "tags": [ + "tags" + ], + "summary": "List Nodes For A Tag", + "description": "Find nodes tagged with the tag, filterable by node type.", + "operationId": "list_nodes_for_a_tag_tags__name__nodes_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + }, + { + "required": false, + "schema": { + "$ref": "#/components/schemas/NodeType" + }, + "name": "node_type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NodeMinimumDetail" + }, + "type": "array", + "title": "Response List Nodes For A Tag Tags Name Nodes Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/attributes": { + "get": { + "tags": [ + "attributes" + ], + "summary": "List Attributes", + "description": "List all available attribute types.", + "operationId": "list_attributes_attributes_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AttributeTypeBase" + }, + "type": "array", + "title": "Response List Attributes Attributes Get" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + }, + "post": { + "tags": [ + "attributes" + ], + "summary": "Add An Attribute Type", + "description": "Add a new attribute type", + "operationId": "Add_an_Attribute_Type_attributes_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutableAttributeTypeFields" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AttributeTypeBase" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/sql/measures": { + "get": { + "tags": [ + "sql" + ], + "summary": "Get Measures Sql", + "description": "Return the measures SQL for a set of metrics with dimensions and filters.\nThis SQL can be used to produce an intermediate table with all the measures\nand dimensions needed for an analytics database (e.g., Druid).", + "operationId": "Get_Measures_SQL_sql_measures_get", + "parameters": [ + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Metrics", + "default": [] + }, + "name": "metrics", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions", + "default": [] + }, + "name": "dimensions", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Filters", + "default": [] + }, + "name": "filters", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Name" + }, + "name": "engine_name", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Version" + }, + "name": "engine_version", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TranslatedSQL" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/sql/{node_name}": { + "get": { + "tags": [ + "sql" + ], + "summary": "Get Sql For A Node", + "description": "Return SQL for a node.", + "operationId": "Get_SQL_For_A_Node_sql__node_name__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions", + "default": [] + }, + "name": "dimensions", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Filters", + "default": [] + }, + "name": "filters", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Orderby", + "default": [] + }, + "name": "orderby", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "integer", + "title": "Limit" + }, + "name": "limit", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Name" + }, + "name": "engine_name", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Version" + }, + "name": "engine_version", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TranslatedSQL" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/sql": { + "get": { + "tags": [ + "sql" + ], + "summary": "Get Sql For Metrics", + "description": "Return SQL for a set of metrics with dimensions and filters", + "operationId": "Get_SQL_For_Metrics_sql_get", + "parameters": [ + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Metrics", + "default": [] + }, + "name": "metrics", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions", + "default": [] + }, + "name": "dimensions", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Filters", + "default": [] + }, + "name": "filters", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Orderby", + "default": [] + }, + "name": "orderby", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "integer", + "title": "Limit" + }, + "name": "limit", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Name" + }, + "name": "engine_name", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "title": "Engine Version" + }, + "name": "engine_version", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TranslatedSQL" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/datajunction-clients/python/new_node/{node_name}": { + "get": { + "tags": [ + "client" + ], + "summary": "Client Code For Creating Node", + "description": "Generate the Python client code used for creating this node", + "operationId": "client_code_for_creating_node_datajunction_clients_python_new_node__node_name__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string", + "title": "Response Client Code For Creating Node Datajunction Clients Python New Node Node Name Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/datajunction-clients/python/add_materialization/{node_name}/{materialization_name}": { + "get": { + "tags": [ + "client" + ], + "summary": "Client Code For Adding Materialization", + "description": "Generate the Python client code used for adding this materialization", + "operationId": "client_code_for_adding_materialization_datajunction_clients_python_add_materialization__node_name___materialization_name__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Materialization Name" + }, + "name": "materialization_name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string", + "title": "Response Client Code For Adding Materialization Datajunction Clients Python Add Materialization Node Name Materialization Name Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/datajunction-clients/python/link_dimension/{node_name}/{column}/{dimension}": { + "get": { + "tags": [ + "client" + ], + "summary": "Client Code For Linking Dimension To Node", + "description": "Generate the Python client code used for linking this node's column to a dimension", + "operationId": "client_code_for_linking_dimension_to_node_datajunction_clients_python_link_dimension__node_name___column___dimension__get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Node Name" + }, + "name": "node_name", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Column" + }, + "name": "column", + "in": "path" + }, + { + "required": true, + "schema": { + "type": "string", + "title": "Dimension" + }, + "name": "dimension", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string", + "title": "Response Client Code For Linking Dimension To Node Datajunction Clients Python Link Dimension Node Name Column Dimension Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/dimensions": { + "get": { + "tags": [ + "dimensions" + ], + "summary": "List Dimensions", + "description": "List all available dimensions.", + "operationId": "list_dimensions_dimensions_get", + "parameters": [ + { + "required": false, + "schema": { + "type": "string", + "title": "Prefix" + }, + "name": "prefix", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Response List Dimensions Dimensions Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/dimensions/{name}/nodes": { + "get": { + "tags": [ + "dimensions" + ], + "summary": "Find Nodes With Dimension", + "description": "List all nodes that have the specified dimension", + "operationId": "find_nodes_with_dimension_dimensions__name__nodes_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Name" + }, + "name": "name", + "in": "path" + }, + { + "required": false, + "schema": { + "items": { + "$ref": "#/components/schemas/NodeType" + }, + "type": "array" + }, + "name": "node_type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NodeRevisionOutput" + }, + "type": "array", + "title": "Response Find Nodes With Dimension Dimensions Name Nodes Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/dimensions/common": { + "get": { + "tags": [ + "dimensions" + ], + "summary": "Find Nodes With Common Dimensions", + "description": "Find all nodes that have the list of common dimensions", + "operationId": "find_nodes_with_common_dimensions_dimensions_common_get", + "parameters": [ + { + "required": false, + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimension" + }, + "name": "dimension", + "in": "query" + }, + { + "required": false, + "schema": { + "items": { + "$ref": "#/components/schemas/NodeType" + }, + "type": "array" + }, + "name": "node_type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NodeRevisionOutput" + }, + "type": "array", + "title": "Response Find Nodes With Common Dimensions Dimensions Common Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/graphql": { + "get": { + "summary": "Handle Http Get", + "operationId": "handle_http_get_graphql_get", + "responses": { + "200": { + "description": "The GraphiQL integrated development environment.", + "content": { + "application/json": { + "schema": {} + } + } + }, + "404": { + "description": "Not found if GraphiQL or query via GET are not enabled." + } + } + }, + "post": { + "summary": "Handle Http Post", + "operationId": "handle_http_post_graphql_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/whoami": { + "get": { + "tags": [ + "Who am I?" + ], + "summary": "Get User", + "description": "Returns the current authenticated user", + "operationId": "get_user_whoami_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserOutput" + } + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/token": { + "get": { + "tags": [ + "Who am I?" + ], + "summary": "Get Short Lived Token", + "description": "Returns a token that expires in 24 hours", + "operationId": "get_short_lived_token_token_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "security": [ + { + "DJHTTPBearer": [] + } + ] + } + }, + "/basic/user/": { + "post": { + "tags": [ + "Basic OAuth2" + ], + "summary": "Create A User", + "description": "Create a new user", + "operationId": "create_a_user_basic_user__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_create_a_user_basic_user__post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/basic/login/": { + "post": { + "tags": [ + "Basic OAuth2" + ], + "summary": "Login", + "description": "Get a JWT token and set it as an HTTP only cookie", + "operationId": "login_basic_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_basic_login__post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/logout/": { + "post": { + "tags": [ + "Basic OAuth2" + ], + "summary": "Logout", + "description": "Logout a user by deleting the auth cookie", + "operationId": "logout_logout__post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ActivityType": { + "type": "string", + "enum": [ + "create", + "delete", + "restore", + "update", + "refresh", + "tag", + "set_attribute", + "status_change" + ], + "title": "ActivityType", + "description": "An activity type" + }, + "AggregationRule": { + "type": "string", + "enum": [ + "additive", + "non-additive", + "semi-additive" + ], + "title": "AggregationRule", + "description": "Type of allowed aggregation for a given measure." + }, + "AttributeOutput": { + "properties": { + "attribute_type": { + "$ref": "#/components/schemas/AttributeTypeName" + } + }, + "type": "object", + "required": [ + "attribute_type" + ], + "title": "AttributeOutput", + "description": "Column attribute output." + }, + "AttributeTypeBase": { + "properties": { + "namespace": { + "type": "string", + "title": "Namespace", + "default": "system" + }, + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "allowed_node_types": { + "items": { + "$ref": "#/components/schemas/NodeType" + }, + "type": "array" + }, + "uniqueness_scope": { + "items": { + "$ref": "#/components/schemas/UniquenessScope" + }, + "type": "array" + }, + "id": { + "type": "integer", + "title": "Id" + } + }, + "type": "object", + "required": [ + "name", + "description", + "allowed_node_types", + "id" + ], + "title": "AttributeTypeBase", + "description": "Base attribute type." + }, + "AttributeTypeIdentifier": { + "properties": { + "namespace": { + "type": "string", + "title": "Namespace", + "default": "system" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "AttributeTypeIdentifier", + "description": "Fields that can be used to identify an attribute type." + }, + "AttributeTypeName": { + "properties": { + "namespace": { + "type": "string", + "title": "Namespace" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": [ + "namespace", + "name" + ], + "title": "AttributeTypeName", + "description": "Attribute type name." + }, + "AvailabilityStateBase": { + "properties": { + "min_temporal_partition": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Min Temporal Partition", + "default": [] + }, + "max_temporal_partition": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Max Temporal Partition", + "default": [] + }, + "catalog": { + "type": "string", + "title": "Catalog" + }, + "schema_": { + "type": "string", + "title": "Schema " + }, + "table": { + "type": "string", + "title": "Table" + }, + "valid_through_ts": { + "type": "integer", + "title": "Valid Through Ts" + }, + "url": { + "type": "string", + "title": "Url" + }, + "categorical_partitions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Categorical Partitions", + "default": [] + }, + "temporal_partitions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Temporal Partitions", + "default": [] + }, + "partitions": { + "items": { + "$ref": "#/components/schemas/PartitionAvailability" + }, + "type": "array", + "title": "Partitions", + "default": [] + } + }, + "type": "object", + "required": [ + "catalog", + "table", + "valid_through_ts" + ], + "title": "AvailabilityStateBase", + "description": "An availability state base" + }, + "BackfillOutput": { + "properties": { + "spec": { + "$ref": "#/components/schemas/PartitionBackfill" + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Urls" + } + }, + "type": "object", + "title": "BackfillOutput", + "description": "Output model for backfills" + }, + "Body_create_a_user_basic_user__post": { + "properties": { + "email": { + "type": "string", + "title": "Email" + }, + "username": { + "type": "string", + "title": "Username" + }, + "password": { + "type": "string", + "title": "Password" + } + }, + "type": "object", + "required": [ + "email", + "username", + "password" + ], + "title": "Body_create_a_user_basic_user__post" + }, + "Body_login_basic_login__post": { + "properties": { + "grant_type": { + "type": "string", + "pattern": "password", + "title": "Grant Type" + }, + "username": { + "type": "string", + "title": "Username" + }, + "password": { + "type": "string", + "title": "Password" + }, + "scope": { + "type": "string", + "title": "Scope", + "default": "" + }, + "client_id": { + "type": "string", + "title": "Client Id" + }, + "client_secret": { + "type": "string", + "title": "Client Secret" + } + }, + "type": "object", + "required": [ + "username", + "password" + ], + "title": "Body_login_basic_login__post" + }, + "CatalogInfo": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "engines": { + "items": { + "$ref": "#/components/schemas/EngineInfo" + }, + "type": "array", + "title": "Engines", + "default": [] + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "CatalogInfo", + "description": "Class for catalog creation" + }, + "ColumnMetadata": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "type": { + "type": "string", + "title": "Type" + }, + "column": { + "type": "string", + "title": "Column" + }, + "node": { + "type": "string", + "title": "Node" + }, + "semantic_entity": { + "type": "string", + "title": "Semantic Entity" + }, + "semantic_type": { + "type": "string", + "title": "Semantic Type" + } + }, + "type": "object", + "required": [ + "name", + "type" + ], + "title": "ColumnMetadata", + "description": "A simple model for column metadata." + }, + "ColumnType": { + "properties": {}, + "type": "object", + "title": "ColumnType", + "description": "Base type for all Column Types" + }, + "CreateCubeNode": { + "properties": { + "metrics": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Metrics" + }, + "dimensions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions" + }, + "filters": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Filters" + }, + "orderby": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Orderby" + }, + "limit": { + "type": "integer", + "title": "Limit" + }, + "description": { + "type": "string", + "title": "Description" + }, + "mode": { + "$ref": "#/components/schemas/NodeMode" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "primary_key": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Primary Key" + }, + "name": { + "type": "string", + "title": "Name" + }, + "namespace": { + "type": "string", + "title": "Namespace", + "default": "default" + } + }, + "type": "object", + "required": [ + "metrics", + "dimensions", + "description", + "mode", + "name" + ], + "title": "CreateCubeNode", + "description": "A create object for cube nodes" + }, + "CreateMeasure": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/NodeColumn" + }, + "type": "array", + "title": "Columns" + }, + "additive": { + "allOf": [ + { + "$ref": "#/components/schemas/AggregationRule" + } + ], + "default": "non-additive" + } + }, + "type": "object", + "required": [ + "name", + "columns" + ], + "title": "CreateMeasure", + "description": "Input for creating a measure" + }, + "CreateNode": { + "properties": { + "required_dimensions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Required Dimensions" + }, + "metric_metadata": { + "$ref": "#/components/schemas/MetricMetadataInput" + }, + "query": { + "type": "string", + "title": "Query" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "mode": { + "$ref": "#/components/schemas/NodeMode" + }, + "primary_key": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Primary Key" + }, + "name": { + "type": "string", + "title": "Name" + }, + "namespace": { + "type": "string", + "title": "Namespace", + "default": "default" + } + }, + "type": "object", + "required": [ + "query", + "description", + "mode", + "name" + ], + "title": "CreateNode", + "description": "Create non-source node object." + }, + "CreateSourceNode": { + "properties": { + "catalog": { + "type": "string", + "title": "Catalog" + }, + "schema_": { + "type": "string", + "title": "Schema " + }, + "table": { + "type": "string", + "title": "Table" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/SourceColumnOutput" + }, + "type": "array", + "title": "Columns" + }, + "missing_table": { + "type": "boolean", + "title": "Missing Table", + "default": false + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "mode": { + "$ref": "#/components/schemas/NodeMode" + }, + "primary_key": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Primary Key" + }, + "name": { + "type": "string", + "title": "Name" + }, + "namespace": { + "type": "string", + "title": "Namespace", + "default": "default" + } + }, + "type": "object", + "required": [ + "catalog", + "schema_", + "table", + "columns", + "description", + "mode", + "name" + ], + "title": "CreateSourceNode", + "description": "A create object for source nodes" + }, + "CreateTag": { + "properties": { + "description": { + "type": "string", + "title": "Description" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "tag_metadata": { + "type": "object", + "title": "Tag Metadata", + "default": {} + }, + "name": { + "type": "string", + "title": "Name" + }, + "tag_type": { + "type": "string", + "title": "Tag Type" + } + }, + "type": "object", + "required": [ + "name", + "tag_type" + ], + "title": "CreateTag", + "description": "Create tag model." + }, + "CubeElementMetadata": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "node_name": { + "type": "string", + "title": "Node Name" + }, + "type": { + "type": "string", + "title": "Type" + }, + "partition": { + "$ref": "#/components/schemas/PartitionOutput" + } + }, + "type": "object", + "required": [ + "name", + "display_name", + "node_name", + "type" + ], + "title": "CubeElementMetadata", + "description": "Metadata for an element in a cube" + }, + "CubeRevisionMetadata": { + "properties": { + "node_revision_id": { + "type": "integer", + "title": "Node Revision Id" + }, + "node_id": { + "type": "integer", + "title": "Node Id" + }, + "type": { + "$ref": "#/components/schemas/NodeType" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "version": { + "type": "string", + "title": "Version" + }, + "status": { + "$ref": "#/components/schemas/NodeStatus" + }, + "mode": { + "$ref": "#/components/schemas/NodeMode" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "availability": { + "$ref": "#/components/schemas/AvailabilityStateBase" + }, + "cube_elements": { + "items": { + "$ref": "#/components/schemas/CubeElementMetadata" + }, + "type": "array", + "title": "Cube Elements" + }, + "cube_node_metrics": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Cube Node Metrics" + }, + "cube_node_dimensions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Cube Node Dimensions" + }, + "query": { + "type": "string", + "title": "Query" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/datajunction_server__models__node__ColumnOutput" + }, + "type": "array", + "title": "Columns" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "materializations": { + "items": { + "$ref": "#/components/schemas/MaterializationConfigOutput" + }, + "type": "array", + "title": "Materializations" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/TagOutput" + }, + "type": "array", + "title": "Tags" + } + }, + "type": "object", + "required": [ + "node_revision_id", + "node_id", + "type", + "name", + "display_name", + "version", + "status", + "mode", + "cube_elements", + "cube_node_metrics", + "cube_node_dimensions", + "columns", + "updated_at", + "materializations" + ], + "title": "CubeRevisionMetadata", + "description": "Metadata for a cube node" + }, + "DAGNodeOutput": { + "properties": { + "namespace": { + "type": "string", + "title": "Namespace" + }, + "node_revision_id": { + "type": "integer", + "title": "Node Revision Id" + }, + "node_id": { + "type": "integer", + "title": "Node Id" + }, + "type": { + "$ref": "#/components/schemas/NodeType" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "version": { + "type": "string", + "title": "Version" + }, + "status": { + "$ref": "#/components/schemas/NodeStatus" + }, + "mode": { + "$ref": "#/components/schemas/NodeMode" + }, + "catalog": { + "$ref": "#/components/schemas/CatalogInfo" + }, + "schema_": { + "type": "string", + "title": "Schema " + }, + "table": { + "type": "string", + "title": "Table" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/datajunction_server__models__node__ColumnOutput" + }, + "type": "array", + "title": "Columns" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "parents": { + "items": { + "$ref": "#/components/schemas/NodeNameOutput" + }, + "type": "array", + "title": "Parents" + }, + "dimension_links": { + "items": { + "$ref": "#/components/schemas/LinkDimensionOutput" + }, + "type": "array", + "title": "Dimension Links" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/TagOutput" + }, + "type": "array", + "title": "Tags", + "default": [] + }, + "current_version": { + "type": "string", + "title": "Current Version" + } + }, + "type": "object", + "required": [ + "namespace", + "node_revision_id", + "node_id", + "type", + "name", + "display_name", + "version", + "status", + "mode", + "columns", + "updated_at", + "parents", + "dimension_links", + "created_at", + "current_version" + ], + "title": "DAGNodeOutput", + "description": "Output for a node in another node's DAG" + }, + "DJError": { + "properties": { + "code": { + "$ref": "#/components/schemas/ErrorCode" + }, + "message": { + "type": "string", + "title": "Message" + }, + "debug": { + "type": "object", + "title": "Debug" + }, + "context": { + "type": "string", + "title": "Context", + "default": "" + } + }, + "type": "object", + "required": [ + "code", + "message" + ], + "title": "DJError", + "description": "An error." + }, + "Dialect": { + "type": "string", + "enum": [ + "spark", + "trino", + "druid" + ], + "title": "Dialect", + "description": "SQL dialect" + }, + "DimensionAttributeOutput": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "node_name": { + "type": "string", + "title": "Node Name" + }, + "node_display_name": { + "type": "string", + "title": "Node Display Name" + }, + "is_primary_key": { + "type": "boolean", + "title": "Is Primary Key" + }, + "type": { + "type": "string", + "title": "Type" + }, + "path": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Path" + } + }, + "type": "object", + "required": [ + "name", + "is_primary_key", + "type", + "path" + ], + "title": "DimensionAttributeOutput", + "description": "Dimension attribute output should include the name and type" + }, + "DimensionValue": { + "properties": { + "value": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Value" + }, + "count": { + "type": "integer", + "title": "Count" + } + }, + "type": "object", + "required": [ + "value" + ], + "title": "DimensionValue", + "description": "Dimension value and count" + }, + "DimensionValues": { + "properties": { + "dimensions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions" + }, + "values": { + "items": { + "$ref": "#/components/schemas/DimensionValue" + }, + "type": "array", + "title": "Values" + }, + "cardinality": { + "type": "integer", + "title": "Cardinality" + } + }, + "type": "object", + "required": [ + "dimensions", + "values", + "cardinality" + ], + "title": "DimensionValues", + "description": "Dimension values" + }, + "DruidConf": { + "properties": { + "granularity": { + "type": "string", + "title": "Granularity" + }, + "intervals": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Intervals" + }, + "timestamp_column": { + "type": "string", + "title": "Timestamp Column" + }, + "timestamp_format": { + "type": "string", + "title": "Timestamp Format" + }, + "parse_spec_format": { + "type": "string", + "title": "Parse Spec Format" + } + }, + "type": "object", + "title": "DruidConf", + "description": "Druid configuration" + }, + "DruidCubeConfigInput": { + "properties": { + "spark": { + "$ref": "#/components/schemas/SparkConf" + }, + "lookback_window": { + "type": "string", + "title": "Lookback Window" + }, + "dimensions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions" + }, + "measures": { + "additionalProperties": { + "$ref": "#/components/schemas/MetricMeasures" + }, + "type": "object", + "title": "Measures" + }, + "metrics": { + "items": { + "$ref": "#/components/schemas/ColumnMetadata" + }, + "type": "array", + "title": "Metrics" + }, + "prefix": { + "type": "string", + "title": "Prefix", + "default": "" + }, + "suffix": { + "type": "string", + "title": "Suffix", + "default": "" + }, + "druid": { + "$ref": "#/components/schemas/DruidConf" + } + }, + "type": "object", + "title": "DruidCubeConfigInput", + "description": "Specific Druid cube materialization fields that require user input" + }, + "EditMeasure": { + "properties": { + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/NodeColumn" + }, + "type": "array", + "title": "Columns" + }, + "additive": { + "$ref": "#/components/schemas/AggregationRule" + } + }, + "type": "object", + "title": "EditMeasure", + "description": "Editable fields on a measure" + }, + "EngineInfo": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "version": { + "type": "string", + "title": "Version" + }, + "uri": { + "type": "string", + "title": "Uri" + }, + "dialect": { + "$ref": "#/components/schemas/Dialect" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "title": "EngineInfo", + "description": "Class for engine creation" + }, + "EntityType": { + "type": "string", + "enum": [ + "attribute", + "availability", + "backfill", + "catalog", + "column_attribute", + "dependency", + "engine", + "link", + "materialization", + "namespace", + "node", + "partition", + "query", + "tag" + ], + "title": "EntityType", + "description": "An entity type for which activity can occur" + }, + "ErrorCode": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 100, + 101, + 102, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 300, + 301, + 302, + 400, + 401, + 402, + 403, + 500, + 501 + ], + "title": "ErrorCode", + "description": "Error codes." + }, + "GenericCubeConfigInput": { + "properties": { + "spark": { + "$ref": "#/components/schemas/SparkConf" + }, + "lookback_window": { + "type": "string", + "title": "Lookback Window" + }, + "dimensions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions" + }, + "measures": { + "additionalProperties": { + "$ref": "#/components/schemas/MetricMeasures" + }, + "type": "object", + "title": "Measures" + }, + "metrics": { + "items": { + "$ref": "#/components/schemas/ColumnMetadata" + }, + "type": "array", + "title": "Metrics" + } + }, + "type": "object", + "title": "GenericCubeConfigInput", + "description": "Generic cube materialization config fields that require user input" + }, + "GenericMaterializationConfigInput": { + "properties": { + "spark": { + "$ref": "#/components/schemas/SparkConf" + }, + "lookback_window": { + "type": "string", + "title": "Lookback Window" + } + }, + "type": "object", + "title": "GenericMaterializationConfigInput", + "description": "User-input portions of the materialization config" + }, + "Granularity": { + "type": "string", + "enum": [ + "second", + "minute", + "hour", + "day", + "week", + "month", + "quarter", + "year" + ], + "title": "Granularity", + "description": "Time dimension granularity." + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "HealthCheck": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "status": { + "$ref": "#/components/schemas/HealthcheckStatus" + } + }, + "type": "object", + "required": [ + "name", + "status" + ], + "title": "HealthCheck", + "description": "A healthcheck response." + }, + "HealthcheckStatus": { + "type": "string", + "enum": [ + "ok", + "failed" + ], + "title": "HealthcheckStatus", + "description": "Possible health statuses." + }, + "HistoryOutput": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "entity_type": { + "$ref": "#/components/schemas/EntityType" + }, + "entity_name": { + "type": "string", + "title": "Entity Name" + }, + "node": { + "type": "string", + "title": "Node" + }, + "activity_type": { + "$ref": "#/components/schemas/ActivityType" + }, + "user": { + "type": "string", + "title": "User" + }, + "pre": { + "type": "object", + "title": "Pre" + }, + "post": { + "type": "object", + "title": "Post" + }, + "details": { + "type": "object", + "title": "Details" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + } + }, + "type": "object", + "required": [ + "id", + "pre", + "post", + "details", + "created_at" + ], + "title": "HistoryOutput", + "description": "Output history event" + }, + "JoinCardinality": { + "type": "string", + "enum": [ + "one_to_one", + "one_to_many", + "many_to_one", + "many_to_many" + ], + "title": "JoinCardinality", + "description": "The version upgrade type" + }, + "JoinType": { + "type": "string", + "enum": [ + "left", + "right", + "inner", + "full", + "cross" + ], + "title": "JoinType", + "description": "Join type" + }, + "LineageColumn": { + "properties": { + "column_name": { + "type": "string", + "title": "Column Name" + }, + "node_name": { + "type": "string", + "title": "Node Name" + }, + "node_type": { + "type": "string", + "title": "Node Type" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "lineage": { + "items": { + "$ref": "#/components/schemas/LineageColumn" + }, + "type": "array", + "title": "Lineage" + } + }, + "type": "object", + "required": [ + "column_name" + ], + "title": "LineageColumn", + "description": "Column in lineage graph" + }, + "LinkDimensionIdentifier": { + "properties": { + "dimension_node": { + "type": "string", + "title": "Dimension Node" + }, + "role": { + "type": "string", + "title": "Role" + } + }, + "type": "object", + "required": [ + "dimension_node" + ], + "title": "LinkDimensionIdentifier", + "description": "Input for linking a dimension to a node" + }, + "LinkDimensionInput": { + "properties": { + "dimension_node": { + "type": "string", + "title": "Dimension Node" + }, + "join_type": { + "allOf": [ + { + "$ref": "#/components/schemas/JoinType" + } + ], + "default": "left" + }, + "join_on": { + "type": "string", + "title": "Join On" + }, + "join_cardinality": { + "allOf": [ + { + "$ref": "#/components/schemas/JoinCardinality" + } + ], + "default": "many_to_one" + }, + "role": { + "type": "string", + "title": "Role" + } + }, + "type": "object", + "required": [ + "dimension_node", + "join_on" + ], + "title": "LinkDimensionInput", + "description": "Input for linking a dimension to a node" + }, + "LinkDimensionOutput": { + "properties": { + "dimension": { + "$ref": "#/components/schemas/NodeNameOutput" + }, + "join_type": { + "$ref": "#/components/schemas/JoinType" + }, + "join_sql": { + "type": "string", + "title": "Join Sql" + }, + "join_cardinality": { + "$ref": "#/components/schemas/JoinCardinality" + }, + "role": { + "type": "string", + "title": "Role" + }, + "foreign_keys": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Foreign Keys" + } + }, + "type": "object", + "required": [ + "dimension", + "join_type", + "join_sql", + "foreign_keys" + ], + "title": "LinkDimensionOutput", + "description": "Input for linking a dimension to a node" + }, + "MaterializationConfigInfoUnified": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "config": { + "type": "object", + "title": "Config" + }, + "schedule": { + "type": "string", + "title": "Schedule" + }, + "job": { + "type": "string", + "title": "Job" + }, + "backfills": { + "items": { + "$ref": "#/components/schemas/BackfillOutput" + }, + "type": "array", + "title": "Backfills" + }, + "strategy": { + "type": "string", + "title": "Strategy" + }, + "output_tables": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Output Tables" + }, + "urls": { + "items": { + "type": "string", + "maxLength": 65536, + "minLength": 1, + "format": "uri" + }, + "type": "array", + "title": "Urls" + } + }, + "type": "object", + "required": [ + "config", + "schedule", + "backfills", + "output_tables", + "urls" + ], + "title": "MaterializationConfigInfoUnified", + "description": "Materialization config + info" + }, + "MaterializationConfigOutput": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "config": { + "type": "object", + "title": "Config" + }, + "schedule": { + "type": "string", + "title": "Schedule" + }, + "job": { + "type": "string", + "title": "Job" + }, + "backfills": { + "items": { + "$ref": "#/components/schemas/BackfillOutput" + }, + "type": "array", + "title": "Backfills" + }, + "strategy": { + "type": "string", + "title": "Strategy" + } + }, + "type": "object", + "required": [ + "config", + "schedule", + "backfills" + ], + "title": "MaterializationConfigOutput", + "description": "Output for materialization config." + }, + "MaterializationInfo": { + "properties": { + "output_tables": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Output Tables" + }, + "urls": { + "items": { + "type": "string", + "maxLength": 65536, + "minLength": 1, + "format": "uri" + }, + "type": "array", + "title": "Urls" + } + }, + "type": "object", + "required": [ + "output_tables", + "urls" + ], + "title": "MaterializationInfo", + "description": "The output when calling the query service's materialization\nAPI endpoint for a cube node." + }, + "MaterializationJobTypeEnum": { + "enum": [ + { + "name": "spark_sql", + "label": "Spark SQL", + "description": "Spark SQL materialization job", + "allowed_node_types": [ + "transform", + "dimension", + "cube" + ], + "job_class": "SparkSqlMaterializationJob" + }, + { + "name": "druid_measures_cube", + "label": "Druid Measures Cube (Pre-Agg Cube)", + "description": "Used to materialize a cube's measures to Druid for low-latency access to a set of metrics and dimensions. While the logical cube definition is at the level of metrics and dimensions, this materialized Druid cube will contain measures and dimensions, with rollup configured on the measures where appropriate.", + "allowed_node_types": [ + "cube" + ], + "job_class": "DruidMeasuresCubeMaterializationJob" + }, + { + "name": "druid_metrics_cube", + "label": "Druid Metrics Cube (Post-Agg Cube)", + "description": "Used to materialize a cube of metrics and dimensions to Druid for low-latency access. The materialized cube is at the metric level, meaning that all metrics will be aggregated to the level of the cube's dimensions.", + "allowed_node_types": [ + "cube" + ], + "job_class": "DruidMetricsCubeMaterializationJob" + } + ], + "title": "MaterializationJobTypeEnum", + "description": "Available materialization job types" + }, + "MaterializationStrategy": { + "type": "string", + "enum": [ + "full", + "snapshot", + "snapshot_partition", + "incremental_time", + "view" + ], + "title": "MaterializationStrategy", + "description": "Materialization strategies" + }, + "Measure": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "field_name": { + "type": "string", + "title": "Field Name" + }, + "agg": { + "type": "string", + "title": "Agg" + }, + "type": { + "type": "string", + "title": "Type" + } + }, + "type": "object", + "required": [ + "name", + "field_name", + "agg", + "type" + ], + "title": "Measure", + "description": "A measure with a simple aggregation" + }, + "MeasureOutput": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/datajunction_server__models__measure__ColumnOutput" + }, + "type": "array", + "title": "Columns" + }, + "additive": { + "$ref": "#/components/schemas/AggregationRule" + } + }, + "type": "object", + "required": [ + "name", + "columns", + "additive" + ], + "title": "MeasureOutput", + "description": "Output model for measures" + }, + "Metric": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "current_version": { + "type": "string", + "title": "Current Version" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "query": { + "type": "string", + "title": "Query" + }, + "upstream_node": { + "type": "string", + "title": "Upstream Node" + }, + "expression": { + "type": "string", + "title": "Expression" + }, + "dimensions": { + "items": { + "$ref": "#/components/schemas/DimensionAttributeOutput" + }, + "type": "array", + "title": "Dimensions" + }, + "metric_metadata": { + "$ref": "#/components/schemas/MetricMetadataOutput" + }, + "required_dimensions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Required Dimensions" + } + }, + "type": "object", + "required": [ + "id", + "name", + "display_name", + "current_version", + "created_at", + "updated_at", + "query", + "upstream_node", + "expression", + "dimensions", + "required_dimensions" + ], + "title": "Metric", + "description": "Class for a metric." + }, + "MetricDirection": { + "type": "string", + "enum": [ + "higher_is_better", + "lower_is_better", + "neutral" + ], + "title": "MetricDirection", + "description": "The direction of the metric that's considered good, i.e., higher is better" + }, + "MetricMeasures": { + "properties": { + "metric": { + "type": "string", + "title": "Metric" + }, + "measures": { + "items": { + "$ref": "#/components/schemas/Measure" + }, + "type": "array", + "title": "Measures" + }, + "combiner": { + "type": "string", + "title": "Combiner" + } + }, + "type": "object", + "required": [ + "metric", + "measures", + "combiner" + ], + "title": "MetricMeasures", + "description": "Represent a metric as a set of measures, along with the expression for\ncombining the measures to make the metric." + }, + "MetricMetadataInput": { + "properties": { + "direction": { + "$ref": "#/components/schemas/MetricDirection" + }, + "unit": { + "type": "string", + "title": "Unit" + } + }, + "type": "object", + "title": "MetricMetadataInput", + "description": "Metric metadata output" + }, + "MetricMetadataOptions": { + "properties": { + "directions": { + "items": { + "$ref": "#/components/schemas/MetricDirection" + }, + "type": "array" + }, + "units": { + "items": { + "$ref": "#/components/schemas/Unit" + }, + "type": "array", + "title": "Units" + } + }, + "type": "object", + "required": [ + "directions", + "units" + ], + "title": "MetricMetadataOptions", + "description": "Metric metadata options list" + }, + "MetricMetadataOutput": { + "properties": { + "direction": { + "$ref": "#/components/schemas/MetricDirection" + }, + "unit": { + "$ref": "#/components/schemas/Unit" + } + }, + "type": "object", + "title": "MetricMetadataOutput", + "description": "Metric metadata output" + }, + "MutableAttributeTypeFields": { + "properties": { + "namespace": { + "type": "string", + "title": "Namespace", + "default": "system" + }, + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "allowed_node_types": { + "items": { + "$ref": "#/components/schemas/NodeType" + }, + "type": "array" + }, + "uniqueness_scope": { + "items": { + "$ref": "#/components/schemas/UniquenessScope" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "name", + "description", + "allowed_node_types" + ], + "title": "MutableAttributeTypeFields", + "description": "Fields on attribute types that users can set." + }, + "NamespaceOutput": { + "properties": { + "namespace": { + "type": "string", + "title": "Namespace" + }, + "num_nodes": { + "type": "integer", + "title": "Num Nodes" + } + }, + "type": "object", + "required": [ + "namespace", + "num_nodes" + ], + "title": "NamespaceOutput", + "description": "Output for a namespace that includes the number of nodes" + }, + "NodeColumn": { + "properties": { + "node": { + "type": "string", + "title": "Node" + }, + "column": { + "type": "string", + "title": "Column" + } + }, + "type": "object", + "required": [ + "node", + "column" + ], + "title": "NodeColumn", + "description": "Defines a column on a node" + }, + "NodeIndexItem": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "type": { + "$ref": "#/components/schemas/NodeType" + } + }, + "type": "object", + "required": [ + "name", + "display_name", + "description", + "type" + ], + "title": "NodeIndexItem", + "description": "Node details used for indexing purposes" + }, + "NodeMinimumDetail": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "version": { + "type": "string", + "title": "Version" + }, + "type": { + "$ref": "#/components/schemas/NodeType" + }, + "status": { + "$ref": "#/components/schemas/NodeStatus" + }, + "mode": { + "$ref": "#/components/schemas/NodeMode" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + } + }, + "type": "object", + "required": [ + "name", + "display_name", + "description", + "version", + "type", + "status", + "mode", + "updated_at" + ], + "title": "NodeMinimumDetail", + "description": "List of high level node details" + }, + "NodeMode": { + "type": "string", + "enum": [ + "published", + "draft" + ], + "title": "NodeMode", + "description": "Node mode.\n\nA node can be in one of the following modes:\n\n1. PUBLISHED - Must be valid and not cause any child nodes to be invalid\n2. DRAFT - Can be invalid, have invalid parents, and include dangling references" + }, + "NodeNameOutput": { + "properties": { + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "NodeNameOutput", + "description": "Node name only" + }, + "NodeOutput": { + "properties": { + "namespace": { + "type": "string", + "title": "Namespace" + }, + "node_revision_id": { + "type": "integer", + "title": "Node Revision Id" + }, + "node_id": { + "type": "integer", + "title": "Node Id" + }, + "type": { + "$ref": "#/components/schemas/NodeType" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "version": { + "type": "string", + "title": "Version" + }, + "status": { + "$ref": "#/components/schemas/NodeStatus" + }, + "mode": { + "$ref": "#/components/schemas/NodeMode" + }, + "catalog": { + "$ref": "#/components/schemas/CatalogInfo" + }, + "schema_": { + "type": "string", + "title": "Schema " + }, + "table": { + "type": "string", + "title": "Table" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "query": { + "type": "string", + "title": "Query" + }, + "availability": { + "$ref": "#/components/schemas/AvailabilityStateBase" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/datajunction_server__models__node__ColumnOutput" + }, + "type": "array", + "title": "Columns" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "materializations": { + "items": { + "$ref": "#/components/schemas/MaterializationConfigOutput" + }, + "type": "array", + "title": "Materializations" + }, + "parents": { + "items": { + "$ref": "#/components/schemas/NodeNameOutput" + }, + "type": "array", + "title": "Parents" + }, + "metric_metadata": { + "$ref": "#/components/schemas/MetricMetadataOutput" + }, + "dimension_links": { + "items": { + "$ref": "#/components/schemas/LinkDimensionOutput" + }, + "type": "array", + "title": "Dimension Links" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/TagOutput" + }, + "type": "array", + "title": "Tags", + "default": [] + }, + "current_version": { + "type": "string", + "title": "Current Version" + }, + "missing_table": { + "type": "boolean", + "title": "Missing Table", + "default": false + } + }, + "type": "object", + "required": [ + "namespace", + "node_revision_id", + "node_id", + "type", + "name", + "display_name", + "version", + "status", + "mode", + "columns", + "updated_at", + "materializations", + "parents", + "created_at", + "current_version" + ], + "title": "NodeOutput", + "description": "Output for a node that shows the current revision." + }, + "NodeRevisionBase": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "type": { + "$ref": "#/components/schemas/NodeType" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "query": { + "type": "string", + "title": "Query" + }, + "mode": { + "allOf": [ + { + "$ref": "#/components/schemas/NodeMode" + } + ], + "default": "published" + } + }, + "type": "object", + "required": [ + "name", + "type" + ], + "title": "NodeRevisionBase", + "description": "A base node revision." + }, + "NodeRevisionOutput": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "node_id": { + "type": "integer", + "title": "Node Id" + }, + "type": { + "$ref": "#/components/schemas/NodeType" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "version": { + "type": "string", + "title": "Version" + }, + "status": { + "$ref": "#/components/schemas/NodeStatus" + }, + "mode": { + "$ref": "#/components/schemas/NodeMode" + }, + "catalog": { + "$ref": "#/components/schemas/CatalogInfo" + }, + "schema_": { + "type": "string", + "title": "Schema " + }, + "table": { + "type": "string", + "title": "Table" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "query": { + "type": "string", + "title": "Query" + }, + "availability": { + "$ref": "#/components/schemas/AvailabilityStateBase" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/datajunction_server__models__node__ColumnOutput" + }, + "type": "array", + "title": "Columns" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "materializations": { + "items": { + "$ref": "#/components/schemas/MaterializationConfigOutput" + }, + "type": "array", + "title": "Materializations" + }, + "parents": { + "items": { + "$ref": "#/components/schemas/NodeNameOutput" + }, + "type": "array", + "title": "Parents" + }, + "metric_metadata": { + "$ref": "#/components/schemas/MetricMetadataOutput" + }, + "dimension_links": { + "items": { + "$ref": "#/components/schemas/LinkDimensionOutput" + }, + "type": "array", + "title": "Dimension Links" + } + }, + "type": "object", + "required": [ + "id", + "node_id", + "type", + "name", + "display_name", + "version", + "status", + "mode", + "columns", + "updated_at", + "materializations", + "parents" + ], + "title": "NodeRevisionOutput", + "description": "Output for a node revision with information about columns and if it is a metric." + }, + "NodeStatus": { + "type": "string", + "enum": [ + "valid", + "invalid" + ], + "title": "NodeStatus", + "description": "Node status.\n\nA node can have one of the following statuses:\n\n1. VALID - All references to other nodes and node columns are valid\n2. INVALID - One or more parent nodes are incompatible or do not exist" + }, + "NodeType": { + "type": "string", + "enum": [ + "source", + "transform", + "metric", + "dimension", + "cube" + ], + "title": "NodeType", + "description": "Node type.\n\nA node can have 4 types, currently:\n\n1. SOURCE nodes are root nodes in the DAG, and point to tables or views in a DB.\n2. TRANSFORM nodes are SQL transformations, reading from SOURCE/TRANSFORM nodes.\n3. METRIC nodes are leaves in the DAG, and have a single aggregation query.\n4. DIMENSION nodes are special SOURCE nodes that can be auto-joined with METRICS.\n5. CUBE nodes contain a reference to a set of METRICS and a set of DIMENSIONS." + }, + "NodeValidation": { + "properties": { + "message": { + "type": "string", + "title": "Message" + }, + "status": { + "$ref": "#/components/schemas/NodeStatus" + }, + "dependencies": { + "items": { + "$ref": "#/components/schemas/NodeRevisionOutput" + }, + "type": "array", + "title": "Dependencies" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/datajunction_server__models__node__ColumnOutput" + }, + "type": "array", + "title": "Columns" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/DJError" + }, + "type": "array", + "title": "Errors" + }, + "missing_parents": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Missing Parents" + } + }, + "type": "object", + "required": [ + "message", + "status", + "dependencies", + "columns", + "errors", + "missing_parents" + ], + "title": "NodeValidation", + "description": "A validation of a provided node definition" + }, + "OAuthProvider": { + "type": "string", + "enum": [ + "basic", + "github", + "google" + ], + "title": "OAuthProvider", + "description": "Support oauth providers" + }, + "PartitionAvailability": { + "properties": { + "min_temporal_partition": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Min Temporal Partition" + }, + "max_temporal_partition": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Max Temporal Partition" + }, + "value": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Value" + }, + "valid_through_ts": { + "type": "integer", + "title": "Valid Through Ts" + } + }, + "type": "object", + "required": [ + "value" + ], + "title": "PartitionAvailability", + "description": "Partition-level availability" + }, + "PartitionBackfill": { + "properties": { + "column_name": { + "type": "string", + "title": "Column Name" + }, + "values": { + "items": {}, + "type": "array", + "title": "Values" + }, + "range": { + "items": {}, + "type": "array", + "title": "Range" + } + }, + "type": "object", + "required": [ + "column_name" + ], + "title": "PartitionBackfill", + "description": "Used for setting backfilled values" + }, + "PartitionInput": { + "properties": { + "type_": { + "$ref": "#/components/schemas/PartitionType" + }, + "granularity": { + "$ref": "#/components/schemas/Granularity" + }, + "format": { + "type": "string", + "title": "Format" + } + }, + "type": "object", + "required": [ + "type_" + ], + "title": "PartitionInput", + "description": "Expected settings for specifying a partition column" + }, + "PartitionOutput": { + "properties": { + "type_": { + "$ref": "#/components/schemas/PartitionType" + }, + "format": { + "type": "string", + "title": "Format" + }, + "granularity": { + "type": "string", + "title": "Granularity" + }, + "expression": { + "type": "string", + "title": "Expression" + } + }, + "type": "object", + "required": [ + "type_" + ], + "title": "PartitionOutput", + "description": "Output for partition" + }, + "PartitionType": { + "type": "string", + "enum": [ + "temporal", + "categorical" + ], + "title": "PartitionType", + "description": "Partition type.\n\nA partition can be temporal or categorical" + }, + "QueryResults": { + "items": { + "$ref": "#/components/schemas/StatementResults" + }, + "type": "array", + "title": "QueryResults", + "description": "Results for a given query." + }, + "QueryState": { + "type": "string", + "enum": [ + "UNKNOWN", + "ACCEPTED", + "SCHEDULED", + "RUNNING", + "FINISHED", + "CANCELED", + "FAILED" + ], + "title": "QueryState", + "description": "Different states of a query." + }, + "QueryWithResults": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "engine_name": { + "type": "string", + "title": "Engine Name" + }, + "engine_version": { + "type": "string", + "title": "Engine Version" + }, + "submitted_query": { + "type": "string", + "title": "Submitted Query" + }, + "executed_query": { + "type": "string", + "title": "Executed Query" + }, + "scheduled": { + "type": "string", + "format": "date-time", + "title": "Scheduled" + }, + "started": { + "type": "string", + "format": "date-time", + "title": "Started" + }, + "finished": { + "type": "string", + "format": "date-time", + "title": "Finished" + }, + "state": { + "allOf": [ + { + "$ref": "#/components/schemas/QueryState" + } + ], + "default": "UNKNOWN" + }, + "progress": { + "type": "number", + "title": "Progress", + "default": 0.0 + }, + "output_table": { + "$ref": "#/components/schemas/TableRef" + }, + "results": { + "$ref": "#/components/schemas/QueryResults" + }, + "next": { + "type": "string", + "maxLength": 65536, + "minLength": 1, + "format": "uri", + "title": "Next" + }, + "previous": { + "type": "string", + "maxLength": 65536, + "minLength": 1, + "format": "uri", + "title": "Previous" + }, + "errors": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Errors" + }, + "links": { + "items": { + "type": "string", + "maxLength": 65536, + "minLength": 1, + "format": "uri" + }, + "type": "array", + "title": "Links" + } + }, + "type": "object", + "required": [ + "id", + "submitted_query", + "results", + "errors" + ], + "title": "QueryWithResults", + "description": "Model for query with results." + }, + "SourceColumnOutput": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "type": { + "$ref": "#/components/schemas/ColumnType" + }, + "attributes": { + "items": { + "$ref": "#/components/schemas/AttributeOutput" + }, + "type": "array", + "title": "Attributes" + }, + "dimension": { + "type": "string", + "title": "Dimension" + } + }, + "type": "object", + "required": [ + "name", + "type" + ], + "title": "SourceColumnOutput", + "description": "A column used in creation of a source node" + }, + "SparkConf": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "SparkConf", + "description": "Spark configuration", + "default": {} + }, + "StatementResults": { + "properties": { + "sql": { + "type": "string", + "title": "Sql" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/ColumnMetadata" + }, + "type": "array", + "title": "Columns" + }, + "rows": { + "items": { + "items": {}, + "type": "array" + }, + "type": "array", + "title": "Rows" + }, + "row_count": { + "type": "integer", + "title": "Row Count", + "default": 0 + } + }, + "type": "object", + "required": [ + "sql", + "columns", + "rows" + ], + "title": "StatementResults", + "description": "Results for a given statement.\n\nThis contains the SQL, column names and types, and rows" + }, + "TableRef": { + "properties": { + "catalog": { + "type": "string", + "title": "Catalog" + }, + "schema": { + "type": "string", + "title": "Schema" + }, + "table": { + "type": "string", + "title": "Table" + } + }, + "type": "object", + "required": [ + "catalog", + "schema", + "table" + ], + "title": "TableRef", + "description": "Table reference" + }, + "TagOutput": { + "properties": { + "description": { + "type": "string", + "title": "Description" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "tag_metadata": { + "type": "object", + "title": "Tag Metadata", + "default": {} + }, + "name": { + "type": "string", + "title": "Name" + }, + "tag_type": { + "type": "string", + "title": "Tag Type" + } + }, + "type": "object", + "required": [ + "name", + "tag_type" + ], + "title": "TagOutput", + "description": "Output tag model." + }, + "TranslatedSQL": { + "properties": { + "sql": { + "type": "string", + "title": "Sql" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/ColumnMetadata" + }, + "type": "array", + "title": "Columns" + }, + "dialect": { + "$ref": "#/components/schemas/Dialect" + }, + "upstream_tables": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Upstream Tables" + } + }, + "type": "object", + "required": [ + "sql" + ], + "title": "TranslatedSQL", + "description": "Class for SQL generated from a given metric." + }, + "UniquenessScope": { + "type": "string", + "enum": [ + "node", + "column_type" + ], + "title": "UniquenessScope", + "description": "The scope at which this attribute needs to be unique." + }, + "Unit": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "label": { + "type": "string", + "title": "Label" + }, + "category": { + "type": "string", + "title": "Category" + }, + "abbreviation": { + "type": "string", + "title": "Abbreviation" + }, + "description": { + "type": "string", + "title": "Description" + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "Unit", + "description": "Metric unit" + }, + "UpdateNode": { + "properties": { + "metrics": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Metrics" + }, + "dimensions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Dimensions" + }, + "filters": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Filters" + }, + "orderby": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Orderby" + }, + "limit": { + "type": "integer", + "title": "Limit" + }, + "description": { + "type": "string", + "title": "Description" + }, + "mode": { + "$ref": "#/components/schemas/NodeMode" + }, + "required_dimensions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Required Dimensions" + }, + "metric_metadata": { + "$ref": "#/components/schemas/MetricMetadataInput" + }, + "query": { + "type": "string", + "title": "Query" + }, + "catalog": { + "type": "string", + "title": "Catalog" + }, + "schema_": { + "type": "string", + "title": "Schema " + }, + "table": { + "type": "string", + "title": "Table" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/SourceColumnOutput" + }, + "type": "array", + "title": "Columns" + }, + "missing_table": { + "type": "boolean", + "title": "Missing Table" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "primary_key": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Primary Key" + } + }, + "additionalProperties": false, + "type": "object", + "title": "UpdateNode", + "description": "Update node object where all fields are optional" + }, + "UpdateTag": { + "properties": { + "description": { + "type": "string", + "title": "Description" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "tag_metadata": { + "type": "object", + "title": "Tag Metadata" + } + }, + "additionalProperties": false, + "type": "object", + "title": "UpdateTag", + "description": "Update tag model. Only works on mutable fields." + }, + "UpsertMaterialization": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "job": { + "$ref": "#/components/schemas/MaterializationJobTypeEnum" + }, + "config": { + "anyOf": [ + { + "$ref": "#/components/schemas/DruidCubeConfigInput" + }, + { + "$ref": "#/components/schemas/GenericCubeConfigInput" + }, + { + "$ref": "#/components/schemas/GenericMaterializationConfigInput" + } + ], + "title": "Config" + }, + "schedule": { + "type": "string", + "title": "Schedule" + }, + "strategy": { + "$ref": "#/components/schemas/MaterializationStrategy" + } + }, + "type": "object", + "required": [ + "job", + "config", + "schedule", + "strategy" + ], + "title": "UpsertMaterialization", + "description": "An upsert object for materialization configs" + }, + "UserOutput": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "username": { + "type": "string", + "title": "Username" + }, + "email": { + "type": "string", + "title": "Email" + }, + "name": { + "type": "string", + "title": "Name" + }, + "oauth_provider": { + "$ref": "#/components/schemas/OAuthProvider" + }, + "is_admin": { + "type": "boolean", + "title": "Is Admin", + "default": false + } + }, + "type": "object", + "required": [ + "id", + "username", + "oauth_provider" + ], + "title": "UserOutput", + "description": "User information to be included in responses" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + }, + "datajunction_server__models__measure__ColumnOutput": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "type": { + "type": "string", + "title": "Type" + }, + "node": { + "type": "string", + "title": "Node" + } + }, + "type": "object", + "required": [ + "name", + "type", + "node" + ], + "title": "ColumnOutput", + "description": "A simplified column schema, without ID or dimensions." + }, + "datajunction_server__models__node__ColumnOutput": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "type": { + "type": "string", + "title": "Type" + }, + "attributes": { + "items": { + "$ref": "#/components/schemas/AttributeOutput" + }, + "type": "array", + "title": "Attributes" + }, + "dimension": { + "$ref": "#/components/schemas/NodeNameOutput" + }, + "partition": { + "$ref": "#/components/schemas/PartitionOutput" + } + }, + "type": "object", + "required": [ + "name", + "type" + ], + "title": "ColumnOutput", + "description": "A simplified column schema, without ID or dimensions." + } + }, + "securitySchemes": { + "DJHTTPBearer": { + "type": "http", + "scheme": "bearer" + } + } + } +} \ No newline at end of file diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 000000000..a10554afa --- /dev/null +++ b/pdm.lock @@ -0,0 +1,2762 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "dev"] +strategy = [] +lock_version = "4.5.0" +content_hash = "sha256:a99c03149e75644e822f874489638d77a150e05361fcb3d0f252562162379e9f" + +[[metadata.targets]] +requires_python = "~=3.10" + +[[package]] +name = "accept-types" +version = "0.4.1" +summary = "Determine the best content to send in an HTTP response" +files = [ + {file = "accept-types-0.4.1.tar.gz", hash = "sha256:fb27099716d8f0360408c8ca86d69dbfed44455834b70d1506250abe521b535a"}, + {file = "accept_types-0.4.1-py3-none-any.whl", hash = "sha256:c87feccdffb66b02f9343ff387d7fd5c451ccb2e1221fbd37ea0cedef5cf290f"}, +] + +[[package]] +name = "alembic" +version = "1.14.1" +requires_python = ">=3.8" +summary = "A database migration tool for SQLAlchemy." +dependencies = [ + "Mako", + "SQLAlchemy>=1.3.0", + "importlib-metadata; python_version < \"3.9\"", + "importlib-resources; python_version < \"3.9\"", + "typing-extensions>=4", +] +files = [ + {file = "alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5"}, + {file = "alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213"}, +] + +[[package]] +name = "amqp" +version = "5.3.1" +requires_python = ">=3.6" +summary = "Low-level AMQP client for Python (fork of amqplib)." +dependencies = [ + "vine<6.0.0,>=5.0.0", +] +files = [ + {file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"}, + {file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"}, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.13.1" +summary = "ANTLR 4.13.1 runtime for Python 3" +dependencies = [ + "typing; python_version < \"3.5\"", +] +files = [ + {file = "antlr4-python3-runtime-4.13.1.tar.gz", hash = "sha256:3cd282f5ea7cfb841537fe01f143350fdb1c0b1ce7981443a2fa8513fddb6d1a"}, + {file = "antlr4_python3_runtime-4.13.1-py3-none-any.whl", hash = "sha256:78ec57aad12c97ac039ca27403ad61cb98aaec8a3f9bb8144f889aa0fa28b943"}, +] + +[[package]] +name = "anyio" +version = "4.8.0" +requires_python = ">=3.9" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +dependencies = [ + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions>=4.5; python_version < \"3.13\"", +] +files = [ + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, +] + +[[package]] +name = "asgiref" +version = "3.8.1" +requires_python = ">=3.8" +summary = "ASGI specs, helper code, and adapters" +dependencies = [ + "typing-extensions>=4; python_version < \"3.11\"", +] +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[[package]] +name = "asn1crypto" +version = "1.5.1" +summary = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" +files = [ + {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, + {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, +] + +[[package]] +name = "astroid" +version = "3.3.8" +requires_python = ">=3.9.0" +summary = "An abstract syntax tree for Python with inference support." +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.11\"", +] +files = [ + {file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"}, + {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +requires_python = ">=3.8" +summary = "Timeout context manager for asyncio programs" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "25.1.0" +requires_python = ">=3.8" +summary = "Classes Without Boilerplate" +files = [ + {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, + {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, +] + +[[package]] +name = "bcrypt" +version = "4.2.1" +requires_python = ">=3.7" +summary = "Modern password hashing for your software and your servers" +files = [ + {file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139"}, + {file = "bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005"}, + {file = "bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526"}, + {file = "bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d"}, + {file = "bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf"}, + {file = "bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c"}, + {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e"}, + {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f"}, + {file = "bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe"}, +] + +[[package]] +name = "billiard" +version = "4.2.1" +requires_python = ">=3.7" +summary = "Python multiprocessing fork with improvements and bugfixes" +files = [ + {file = "billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"}, + {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, +] + +[[package]] +name = "cachelib" +version = "0.13.0" +requires_python = ">=3.8" +summary = "A collection of cache libraries in the same API interface." +files = [ + {file = "cachelib-0.13.0-py3-none-any.whl", hash = "sha256:8c8019e53b6302967d4e8329a504acf75e7bc46130291d30188a6e4e58162516"}, + {file = "cachelib-0.13.0.tar.gz", hash = "sha256:209d8996e3c57595bee274ff97116d1d73c4980b2fd9a34c7846cd07fd2e1a48"}, +] + +[[package]] +name = "cachetools" +version = "5.5.1" +requires_python = ">=3.7" +summary = "Extensible memoizing collections and decorators" +files = [ + {file = "cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb"}, + {file = "cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95"}, +] + +[[package]] +name = "celery" +version = "5.4.0" +requires_python = ">=3.8" +summary = "Distributed Task Queue." +dependencies = [ + "backports-zoneinfo>=0.2.1; python_version < \"3.9\"", + "billiard<5.0,>=4.2.0", + "click-didyoumean>=0.3.0", + "click-plugins>=1.1.1", + "click-repl>=0.2.0", + "click<9.0,>=8.1.2", + "importlib-metadata>=3.6; python_version < \"3.8\"", + "kombu<6.0,>=5.3.4", + "python-dateutil>=2.8.2", + "tzdata>=2022.7", + "vine<6.0,>=5.1.0", +] +files = [ + {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, + {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, +] + +[[package]] +name = "celery" +version = "5.4.0" +extras = ["redis"] +requires_python = ">=3.8" +summary = "Distributed Task Queue." +dependencies = [ + "celery==5.4.0", + "redis!=4.5.5,<6.0.0,>=4.5.2", +] +files = [ + {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, + {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, +] + +[[package]] +name = "certifi" +version = "2024.12.14" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +files = [ + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +requires_python = ">=3.8" +summary = "Validate configuration and produce human readable error messages." +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +requires_python = ">=3.7" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + +[[package]] +name = "click" +version = "8.1.8" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +dependencies = [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +requires_python = ">=3.6.2" +summary = "Enables git-like *did-you-mean* feature in click" +dependencies = [ + "click>=7", +] +files = [ + {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, + {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, +] + +[[package]] +name = "click-plugins" +version = "1.1.1" +summary = "An extension module for click to enable registering CLI commands via setuptools entry-points." +dependencies = [ + "click>=4.0", +] +files = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +requires_python = ">=3.6" +summary = "REPL plugin for Click" +dependencies = [ + "click>=7.0", + "prompt-toolkit>=3.0.36", +] +files = [ + {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, + {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "44.0.0" +requires_python = "!=3.9.0,!=3.9.1,>=3.7" +summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +dependencies = [ + "cffi>=1.12; platform_python_implementation != \"PyPy\"", +] +files = [ + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, +] + +[[package]] +name = "datajunction-query" +version = "0.0.1a83" +requires_python = "<4.0,>=3.10" +editable = true +path = "./datajunction-query" +summary = "OSS Implementation of a DataJunction Query Service" +dependencies = [ + "accept-types==0.4.1", + "cachelib>=0.4.0", + "duckdb-engine", + "duckdb==0.8.1", + "fastapi>=0.79.0", + "importlib-metadata", + "msgpack>=1.0.3", + "psycopg[async,pool]>=3.2.1", + "pytest-asyncio>=0.24.0", + "pytest-integration>=0.2.3", + "python-dotenv==0.19.2", + "pyyaml>=6.0.1", + "requests<=2.29.0,>=2.28.2", + "rich>=10.16.2", + "snowflake-connector-python>=3.3.1", + "sqlalchemy>=2.0.34", + "tenacity>=9.0.0", + "toml>=0.10.2", + "trino>=0.324.0", +] + +[[package]] +name = "datajunction-reflection" +version = "0.0.1a83" +requires_python = "<4.0,>=3.8" +editable = true +path = "./datajunction-reflection" +summary = "OSS Implementation of a DataJunction Reflection Service" +dependencies = [ + "celery[redis]>=5.2.3", + "importlib-metadata", + "pydantic<2.0", + "python-dotenv==0.19.2", + "requests>=2.26.0", +] + +[[package]] +name = "datajunction-server" +version = "0.0.1a83" +requires_python = "<4.0,>=3.10" +editable = true +path = "./datajunction-server" +summary = "DataJunction server library for running to a DataJunction server" +dependencies = [ + "alembic>=1.10.3", + "antlr4-python3-runtime==4.13.1", + "bcrypt>=4.0.1", + "cachelib<1.0.0,>=0.10.2", + "cachetools>=5.3.1", + "celery<6.0.0,>=5.2.7", + "cryptography>=41.0.3", + "fastapi-cache2>=0.2.1", + "fastapi>=0.110.0", + "google-api-python-client>=2.95.0", + "google-auth-httplib2>=0.1.0", + "google-auth-oauthlib>=1.0.0", + "jinja2>=3.1.4", + "line-profiler>=4.0.3", + "msgpack<2.0.0,>=1.0.5", + "nbformat>=5.10.4", + "opentelemetry-instrumentation-fastapi==0.38b0", + "passlib>=1.7.4", + "psycopg>=3.1.16", + "pydantic<2", + "python-dotenv<1.0.0,>=0.19.0", + "python-jose>=3.3.0", + "python-multipart>=0.0.20", + "redis<5.0.0,>=4.5.4", + "requests<=2.29.0,>=2.28.2", + "rich<14.0.0,>=13.3.3", + "sqlalchemy-utils<1.0.0,>=0.40.0", + "sqlalchemy>=2", + "sse-starlette<=2.0.0,>=1.6.0", + "strawberry-graphql>=0.204.0", + "types-cachetools>=5.3.0.6", + "yarl<2.0.0,>=1.8.2", +] + +[[package]] +name = "deprecated" +version = "1.2.17" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +summary = "Python @deprecated decorator to deprecate old python classes, functions or methods." +dependencies = [ + "wrapt<2,>=1.10", +] +files = [ + {file = "Deprecated-1.2.17-py2.py3-none-any.whl", hash = "sha256:69cdc0a751671183f569495e2efb14baee4344b0236342eec29f1fde25d61818"}, + {file = "deprecated-1.2.17.tar.gz", hash = "sha256:0114a10f0bbb750b90b2c2296c90cf7e9eaeb0abb5cf06c80de2c60138de0a82"}, +] + +[[package]] +name = "dill" +version = "0.3.9" +requires_python = ">=3.8" +summary = "serialize all of Python" +files = [ + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, +] + +[[package]] +name = "distlib" +version = "0.3.9" +summary = "Distribution utilities" +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "duckdb" +version = "0.8.1" +summary = "DuckDB embedded database" +files = [ + {file = "duckdb-0.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:14781d21580ee72aba1f5dcae7734674c9b6c078dd60470a08b2b420d15b996d"}, + {file = "duckdb-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f13bf7ab0e56ddd2014ef762ae4ee5ea4df5a69545ce1191b8d7df8118ba3167"}, + {file = "duckdb-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4032042d8363e55365bbca3faafc6dc336ed2aad088f10ae1a534ebc5bcc181"}, + {file = "duckdb-0.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a71bd8f0b0ca77c27fa89b99349ef22599ffefe1e7684ae2e1aa2904a08684"}, + {file = "duckdb-0.8.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24568d6e48f3dbbf4a933109e323507a46b9399ed24c5d4388c4987ddc694fd0"}, + {file = "duckdb-0.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297226c0dadaa07f7c5ae7cbdb9adba9567db7b16693dbd1b406b739ce0d7924"}, + {file = "duckdb-0.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5792cf777ece2c0591194006b4d3e531f720186102492872cb32ddb9363919cf"}, + {file = "duckdb-0.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:12803f9f41582b68921d6b21f95ba7a51e1d8f36832b7d8006186f58c3d1b344"}, + {file = "duckdb-0.8.1-cp310-cp310-win32.whl", hash = "sha256:d0953d5a2355ddc49095e7aef1392b7f59c5be5cec8cdc98b9d9dc1f01e7ce2b"}, + {file = "duckdb-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e6583c98a7d6637e83bcadfbd86e1f183917ea539f23b6b41178f32f813a5eb"}, + {file = "duckdb-0.8.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fad7ed0d4415f633d955ac24717fa13a500012b600751d4edb050b75fb940c25"}, + {file = "duckdb-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81ae602f34d38d9c48dd60f94b89f28df3ef346830978441b83c5b4eae131d08"}, + {file = "duckdb-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d75cfe563aaa058d3b4ccaaa371c6271e00e3070df5de72361fd161b2fe6780"}, + {file = "duckdb-0.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbb55e7a3336f2462e5e916fc128c47fe1c03b6208d6bd413ac11ed95132aa0"}, + {file = "duckdb-0.8.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6df53efd63b6fdf04657385a791a4e3c4fb94bfd5db181c4843e2c46b04fef5"}, + {file = "duckdb-0.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b188b80b70d1159b17c9baaf541c1799c1ce8b2af4add179a9eed8e2616be96"}, + {file = "duckdb-0.8.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ad481ee353f31250b45d64b4a104e53b21415577943aa8f84d0af266dc9af85"}, + {file = "duckdb-0.8.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1d1b1729993611b1892509d21c21628917625cdbe824a61ce891baadf684b32"}, + {file = "duckdb-0.8.1-cp311-cp311-win32.whl", hash = "sha256:2d8f9cc301e8455a4f89aa1088b8a2d628f0c1f158d4cf9bc78971ed88d82eea"}, + {file = "duckdb-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:07457a43605223f62d93d2a5a66b3f97731f79bbbe81fdd5b79954306122f612"}, + {file = "duckdb-0.8.1.tar.gz", hash = "sha256:a54d37f4abc2afc4f92314aaa56ecf215a411f40af4bffe1e86bd25e62aceee9"}, +] + +[[package]] +name = "duckdb-engine" +version = "0.15.0" +requires_python = "<4,>=3.8" +summary = "SQLAlchemy driver for duckdb" +dependencies = [ + "duckdb>=0.5.0", + "packaging>=21", + "sqlalchemy>=1.3.22", +] +files = [ + {file = "duckdb_engine-0.15.0-py3-none-any.whl", hash = "sha256:d18acd73f03202145e1baa86605dca3612080fd0a849dbc42b38111ffee6857c"}, + {file = "duckdb_engine-0.15.0.tar.gz", hash = "sha256:59f67ec95ebf9eb4dea22994664dfd34edce3c7416b862daa46da43f572ad6ef"}, +] + +[[package]] +name = "ecdsa" +version = "0.19.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6" +summary = "ECDSA cryptographic signature library (pure python)" +dependencies = [ + "six>=1.9.0", +] +files = [ + {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, + {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[[package]] +name = "fastapi" +version = "0.115.7" +requires_python = ">=3.8" +summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +dependencies = [ + "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", + "starlette<0.46.0,>=0.40.0", + "typing-extensions>=4.8.0", +] +files = [ + {file = "fastapi-0.115.7-py3-none-any.whl", hash = "sha256:eb6a8c8bf7f26009e8147111ff15b5177a0e19bb4a45bc3486ab14804539d21e"}, + {file = "fastapi-0.115.7.tar.gz", hash = "sha256:0f106da6c01d88a6786b3248fb4d7a940d071f6f488488898ad5d354b25ed015"}, +] + +[[package]] +name = "fastapi-cache2" +version = "0.2.2" +requires_python = "<4.0,>=3.8" +summary = "Cache for FastAPI" +dependencies = [ + "fastapi", + "importlib-metadata<7.0.0,>=6.6.0; python_version < \"3.8\"", + "pendulum<4.0.0,>=3.0.0", + "typing-extensions>=4.1.0", + "uvicorn", +] +files = [ + {file = "fastapi_cache2-0.2.2-py3-none-any.whl", hash = "sha256:e1fae86d8eaaa6c8501dfe08407f71d69e87cc6748042d59d51994000532846c"}, + {file = "fastapi_cache2-0.2.2.tar.gz", hash = "sha256:71bf4450117dc24224ec120be489dbe09e331143c9f74e75eb6f576b78926026"}, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.1" +summary = "Fastest Python implementation of JSON schema" +files = [ + {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, + {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, +] + +[[package]] +name = "filelock" +version = "3.17.0" +requires_python = ">=3.9" +summary = "A platform independent file lock." +files = [ + {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, + {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, +] + +[[package]] +name = "google-api-core" +version = "2.24.1rc1" +requires_python = ">=3.7" +summary = "Google API client core library" +dependencies = [ + "google-auth<3.0.dev0,>=2.14.1", + "googleapis-common-protos<2.0.dev0,>=1.56.2", + "proto-plus<2.0.0dev,>=1.22.3", + "proto-plus<2.0.0dev,>=1.25.0; python_version >= \"3.13\"", + "protobuf!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.19.5", + "requests<3.0.0.dev0,>=2.18.0", +] +files = [ + {file = "google_api_core-2.24.1rc1-py3-none-any.whl", hash = "sha256:92ee3eed90a397a9f4dd13c034a36cbe7dba2a58e01e5668619847b68a527b73"}, + {file = "google_api_core-2.24.1rc1.tar.gz", hash = "sha256:d1cf8265c8b0b171a87d84adc8709a5e48147ca529d6f96d6a2be613a195eb78"}, +] + +[[package]] +name = "google-api-python-client" +version = "2.159.0" +requires_python = ">=3.7" +summary = "Google API Client Library for Python" +dependencies = [ + "google-api-core!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0.dev0,>=1.31.5", + "google-auth!=2.24.0,!=2.25.0,<3.0.0.dev0,>=1.32.0", + "google-auth-httplib2<1.0.0,>=0.2.0", + "httplib2<1.dev0,>=0.19.0", + "uritemplate<5,>=3.0.1", +] +files = [ + {file = "google_api_python_client-2.159.0-py2.py3-none-any.whl", hash = "sha256:baef0bb631a60a0bd7c0bf12a5499e3a40cd4388484de7ee55c1950bf820a0cf"}, + {file = "google_api_python_client-2.159.0.tar.gz", hash = "sha256:55197f430f25c907394b44fa078545ffef89d33fd4dca501b7db9f0d8e224bd6"}, +] + +[[package]] +name = "google-auth" +version = "2.38.0" +requires_python = ">=3.7" +summary = "Google Authentication Library" +dependencies = [ + "cachetools<6.0,>=2.0.0", + "pyasn1-modules>=0.2.1", + "rsa<5,>=3.1.4", +] +files = [ + {file = "google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a"}, + {file = "google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4"}, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.2.0" +summary = "Google Authentication Library: httplib2 transport" +dependencies = [ + "google-auth", + "httplib2>=0.19.0", +] +files = [ + {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, + {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, +] + +[[package]] +name = "google-auth-oauthlib" +version = "1.2.1" +requires_python = ">=3.6" +summary = "Google Authentication Library" +dependencies = [ + "google-auth>=2.15.0", + "requests-oauthlib>=0.7.0", +] +files = [ + {file = "google_auth_oauthlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f"}, + {file = "google_auth_oauthlib-1.2.1.tar.gz", hash = "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263"}, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.66.0" +requires_python = ">=3.7" +summary = "Common protobufs used in Google APIs" +dependencies = [ + "protobuf!=3.20.0,!=3.20.1,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.20.2", +] +files = [ + {file = "googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"}, + {file = "googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c"}, +] + +[[package]] +name = "graphql-core" +version = "3.2.5" +requires_python = "<4,>=3.6" +summary = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." +dependencies = [ + "typing-extensions<5,>=4; python_version < \"3.10\"", +] +files = [ + {file = "graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a"}, + {file = "graphql_core-3.2.5.tar.gz", hash = "sha256:e671b90ed653c808715645e3998b7ab67d382d55467b7e2978549111bbabf8d5"}, +] + +[[package]] +name = "greenlet" +version = "3.1.1" +requires_python = ">=3.7" +summary = "Lightweight in-process concurrent programming" +files = [ + {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, + {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, + {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, + {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, + {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, + {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, + {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, + {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, + {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httplib2" +version = "0.22.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "A comprehensive HTTP client library." +dependencies = [ + "pyparsing!=3.0.0,!=3.0.1,!=3.0.2,!=3.0.3,<4,>=2.4.2; python_version > \"3.0\"", + "pyparsing<3,>=2.4.2; python_version < \"3.0\"", +] +files = [ + {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, + {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, +] + +[[package]] +name = "identify" +version = "2.6.6" +requires_python = ">=3.9" +summary = "File identification library for Python" +files = [ + {file = "identify-2.6.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881"}, + {file = "identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251"}, +] + +[[package]] +name = "idna" +version = "3.10" +requires_python = ">=3.6" +summary = "Internationalized Domain Names in Applications (IDNA)" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +requires_python = ">=3.8" +summary = "Read metadata from Python packages" +dependencies = [ + "typing-extensions>=3.6.4; python_version < \"3.8\"", + "zipp>=3.20", +] +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.13.2" +requires_python = ">=3.8.0" +summary = "A Python utility / library to sort Python imports." +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +requires_python = ">=3.8" +summary = "An implementation of JSON Schema validation for Python" +dependencies = [ + "attrs>=22.2.0", + "importlib-resources>=1.4.0; python_version < \"3.9\"", + "jsonschema-specifications>=2023.03.6", + "pkgutil-resolve-name>=1.3.10; python_version < \"3.9\"", + "referencing>=0.28.4", + "rpds-py>=0.7.1", +] +files = [ + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, +] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +requires_python = ">=3.9" +summary = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +dependencies = [ + "referencing>=0.31.0", +] +files = [ + {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, + {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, +] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +requires_python = ">=3.8" +summary = "Jupyter core package. A base package on which Jupyter projects rely." +dependencies = [ + "platformdirs>=2.5", + "pywin32>=300; sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"", + "traitlets>=5.3", +] +files = [ + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, +] + +[[package]] +name = "kombu" +version = "5.4.2" +requires_python = ">=3.8" +summary = "Messaging library for Python." +dependencies = [ + "amqp<6.0.0,>=5.1.1", + "backports-zoneinfo[tzdata]>=0.2.1; python_version < \"3.9\"", + "typing-extensions==4.12.2; python_version < \"3.10\"", + "tzdata; python_version >= \"3.9\"", + "vine==5.1.0", +] +files = [ + {file = "kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763"}, + {file = "kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf"}, +] + +[[package]] +name = "line-profiler" +version = "4.2.0" +requires_python = ">=3.8" +summary = "Line-by-line profiler" +files = [ + {file = "line_profiler-4.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:70e2503f52ee6464ac908b578d73ad6dae21d689c95f2252fee97d7aa8426693"}, + {file = "line_profiler-4.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b6047c8748d7a2453522eaea3edc8d9febc658b57f2ea189c03fe3d5e34595b5"}, + {file = "line_profiler-4.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0048360a2afbd92c0b423f8207af1f6581d85c064c0340b0d02c63c8e0c8292c"}, + {file = "line_profiler-4.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e71fa1c85f21e3de575c7c617fd4eb607b052cc7b4354035fecc18f3f2a4317"}, + {file = "line_profiler-4.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5ec99d48cffdf36efbcd7297e81cc12bf2c0a7e0627a567f3ab0347e607b242"}, + {file = "line_profiler-4.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bfc9582f19a64283434fc6a3fd41a3a51d59e3cce2dc7adc5fe859fcae67e746"}, + {file = "line_profiler-4.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2b5dcfb3205e18c98c94388065f1604dc9d709df4dd62300ff8c5bbbd9bd163f"}, + {file = "line_profiler-4.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:4999eb1db5d52cb34a5293941986eea4357fb9fe3305a160694e5f13c9ec4008"}, + {file = "line_profiler-4.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:402406f200401a496fb93e1788387bf2d87c921d7f8f7e5f88324ac9efb672ac"}, + {file = "line_profiler-4.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d9a0b5696f1ad42bb31e90706e5d57845833483d1d07f092b66b4799847a2f76"}, + {file = "line_profiler-4.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2f950fa19f797a9ab55c8d7b33a7cdd95c396cf124c3adbc1cf93a1978d2767"}, + {file = "line_profiler-4.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d09fd8f580716da5a0b9a7f544a306b468f38eee28ba2465c56e0aa5d7d1822"}, + {file = "line_profiler-4.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:628f585960c6538873a9760d112db20b76b6035d3eaad7711a8bd80fa909d7ea"}, + {file = "line_profiler-4.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:63ed929c7d41e230cc1c4838c25bbee165d7f2fa974ca28d730ea69e501fc44d"}, + {file = "line_profiler-4.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6bda74fc206ba375396068526e9e7b5466a24c7e54cbd6ee1c98c1e0d1f0fd99"}, + {file = "line_profiler-4.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:eaf6eb827c202c07b8b8d82363bb039a6747fbf84ca04279495a91b7da3b773f"}, + {file = "line_profiler-4.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82d29887f1226938a86db30ca3a125b1bde89913768a2a486fa14d0d3f8c0d91"}, + {file = "line_profiler-4.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bf60706467203db0a872b93775a5e5902a02b11d79f8f75a8f8ef381b75789e1"}, + {file = "line_profiler-4.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:934fd964eed9bed87e3c01e8871ee6bdc54d10edf7bf14d20e72f7be03567ae3"}, + {file = "line_profiler-4.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d623e5b37fa48c7ad0c29b4353244346a5dcb1bf75e117e19400b8ffd3393d1b"}, + {file = "line_profiler-4.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efcdbed9ba9003792d8bfd56c11bb3d4e29ad7e0d2f583e1c774de73bbf02933"}, + {file = "line_profiler-4.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:df0149c191a95f2dbc93155b2f9faaee563362d61e78b8986cdb67babe017cdc"}, + {file = "line_profiler-4.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e3a1ca491a8606ed674882b59354087f6e9ab6b94aa6d5fa5d565c6f2acc7a8"}, + {file = "line_profiler-4.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:a85ff57d4ef9d899ca12d6b0883c3cab1786388b29d2fb5f30f909e70bb9a691"}, + {file = "line_profiler-4.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:49db0804e9e330076f0b048d63fd3206331ca0104dd549f61b2466df0f10ecda"}, + {file = "line_profiler-4.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2e983ed4fb2cd68bb8896f6bad7f29ddf9112b978f700448510477bc9fde18db"}, + {file = "line_profiler-4.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d6b27c5880b29369e6bebfe434a16c60cbcd290aa4c384ac612e5777737893f8"}, + {file = "line_profiler-4.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2584dc0af3107efa60bd2ccaa7233dca98e3dff4b11138c0ac30355bc87f1a"}, + {file = "line_profiler-4.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6767d8b922a7368b6917a47c164c3d96d48b82109ad961ef518e78800947cef4"}, + {file = "line_profiler-4.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3137672a769717be4da3a6e006c3bd7b66ad4a341ba89ee749ef96c158a15b22"}, + {file = "line_profiler-4.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:727e970d358616a1a33d51d696efec932a5ef7730785df62658bd7e74aa58951"}, + {file = "line_profiler-4.2.0.tar.gz", hash = "sha256:09e10f25f876514380b3faee6de93fb0c228abba85820ba1a591ddb3eb451a96"}, +] + +[[package]] +name = "mako" +version = "1.3.8" +requires_python = ">=3.8" +summary = "A super-fast templating language that borrows the best ideas from the existing templating languages." +dependencies = [ + "MarkupSafe>=0.9.2", +] +files = [ + {file = "Mako-1.3.8-py3-none-any.whl", hash = "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627"}, + {file = "mako-1.3.8.tar.gz", hash = "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +requires_python = ">=3.9" +summary = "Safely add untrusted strings to HTML/XML markup." +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +requires_python = ">=3.6" +summary = "McCabe checker, plugin for flake8" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "msgpack" +version = "1.1.0" +requires_python = ">=3.8" +summary = "MessagePack serializer" +files = [ + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, + {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, + {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, + {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, + {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, + {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, + {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, + {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, + {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, + {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, +] + +[[package]] +name = "multidict" +version = "6.1.0" +requires_python = ">=3.8" +summary = "multidict implementation" +dependencies = [ + "typing-extensions>=4.1.0; python_version < \"3.11\"", +] +files = [ + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +requires_python = ">=3.8" +summary = "The Jupyter Notebook format" +dependencies = [ + "fastjsonschema>=2.15", + "jsonschema>=2.6", + "jupyter-core!=5.0.*,>=4.12", + "traitlets>=5.1", +] +files = [ + {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, + {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Node.js virtual environment builder" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +requires_python = ">=3.6" +summary = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[[package]] +name = "opentelemetry-api" +version = "1.29.0" +requires_python = ">=3.8" +summary = "OpenTelemetry Python API" +dependencies = [ + "deprecated>=1.2.6", + "importlib-metadata<=8.5.0,>=6.0", +] +files = [ + {file = "opentelemetry_api-1.29.0-py3-none-any.whl", hash = "sha256:5fcd94c4141cc49c736271f3e1efb777bebe9cc535759c54c936cca4f1b312b8"}, + {file = "opentelemetry_api-1.29.0.tar.gz", hash = "sha256:d04a6cf78aad09614f52964ecb38021e248f5714dc32c2e0d8fd99517b4d69cf"}, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.38b0" +requires_python = ">=3.7" +summary = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +dependencies = [ + "opentelemetry-api~=1.4", + "setuptools>=16.0", + "wrapt<2.0.0,>=1.0.0", +] +files = [ + {file = "opentelemetry_instrumentation-0.38b0-py3-none-any.whl", hash = "sha256:48eed87e5db9d2cddd57a8ea359bd15318560c0ffdd80d90a5fc65816e15b7f4"}, + {file = "opentelemetry_instrumentation-0.38b0.tar.gz", hash = "sha256:3dbe93248eec7652d5725d3c6d2f9dd048bb8fda6b0505aadbc99e51638d833c"}, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.38b0" +requires_python = ">=3.7" +summary = "ASGI instrumentation for OpenTelemetry" +dependencies = [ + "asgiref~=3.0", + "opentelemetry-api~=1.12", + "opentelemetry-instrumentation==0.38b0", + "opentelemetry-semantic-conventions==0.38b0", + "opentelemetry-util-http==0.38b0", +] +files = [ + {file = "opentelemetry_instrumentation_asgi-0.38b0-py3-none-any.whl", hash = "sha256:c5bba11505008a3cd1b2c42b72f85f3f4f5af50ab931eddd0b01bde376dc5971"}, + {file = "opentelemetry_instrumentation_asgi-0.38b0.tar.gz", hash = "sha256:32d1034c253de6048d0d0166b304f9125267ca9329e374202ebe011a206eba53"}, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.38b0" +requires_python = ">=3.7" +summary = "OpenTelemetry FastAPI Instrumentation" +dependencies = [ + "opentelemetry-api~=1.12", + "opentelemetry-instrumentation-asgi==0.38b0", + "opentelemetry-instrumentation==0.38b0", + "opentelemetry-semantic-conventions==0.38b0", + "opentelemetry-util-http==0.38b0", +] +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.38b0-py3-none-any.whl", hash = "sha256:91139586732e437b1c3d5cf838dc5be910bce27b4b679612112be03fcc4fa2aa"}, + {file = "opentelemetry_instrumentation_fastapi-0.38b0.tar.gz", hash = "sha256:8946fd414084b305ad67556a1907e2d4a497924d023effc5ea3b4b1b0c55b256"}, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.38b0" +requires_python = ">=3.7" +summary = "OpenTelemetry Semantic Conventions" +files = [ + {file = "opentelemetry_semantic_conventions-0.38b0-py3-none-any.whl", hash = "sha256:b0ba36e8b70bfaab16ee5a553d809309cc11ff58aec3d2550d451e79d45243a7"}, + {file = "opentelemetry_semantic_conventions-0.38b0.tar.gz", hash = "sha256:37f09e47dd5fc316658bf9ee9f37f9389b21e708faffa4a65d6a3de484d22309"}, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.38b0" +requires_python = ">=3.7" +summary = "Web util for OpenTelemetry" +files = [ + {file = "opentelemetry_util_http-0.38b0-py3-none-any.whl", hash = "sha256:8e5f0451eeb5307b2c628dd799886adc5e113fb13a7207c29c672e8d168eabd8"}, + {file = "opentelemetry_util_http-0.38b0.tar.gz", hash = "sha256:85eb032b6129c4d7620583acf574e99fe2e73c33d60e256b54af436f76ceb5ae"}, +] + +[[package]] +name = "packaging" +version = "24.2" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "passlib" +version = "1.7.4" +summary = "comprehensive password hashing framework supporting over 30 schemes" +files = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] + +[[package]] +name = "pendulum" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python datetimes made easy" +dependencies = [ + "backports-zoneinfo>=0.2.1; python_version < \"3.9\"", + "importlib-resources>=5.9.0; python_version < \"3.9\"", + "python-dateutil>=2.6", + "time-machine>=2.6.0; implementation_name != \"pypy\"", + "tzdata>=2020.1", +] +files = [ + {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, + {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, + {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, + {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, + {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, + {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, + {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, + {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +requires_python = ">=3.8" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +requires_python = ">=3.8" +summary = "plugin and hook calling mechanisms for python" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[[package]] +name = "pre-commit" +version = "4.1.0" +requires_python = ">=3.9" +summary = "A framework for managing and maintaining multi-language pre-commit hooks." +dependencies = [ + "cfgv>=2.0.0", + "identify>=1.0.0", + "nodeenv>=0.11.1", + "pyyaml>=5.1", + "virtualenv>=20.10.0", +] +files = [ + {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, + {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.50" +requires_python = ">=3.8.0" +summary = "Library for building powerful interactive command lines in Python" +dependencies = [ + "wcwidth", +] +files = [ + {file = "prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"}, + {file = "prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab"}, +] + +[[package]] +name = "propcache" +version = "0.2.1" +requires_python = ">=3.9" +summary = "Accelerated property cache" +files = [ + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"}, + {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"}, + {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"}, + {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"}, + {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"}, + {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"}, + {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"}, + {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"}, + {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"}, + {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"}, + {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, +] + +[[package]] +name = "proto-plus" +version = "1.26.0rc1" +requires_python = ">=3.7" +summary = "Beautiful, Pythonic protocol buffers" +dependencies = [ + "protobuf<6.0.0dev,>=3.19.0", +] +files = [ + {file = "proto_plus-1.26.0rc1-py3-none-any.whl", hash = "sha256:a0ad6fbc2e194dbbb813edc22ee2e509a7c38df7ecea2fd2803bce0536eaf0f4"}, + {file = "proto_plus-1.26.0rc1.tar.gz", hash = "sha256:04eeceecd6a038285e2aa8996b53c045d04a568c5c48b7eaa79c097a4984a4c7"}, +] + +[[package]] +name = "protobuf" +version = "5.29.3" +requires_python = ">=3.8" +summary = "" +files = [ + {file = "protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888"}, + {file = "protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a"}, + {file = "protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f"}, + {file = "protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f"}, + {file = "protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620"}, +] + +[[package]] +name = "psycopg" +version = "3.2.4" +requires_python = ">=3.8" +summary = "PostgreSQL database adapter for Python" +dependencies = [ + "backports-zoneinfo>=0.2.0; python_version < \"3.9\"", + "typing-extensions>=4.6; python_version < \"3.13\"", + "tzdata; sys_platform == \"win32\"", +] +files = [ + {file = "psycopg-3.2.4-py3-none-any.whl", hash = "sha256:43665368ccd48180744cab26b74332f46b63b7e06e8ce0775547a3533883d381"}, + {file = "psycopg-3.2.4.tar.gz", hash = "sha256:f26f1346d6bf1ef5f5ef1714dd405c67fb365cfd1c6cea07de1792747b167b92"}, +] + +[[package]] +name = "psycopg-pool" +version = "3.2.4" +requires_python = ">=3.8" +summary = "Connection Pool for Psycopg" +dependencies = [ + "typing-extensions>=4.6", +] +files = [ + {file = "psycopg_pool-3.2.4-py3-none-any.whl", hash = "sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224"}, + {file = "psycopg_pool-3.2.4.tar.gz", hash = "sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed"}, +] + +[[package]] +name = "psycopg" +version = "3.2.4" +extras = ["async", "pool"] +requires_python = ">=3.8" +summary = "PostgreSQL database adapter for Python" +dependencies = [ + "psycopg-pool", + "psycopg==3.2.4", +] +files = [ + {file = "psycopg-3.2.4-py3-none-any.whl", hash = "sha256:43665368ccd48180744cab26b74332f46b63b7e06e8ce0775547a3533883d381"}, + {file = "psycopg-3.2.4.tar.gz", hash = "sha256:f26f1346d6bf1ef5f5ef1714dd405c67fb365cfd1c6cea07de1792747b167b92"}, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +requires_python = ">=3.8" +summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +requires_python = ">=3.8" +summary = "A collection of ASN.1-based protocols modules" +dependencies = [ + "pyasn1<0.7.0,>=0.4.6", +] +files = [ + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +requires_python = ">=3.8" +summary = "C parser in Python" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pydantic" +version = "1.10.21" +requires_python = ">=3.7" +summary = "Data validation and settings management using python type hints" +dependencies = [ + "typing-extensions>=4.2.0", +] +files = [ + {file = "pydantic-1.10.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:245e486e0fec53ec2366df9cf1cba36e0bbf066af7cd9c974bbbd9ba10e1e586"}, + {file = "pydantic-1.10.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c54f8d4c151c1de784c5b93dfbb872067e3414619e10e21e695f7bb84d1d1fd"}, + {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b64708009cfabd9c2211295144ff455ec7ceb4c4fb45a07a804309598f36187"}, + {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a148410fa0e971ba333358d11a6dea7b48e063de127c2b09ece9d1c1137dde4"}, + {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:36ceadef055af06e7756eb4b871cdc9e5a27bdc06a45c820cd94b443de019bbf"}, + {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0501e1d12df6ab1211b8cad52d2f7b2cd81f8e8e776d39aa5e71e2998d0379f"}, + {file = "pydantic-1.10.21-cp310-cp310-win_amd64.whl", hash = "sha256:c261127c275d7bce50b26b26c7d8427dcb5c4803e840e913f8d9df3f99dca55f"}, + {file = "pydantic-1.10.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b6350b68566bb6b164fb06a3772e878887f3c857c46c0c534788081cb48adf4"}, + {file = "pydantic-1.10.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:935b19fdcde236f4fbf691959fa5c3e2b6951fff132964e869e57c70f2ad1ba3"}, + {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6a04efdcd25486b27f24c1648d5adc1633ad8b4506d0e96e5367f075ed2e0b"}, + {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1ba253eb5af8d89864073e6ce8e6c8dec5f49920cff61f38f5c3383e38b1c9f"}, + {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:57f0101e6c97b411f287a0b7cf5ebc4e5d3b18254bf926f45a11615d29475793"}, + {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e85834f0370d737c77a386ce505c21b06bfe7086c1c568b70e15a568d9670d"}, + {file = "pydantic-1.10.21-cp311-cp311-win_amd64.whl", hash = "sha256:6a497bc66b3374b7d105763d1d3de76d949287bf28969bff4656206ab8a53aa9"}, + {file = "pydantic-1.10.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ed4a5f13cf160d64aa331ab9017af81f3481cd9fd0e49f1d707b57fe1b9f3ae"}, + {file = "pydantic-1.10.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b7693bb6ed3fbe250e222f9415abb73111bb09b73ab90d2d4d53f6390e0ccc1"}, + {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185d5f1dff1fead51766da9b2de4f3dc3b8fca39e59383c273f34a6ae254e3e2"}, + {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38e6d35cf7cd1727822c79e324fa0677e1a08c88a34f56695101f5ad4d5e20e5"}, + {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1d7c332685eafacb64a1a7645b409a166eb7537f23142d26895746f628a3149b"}, + {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c9b782db6f993a36092480eeaab8ba0609f786041b01f39c7c52252bda6d85f"}, + {file = "pydantic-1.10.21-cp312-cp312-win_amd64.whl", hash = "sha256:7ce64d23d4e71d9698492479505674c5c5b92cda02b07c91dfc13633b2eef805"}, + {file = "pydantic-1.10.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0067935d35044950be781933ab91b9a708eaff124bf860fa2f70aeb1c4be7212"}, + {file = "pydantic-1.10.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5e8148c2ce4894ce7e5a4925d9d3fdce429fb0e821b5a8783573f3611933a251"}, + {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4973232c98b9b44c78b1233693e5e1938add5af18042f031737e1214455f9b8"}, + {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:662bf5ce3c9b1cef32a32a2f4debe00d2f4839fefbebe1d6956e681122a9c839"}, + {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98737c3ab5a2f8a85f2326eebcd214510f898881a290a7939a45ec294743c875"}, + {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0bb58bbe65a43483d49f66b6c8474424d551a3fbe8a7796c42da314bac712738"}, + {file = "pydantic-1.10.21-cp313-cp313-win_amd64.whl", hash = "sha256:e622314542fb48542c09c7bd1ac51d71c5632dd3c92dc82ede6da233f55f4848"}, + {file = "pydantic-1.10.21-py3-none-any.whl", hash = "sha256:db70c920cba9d05c69ad4a9e7f8e9e83011abb2c6490e561de9ae24aee44925c"}, + {file = "pydantic-1.10.21.tar.gz", hash = "sha256:64b48e2b609a6c22178a56c408ee1215a7206077ecb8a193e2fda31858b2362a"}, +] + +[[package]] +name = "pygments" +version = "2.19.1" +requires_python = ">=3.8" +summary = "Pygments is a syntax highlighting package written in Python." +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +requires_python = ">=3.9" +summary = "JSON Web Token implementation in Python" +files = [ + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, +] + +[[package]] +name = "pylint" +version = "3.3.3" +requires_python = ">=3.9.0" +summary = "python code static checker" +dependencies = [ + "astroid<=3.4.0-dev0,>=3.3.8", + "colorama>=0.4.5; sys_platform == \"win32\"", + "dill>=0.2; python_version < \"3.11\"", + "dill>=0.3.6; python_version >= \"3.11\"", + "dill>=0.3.7; python_version >= \"3.12\"", + "isort!=5.13.0,<6,>=4.2.5", + "mccabe<0.8,>=0.6", + "platformdirs>=2.2.0", + "tomli>=1.1.0; python_version < \"3.11\"", + "tomlkit>=0.10.1", + "typing-extensions>=3.10.0; python_version < \"3.10\"", +] +files = [ + {file = "pylint-3.3.3-py3-none-any.whl", hash = "sha256:26e271a2bc8bce0fc23833805a9076dd9b4d5194e2a02164942cb3cdc37b4183"}, + {file = "pylint-3.3.3.tar.gz", hash = "sha256:07c607523b17e6d16e2ae0d7ef59602e332caa762af64203c24b41c27139f36a"}, +] + +[[package]] +name = "pyopenssl" +version = "24.3.0" +requires_python = ">=3.7" +summary = "Python wrapper module around the OpenSSL library" +dependencies = [ + "cryptography<45,>=41.0.5", +] +files = [ + {file = "pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a"}, + {file = "pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36"}, +] + +[[package]] +name = "pyparsing" +version = "3.2.1" +requires_python = ">=3.9" +summary = "pyparsing module - Classes and methods to define and execute parsing grammars" +files = [ + {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, + {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, +] + +[[package]] +name = "pytest" +version = "8.3.4" +requires_python = ">=3.8" +summary = "pytest: simple powerful testing with Python" +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2,>=1.5", + "tomli>=1; python_version < \"3.11\"", +] +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[[package]] +name = "pytest-asyncio" +version = "0.25.2" +requires_python = ">=3.9" +summary = "Pytest support for asyncio" +dependencies = [ + "pytest<9,>=8.2", +] +files = [ + {file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"}, + {file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"}, +] + +[[package]] +name = "pytest-integration" +version = "0.2.3" +requires_python = ">=3.6" +summary = "Organizing pytests by integration or not" +files = [ + {file = "pytest_integration-0.2.3-py3-none-any.whl", hash = "sha256:7f59ed1fa1cc8cb240f9495b68bc02c0421cce48589f78e49b7b842231604b12"}, + {file = "pytest_integration-0.2.3.tar.gz", hash = "sha256:b00988a5de8a6826af82d4c7a3485b43fbf32c11235e9f4a8b7225eef5fbcf65"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[[package]] +name = "python-dotenv" +version = "0.19.2" +requires_python = ">=3.5" +summary = "Read key-value pairs from a .env file and set them as environment variables" +files = [ + {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, + {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, +] + +[[package]] +name = "python-jose" +version = "3.3.0" +summary = "JOSE implementation in Python" +dependencies = [ + "ecdsa!=0.15", + "pyasn1", + "rsa", +] +files = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +requires_python = ">=3.8" +summary = "A streaming multipart parser for Python" +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + +[[package]] +name = "pytz" +version = "2024.2" +summary = "World timezone definitions, modern and historical" +files = [ + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] + +[[package]] +name = "pywin32" +version = "308" +summary = "Python for Window Extensions" +files = [ + {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, + {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, + {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, + {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, + {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, + {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, + {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, + {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, + {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, + {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, + {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, + {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +requires_python = ">=3.8" +summary = "YAML parser and emitter for Python" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "redis" +version = "4.6.0" +requires_python = ">=3.7" +summary = "Python client for Redis database and key-value store" +dependencies = [ + "async-timeout>=4.0.2; python_full_version <= \"3.11.2\"", + "importlib-metadata>=1.0; python_version < \"3.8\"", + "typing-extensions; python_version < \"3.8\"", +] +files = [ + {file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"}, + {file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"}, +] + +[[package]] +name = "referencing" +version = "0.36.2" +requires_python = ">=3.9" +summary = "JSON Referencing + Python" +dependencies = [ + "attrs>=22.2.0", + "rpds-py>=0.7.0", + "typing-extensions>=4.4.0; python_version < \"3.13\"", +] +files = [ + {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, + {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, +] + +[[package]] +name = "requests" +version = "2.29.0" +requires_python = ">=3.7" +summary = "Python HTTP for Humans." +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<1.27,>=1.21.1", +] +files = [ + {file = "requests-2.29.0-py3-none-any.whl", hash = "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b"}, + {file = "requests-2.29.0.tar.gz", hash = "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059"}, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +requires_python = ">=3.4" +summary = "OAuthlib authentication support for Requests." +dependencies = [ + "oauthlib>=3.0.0", + "requests>=2.0.0", +] +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[[package]] +name = "rich" +version = "13.9.4" +requires_python = ">=3.8.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", + "typing-extensions<5.0,>=4.0.0; python_version < \"3.11\"", +] +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[[package]] +name = "rpds-py" +version = "0.22.3" +requires_python = ">=3.9" +summary = "Python bindings to Rust's persistent data structures (rpds)" +files = [ + {file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"}, + {file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf"}, + {file = "rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652"}, + {file = "rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a"}, + {file = "rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64"}, + {file = "rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7"}, + {file = "rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627"}, + {file = "rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f"}, + {file = "rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de"}, + {file = "rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d"}, + {file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"}, +] + +[[package]] +name = "rsa" +version = "4.9" +requires_python = ">=3.6,<4" +summary = "Pure-Python RSA implementation" +dependencies = [ + "pyasn1>=0.1.3", +] +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[[package]] +name = "setuptools" +version = "75.8.0" +requires_python = ">=3.9" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +files = [ + {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, + {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, +] + +[[package]] +name = "six" +version = "1.17.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Python 2 and 3 compatibility utilities" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "snowflake-connector-python" +version = "3.13.0" +requires_python = ">=3.8" +summary = "Snowflake Connector for Python" +dependencies = [ + "asn1crypto<2.0.0,>0.24.0", + "certifi>=2017.4.17", + "cffi<2.0.0,>=1.9", + "charset-normalizer<4,>=2", + "cryptography>=3.1.0", + "filelock<4,>=3.5", + "idna<4,>=2.5", + "importlib-metadata; python_version < \"3.8\"", + "packaging", + "platformdirs<5.0.0,>=2.6.0", + "pyOpenSSL<25.0.0,>=22.0.0", + "pyjwt<3.0.0", + "pytz", + "requests<3.0.0", + "sortedcontainers>=2.4.0", + "tomlkit", + "typing-extensions<5,>=4.3", + "urllib3<2.0.0,>=1.21.1; python_version < \"3.10\"", +] +files = [ + {file = "snowflake_connector_python-3.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:faa37d43c81e10652ce21aefcd10dc9152cace9823fbfdc5b556698c632d1b6d"}, + {file = "snowflake_connector_python-3.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fabf573808099ff39e604ededf6e3a8da4c4a2d1d1f5a8919cdeb6962883f059"}, + {file = "snowflake_connector_python-3.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae281fb5ce0be738201c7c5cd7338315d42760e137aa305673a50301b9490c00"}, + {file = "snowflake_connector_python-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:01dfe963e206eb0e1d19a31e193036d3699fe84e1d96f4b205dfc05b795a182a"}, + {file = "snowflake_connector_python-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:065cde62168ee9bf54ddd9844c525c54e8325baa30659a3956fce256ff122108"}, + {file = "snowflake_connector_python-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1356be910c37c25b7fca1216c661dc8018b2963f7e60ce6b36bd72c2264ab04c"}, + {file = "snowflake_connector_python-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:411dc5afbf6329ae09e4ad3dd0d49a2d414803a5607628e8fc201395a07b63d6"}, + {file = "snowflake_connector_python-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:b3088ddbe4baf7f104dc70d64ae6019eb67115941c1bbd1f99fdd723f35cb25a"}, + {file = "snowflake_connector_python-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddcb362e1f2ce7564bf0bd63ee7c99616c91cdff9f415918c09346e2e835a9f"}, + {file = "snowflake_connector_python-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eab9ed9c1ffed427495bfeeedd7a96b864f8eba524d9dd7d3b225efe1a75bfb8"}, + {file = "snowflake_connector_python-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cec3bf208ecf2f95df43e31436a3dd72bb7dc7b715d67ebb5387a5da05ed3f74"}, + {file = "snowflake_connector_python-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:c06f9d5783b94dab7181bb208ec0d807a3b59b7e0b9d1e514b4794bd67cea897"}, + {file = "snowflake_connector_python-3.13.0.tar.gz", hash = "sha256:5081d21638fdda98f27be976dde6c8ca79eb8b5493cf5dfbb2614c94b6fb3e10"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +summary = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.37" +requires_python = ">=3.7" +summary = "Database Abstraction Library" +dependencies = [ + "greenlet!=0.4.17; (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version < \"3.14\"", + "importlib-metadata; python_version < \"3.8\"", + "typing-extensions>=4.6.0", +] +files = [ + {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-win32.whl", hash = "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-win_amd64.whl", hash = "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78361be6dc9073ed17ab380985d1e45e48a642313ab68ab6afa2457354ff692c"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b661b49d0cb0ab311a189b31e25576b7ac3e20783beb1e1817d72d9d02508bf5"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d57bafbab289e147d064ffbd5cca2d7b1394b63417c0636cea1f2e93d16eb9e8"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2c0913f02341d25fb858e4fb2031e6b0813494cca1ba07d417674128ce11b"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9df21b8d9e5c136ea6cde1c50d2b1c29a2b5ff2b1d610165c23ff250e0704087"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db18ff6b8c0f1917f8b20f8eca35c28bbccb9f83afa94743e03d40203ed83de9"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-win32.whl", hash = "sha256:46954173612617a99a64aee103bcd3f078901b9a8dcfc6ae80cbf34ba23df989"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-win_amd64.whl", hash = "sha256:7b7e772dc4bc507fdec4ee20182f15bd60d2a84f1e087a8accf5b5b7a0dcf2ba"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2952748ecd67ed3b56773c185e85fc084f6bdcdec10e5032a7c25a6bc7d682ef"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3151822aa1db0eb5afd65ccfafebe0ef5cda3a7701a279c8d0bf17781a793bb4"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaa8039b6d20137a4e02603aba37d12cd2dde7887500b8855356682fc33933f4"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cdba1f73b64530c47b27118b7053b8447e6d6f3c8104e3ac59f3d40c33aa9fd"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1b2690456528a87234a75d1a1644cdb330a6926f455403c8e4f6cad6921f9098"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf5ae8a9dcf657fd72144a7fd01f243236ea39e7344e579a121c4205aedf07bb"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-win32.whl", hash = "sha256:ea308cec940905ba008291d93619d92edaf83232ec85fbd514dcb329f3192761"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-win_amd64.whl", hash = "sha256:635d8a21577341dfe4f7fa59ec394b346da12420b86624a69e466d446de16aff"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c4096727193762e72ce9437e2a86a110cf081241919ce3fab8e89c02f6b6658"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4fb5ac86d8fe8151966814f6720996430462e633d225497566b3996966b9bdb"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e56a139bfe136a22c438478a86f8204c1eb5eed36f4e15c4224e4b9db01cb3e4"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f95fc8e3f34b5f6b3effb49d10ac97c569ec8e32f985612d9b25dd12d0d2e94"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c505edd429abdfe3643fa3b2e83efb3445a34a9dc49d5f692dd087be966020e0"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12b0f1ec623cccf058cf21cb544f0e74656618165b083d78145cafde156ea7b6"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-win32.whl", hash = "sha256:293f9ade06b2e68dd03cfb14d49202fac47b7bb94bffcff174568c951fbc7af2"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-win_amd64.whl", hash = "sha256:d70f53a0646cc418ca4853da57cf3ddddbccb8c98406791f24426f2dd77fd0e2"}, + {file = "SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1"}, + {file = "sqlalchemy-2.0.37.tar.gz", hash = "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb"}, +] + +[[package]] +name = "sqlalchemy-utils" +version = "0.41.2" +requires_python = ">=3.7" +summary = "Various utility functions for SQLAlchemy." +dependencies = [ + "SQLAlchemy>=1.3", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990"}, + {file = "SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e"}, +] + +[[package]] +name = "sse-starlette" +version = "2.0.0" +requires_python = ">=3.8" +summary = "SSE plugin for Starlette" +dependencies = [ + "anyio", + "starlette", + "uvicorn", +] +files = [ + {file = "sse_starlette-2.0.0-py3-none-any.whl", hash = "sha256:c4dd134302cb9708d47cae23c365fe0a089aa2a875d2f887ac80f235a9ee5744"}, + {file = "sse_starlette-2.0.0.tar.gz", hash = "sha256:0c43cc43aca4884c88c8416b65777c4de874cc4773e6458d3579c0a353dc2fb7"}, +] + +[[package]] +name = "starlette" +version = "0.45.3" +requires_python = ">=3.9" +summary = "The little ASGI library that shines." +dependencies = [ + "anyio<5,>=3.6.2", + "typing-extensions>=3.10.0; python_version < \"3.10\"", +] +files = [ + {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"}, + {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"}, +] + +[[package]] +name = "strawberry-graphql" +version = "0.258.0" +requires_python = "<4.0,>=3.9" +summary = "A library for creating GraphQL APIs" +dependencies = [ + "graphql-core<3.4.0,>=3.2.0", + "python-dateutil<3.0.0,>=2.7.0", + "typing-extensions>=4.5.0", +] +files = [ + {file = "strawberry_graphql-0.258.0-py3-none-any.whl", hash = "sha256:041adda6e9a97ca337793f6c07fe4db0a9793226907394a00022782f132040ec"}, + {file = "strawberry_graphql-0.258.0.tar.gz", hash = "sha256:3975c638f751e9b87cefd5eb1a29c1f33e639b1f218f199578114fb839dec94c"}, +] + +[[package]] +name = "tenacity" +version = "9.0.0" +requires_python = ">=3.8" +summary = "Retry code until it succeeds" +files = [ + {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, + {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, +] + +[[package]] +name = "time-machine" +version = "2.16.0" +requires_python = ">=3.9" +summary = "Travel through time in your tests." +dependencies = [ + "python-dateutil", +] +files = [ + {file = "time_machine-2.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09531af59fdfb39bfd24d28bd1e837eff5a5d98318509a31b6cfd57d27801e52"}, + {file = "time_machine-2.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:92d0b0f3c49f34dd76eb462f0afdc61ed1cb318c06c46d03e99b44ebb489bdad"}, + {file = "time_machine-2.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c29616e18e2349a8766d5b6817920fc74e39c00fa375d202231e9d525a1b882"}, + {file = "time_machine-2.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1ceb6035a64cb00650e3ab203cf3faffac18576a3f3125c24df468b784077c7"}, + {file = "time_machine-2.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64c205ea37b8c4ba232645335fc3b75bc2d03ce30f0a34649e36cae85652ee96"}, + {file = "time_machine-2.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dfe92412bd11104c4f0fb2da68653e6c45b41f7217319a83a8b66ed4f20148b3"}, + {file = "time_machine-2.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d5fe7a6284e3dce87ae13a25029c53542dd27a28d151f3ef362ec4dd9c3e45fd"}, + {file = "time_machine-2.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0fca3025266d88d1b48be162a43b7c2d91c81cc5b3bee9f01194678ffb9969a"}, + {file = "time_machine-2.16.0-cp310-cp310-win32.whl", hash = "sha256:4149e17018af07a5756a1df84aea71e6e178598c358c860c6bfec42170fa7970"}, + {file = "time_machine-2.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:01bc257e9418980a4922de94775be42a966e1a082fb01a1635917f9afc7b84ca"}, + {file = "time_machine-2.16.0-cp310-cp310-win_arm64.whl", hash = "sha256:6895e3e84119594ab12847c928f619d40ae9cedd0755515dc154a5b5dc6edd9f"}, + {file = "time_machine-2.16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8f936566ef9f09136a3d5db305961ef6d897b76b240c9ff4199144aed6dd4fe5"}, + {file = "time_machine-2.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5886e23ede3478ca2a3e0a641f5d09dd784dfa9e48c96e8e5e31fc4fe77b6dc0"}, + {file = "time_machine-2.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76caf539fa4941e1817b7c482c87c65c52a1903fea761e84525955c6106fafb"}, + {file = "time_machine-2.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:298aa423e07c8b21b991782f01d7749c871c792319c2af3e9755f9ab49033212"}, + {file = "time_machine-2.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391ae9c484736850bb44ef125cbad52fe2d1b69e42c95dc88c43af8ead2cc7"}, + {file = "time_machine-2.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:503e7ff507c2089699d91885fc5b9c8ff16774a7b6aff48b4dcee0c0a0685b61"}, + {file = "time_machine-2.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eee7b0fc4fbab2c6585ea17606c6548be83919c70deea0865409fe9fc2d8cdce"}, + {file = "time_machine-2.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9db5e5b3ccdadaafa5730c2f9db44c38b013234c9ad01f87738907e19bdba268"}, + {file = "time_machine-2.16.0-cp311-cp311-win32.whl", hash = "sha256:2552f0767bc10c9d668f108fef9b487809cdeb772439ce932e74136365c69baf"}, + {file = "time_machine-2.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:12474fcdbc475aa6fe5275fe7224e685c5b9777f5939647f35980e9614ae7558"}, + {file = "time_machine-2.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:ac2df0fa564356384515ed62cb6679f33f1f529435b16b0ec0f88414635dbe39"}, + {file = "time_machine-2.16.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:84788f4d62a8b1bf5e499bb9b0e23ceceea21c415ad6030be6267ce3d639842f"}, + {file = "time_machine-2.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:15ec236b6571730236a193d9d6c11d472432fc6ab54e85eac1c16d98ddcd71bf"}, + {file = "time_machine-2.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cedc989717c8b44a3881ac3d68ab5a95820448796c550de6a2149ed1525157f0"}, + {file = "time_machine-2.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d26d79de1c63a8c6586c75967e09b0ff306aa7e944a1eaddb74595c9b1839ca"}, + {file = "time_machine-2.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:317b68b56a9c3731e0cf8886e0f94230727159e375988b36c60edce0ddbcb44a"}, + {file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:43e1e18279759897be3293a255d53e6b1cb0364b69d9591d0b80c51e461c94b0"}, + {file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e43adb22def972a29d2b147999b56897116085777a0fea182fd93ee45730611e"}, + {file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0c766bea27a0600e36806d628ebc4b47178b12fcdfb6c24dc0a566a9c06bfe7f"}, + {file = "time_machine-2.16.0-cp312-cp312-win32.whl", hash = "sha256:6dae82ab647d107817e013db82223e20a9853fa88543fec853ae326382d03c2e"}, + {file = "time_machine-2.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:265462c77dc9576267c3c7f20707780a171a9fdbac93ac22e608c309efd68c33"}, + {file = "time_machine-2.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:ef768e14768eebe3bb1196c0dece8e14c1c6991605721214a0c3c68cf77eb216"}, + {file = "time_machine-2.16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7751bf745d54e9e8b358c0afa332815da9b8a6194b26d0fd62876ab6c4d5c9c0"}, + {file = "time_machine-2.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1784edf173ca840ba154de6eed000b5727f65ab92972c2f88cec5c4d6349c5f2"}, + {file = "time_machine-2.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f5876a5682ce1f517e55d7ace2383432627889f6f7e338b961f99d684fd9e8d"}, + {file = "time_machine-2.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:806672529a2e255cd901f244c9033767dc1fa53466d0d3e3e49565a1572a64fe"}, + {file = "time_machine-2.16.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:667b150fedb54acdca2a4bea5bf6da837b43e6dd12857301b48191f8803ba93f"}, + {file = "time_machine-2.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:da3ae1028af240c0c46c79adf9c1acffecc6ed1701f2863b8132f5ceae6ae4b5"}, + {file = "time_machine-2.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:520a814ea1b2706c89ab260a54023033d3015abef25c77873b83e3d7c1fafbb2"}, + {file = "time_machine-2.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8243664438bb468408b29c6865958662d75e51f79c91842d2794fa22629eb697"}, + {file = "time_machine-2.16.0-cp313-cp313-win32.whl", hash = "sha256:32d445ce20d25c60ab92153c073942b0bac9815bfbfd152ce3dcc225d15ce988"}, + {file = "time_machine-2.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:f6927dda86425f97ffda36131f297b1a601c64a6ee6838bfa0e6d3149c2f0d9f"}, + {file = "time_machine-2.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:4d3843143c46dddca6491a954bbd0abfd435681512ac343169560e9bab504129"}, + {file = "time_machine-2.16.0.tar.gz", hash = "sha256:4a99acc273d2f98add23a89b94d4dd9e14969c01214c8514bfa78e4e9364c7e2"}, +] + +[[package]] +name = "toml" +version = "0.10.2" +requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python Library for Tom's Obvious, Minimal Language" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +requires_python = ">=3.8" +summary = "A lil' TOML parser" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +requires_python = ">=3.8" +summary = "Style preserving TOML library" +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +requires_python = ">=3.8" +summary = "Traitlets Python configuration system" +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + +[[package]] +name = "trino" +version = "0.324.0" +requires_python = ">=3.7" +summary = "Client for the Trino distributed SQL Engine" +dependencies = [ + "backports-zoneinfo; python_version < \"3.9\"", + "python-dateutil", + "pytz", + "requests", + "tzlocal", +] +files = [ + {file = "trino-0.324.0-py3-none-any.whl", hash = "sha256:19390848b8a88dcd7691e5a8a2d722bca96ec1cfa509e8b4d904977202599147"}, + {file = "trino-0.324.0.tar.gz", hash = "sha256:aa4d25376529dbfc5b5d0968e808438f923eb7e296b1bc091ed0e3fb5a6957ad"}, +] + +[[package]] +name = "types-cachetools" +version = "5.5.0.20240820" +requires_python = ">=3.8" +summary = "Typing stubs for cachetools" +files = [ + {file = "types-cachetools-5.5.0.20240820.tar.gz", hash = "sha256:b888ab5c1a48116f7799cd5004b18474cd82b5463acb5ffb2db2fc9c7b053bc0"}, + {file = "types_cachetools-5.5.0.20240820-py3-none-any.whl", hash = "sha256:efb2ed8bf27a4b9d3ed70d33849f536362603a90b8090a328acf0cd42fda82e2"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2025.1" +requires_python = ">=2" +summary = "Provider of IANA time zone data" +files = [ + {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, + {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, +] + +[[package]] +name = "tzlocal" +version = "5.2" +requires_python = ">=3.8" +summary = "tzinfo object for the local timezone" +dependencies = [ + "backports-zoneinfo; python_version < \"3.9\"", + "tzdata; platform_system == \"Windows\"", +] +files = [ + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, +] + +[[package]] +name = "uritemplate" +version = "4.1.1" +requires_python = ">=3.6" +summary = "Implementation of RFC 6570 URI Templates" +files = [ + {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, + {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +files = [ + {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, + {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, +] + +[[package]] +name = "uvicorn" +version = "0.34.0" +requires_python = ">=3.9" +summary = "The lightning-fast ASGI server." +dependencies = [ + "click>=7.0", + "h11>=0.8", + "typing-extensions>=4.0; python_version < \"3.11\"", +] +files = [ + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, +] + +[[package]] +name = "vine" +version = "5.1.0" +requires_python = ">=3.6" +summary = "Python promises." +files = [ + {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, + {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, +] + +[[package]] +name = "virtualenv" +version = "20.29.1" +requires_python = ">=3.8" +summary = "Virtual Python Environment builder" +dependencies = [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.12.2", + "importlib-metadata>=6.6; python_version < \"3.8\"", + "platformdirs<5,>=3.9.1", +] +files = [ + {file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"}, + {file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"}, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +summary = "Measures the displayed width of unicode strings in a terminal" +dependencies = [ + "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", +] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "wrapt" +version = "1.17.2" +requires_python = ">=3.8" +summary = "Module for decorators, wrappers and monkey patching." +files = [ + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, + {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, + {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, + {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, + {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, + {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, + {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, + {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, + {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, + {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, + {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, + {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, + {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, +] + +[[package]] +name = "yarl" +version = "1.18.3" +requires_python = ">=3.9" +summary = "Yet another URL library" +dependencies = [ + "idna>=2.0", + "multidict>=4.0", + "propcache>=0.2.0", +] +files = [ + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, + {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, + {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, + {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, + {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, + {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, + {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, + {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, + {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, + {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, + {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, +] + +[[package]] +name = "zipp" +version = "3.21.0" +requires_python = ">=3.9" +summary = "Backport of pathlib-compatible object wrapper for zip files" +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] diff --git a/pyproject.toml b/pyproject.toml index 399c8648f..33ca66954 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,12 @@ -[build-system] -# AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD! -requires = ["setuptools>=46.1.0,<50", "setuptools_scm[toml]>=5", "wheel"] -build-backend = "setuptools.build_meta" +[project] +dependencies = [] +requires-python = ">=3.10,<4.0" -[tool.setuptools_scm] -# See configuration details in https://github.com/pypa/setuptools_scm -version_scheme = "no-guess-dev" +[tool.pdm.dev-dependencies] +dev = [ + "pre-commit>=3.2.2", + "pylint>=2.17.3", + "-e datajunction-server @ file:///${PROJECT_ROOT}/datajunction-server", + "-e datajunction-query @ file:///${PROJECT_ROOT}/datajunction-query", + "-e datajunction-reflection @ file:///${PROJECT_ROOT}/datajunction-reflection", +] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 592334a6b..000000000 --- a/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -# -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: -# -# pip-compile --no-annotate -# -colorama==0.4.4 -commonmark==0.9.1 -docopt==0.6.2 -greenlet==1.1.2 -pydantic==1.8.2 -pygments==2.10.0 -pyyaml==5.4.1 -rich==10.16.1 -sqlalchemy==1.4.25 -sqlalchemy2-stubs==0.0.2a19 -sqlmodel==0.0.5 -typing-extensions==4.0.1 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e55277d35..000000000 --- a/setup.cfg +++ /dev/null @@ -1,139 +0,0 @@ -# This file is used to configure your project. -# Read more about the various options under: -# https://setuptools.readthedocs.io/en/stable/userguide/declarative_config.html - -[metadata] -name = datajunction -description = Add a short description here! -author = Beto Dealmeida -author_email = roberto@dealmeida.net -license = MIT -long_description = file: README.rst -long_description_content_type = text/x-rst; charset=UTF-8 -url = https://github.com/pyscaffold/pyscaffold/ -# Add here related links, for example: -project_urls = - Documentation = https://pyscaffold.org/ -# Source = https://github.com/pyscaffold/pyscaffold/ -# Changelog = https://pyscaffold.org/en/latest/changelog.html -# Tracker = https://github.com/pyscaffold/pyscaffold/issues -# Conda-Forge = https://anaconda.org/conda-forge/pyscaffold -# Download = https://pypi.org/project/PyScaffold/#files -# Twitter = https://twitter.com/PyScaffold - -# Change if running only on Windows, Mac or Linux (comma-separated) -platforms = any - -# Add here all kinds of additional classifiers as defined under -# https://pypi.org/classifiers/ -classifiers = - Development Status :: 4 - Beta - Programming Language :: Python - - -[options] -zip_safe = False -packages = find_namespace: -include_package_data = True -package_dir = - =src - -# Require a min/specific Python version (comma-separated conditions) -# python_requires = >=3.8 - -# Add here dependencies of your project (line-separated), e.g. requests>=2.2,<3.0. -# Version specifiers like >=2.2,<3.0 avoid problems due to API changes in -# new major versions. This works if the required packages follow Semantic Versioning. -# For more information, check out https://semver.org/. -install_requires = - importlib-metadata; python_version<"3.8" - docopt==0.6.2 - rich>=10.9.0,<11 - PyYAML>=5.4.1,<6 - sqlmodel==0.0.5 - - -[options.packages.find] -where = src -exclude = - tests - -[options.extras_require] -# Add here additional requirements for extra features, to install with: -# `pip install datajunction[PDF]` like: -# PDF = ReportLab; RXP - -# Add here test requirements (semicolon/line-separated) -testing = - setuptools>=49.6.0,<50 - pytest>=6.2.5,<7 - pytest-cov>=2.12.1,<3 - pytest-mock>=3.6.1,<4 - pyfakefs>=4.5.1,<5 - pre-commit>=2.15.0,<3 - freezegun>=1.1.0,<2 - pytest-asyncio==0.15.1 - codespell>=2.1.0,<3 - pylint>=2.11.1,<3 - pip-tools>=6.4.0,<7 -docs = - sphinx>=4.1.2,<5 - -[options.entry_points] -# Add here console scripts like: -# console_scripts = -# script_name = datajunction.module:function -# For example: -# console_scripts = -# fibonacci = datajunction.skeleton:run -# And any other entry points, for example: -# pyscaffold.cli = -# awesome = pyscaffoldext.awesome.extension:AwesomeExtension -console_scripts = - dj = datajunction.console:run - -[tool:pytest] -# Specify command line options as you would do when invoking pytest directly. -# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml -# in order to write a coverage file that can be read by Jenkins. -# CAUTION: --cov flags may prohibit setting breakpoints while debugging. -# Comment those flags to avoid this pytest issue. -addopts = - --cov datajunction --cov-report term-missing - --verbose -norecursedirs = - dist - build - .tox -testpaths = tests -# Use pytest markers to select/deselect specific tests -# markers = -# slow: mark tests as slow (deselect with '-m "not slow"') -# system: mark end-to-end system tests - -[devpi:upload] -# Options for the devpi: PyPI server and packaging tool -# VCS export must be deactivated since we are using setuptools-scm -no_vcs = 1 -formats = bdist_wheel - -[flake8] -# Some sane defaults for the code style checker flake8 -max_line_length = 88 -extend_ignore = E203, W503 -# ^ Black-compatible -# E203 and W503 have edge cases handled by black -exclude = - .tox - build - dist - .eggs - docs/conf.py - -[pyscaffold] -# PyScaffold's parameters when the project was created. -# This will be used when updating. Do not change! -version = 4.1 -package = datajunction -extensions = - pre_commit diff --git a/setup.py b/setup.py deleted file mode 100644 index c614bb0cb..000000000 --- a/setup.py +++ /dev/null @@ -1,21 +0,0 @@ -""" - Setup file for datajunction. - Use setup.cfg to configure your project. - - This file was generated with PyScaffold 4.1. - PyScaffold helps you to put up the scaffold of your new Python project. - Learn more under: https://pyscaffold.org/ -""" -from setuptools import setup - -if __name__ == "__main__": - try: - setup(use_scm_version={"version_scheme": "no-guess-dev"}) - except: # noqa - print( - "\n\nAn error occurred while building the project, " - "please ensure you have the most updated version of setuptools, " - "setuptools_scm and wheel with:\n" - " pip install -U setuptools setuptools_scm wheel\n\n", - ) - raise diff --git a/src/datajunction/__init__.py b/src/datajunction/__init__.py deleted file mode 100644 index dc8d37ac0..000000000 --- a/src/datajunction/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -Package version and name. -""" -# pylint: disable=fixme - -import sys - -if sys.version_info[:2] >= (3, 8): - # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8` - from importlib.metadata import PackageNotFoundError, version # pragma: no cover -else: - from importlib_metadata import PackageNotFoundError, version # pragma: no cover - -try: - # Change here if project is renamed and does not equal the package name - DIST_NAME = __name__ - __version__ = version(DIST_NAME) -except PackageNotFoundError: # pragma: no cover - __version__ = "unknown" -finally: - del version, PackageNotFoundError diff --git a/src/datajunction/cli/compile.py b/src/datajunction/cli/compile.py deleted file mode 100644 index 672135b42..000000000 --- a/src/datajunction/cli/compile.py +++ /dev/null @@ -1,238 +0,0 @@ -""" -Compile a metrics repository. - -This will: - - 1. Build graph of nodes. - 2. Retrieve the schema of source nodes. - 3. Infer the schema of downstream nodes. - 4. Save everything to the DB. - -""" - -import asyncio -import logging -from datetime import datetime, timezone -from pathlib import Path -from typing import Any, Dict, List, Optional - -import yaml -from sqlalchemy import inspect -from sqlmodel import Session, SQLModel, create_engine, select - -from datajunction.models import ( - Column, - Database, - Node, - Representation, - get_name_from_path, -) -from datajunction.utils import load_config - -_logger = logging.getLogger(__name__) - - -def load_data(path: Path) -> Dict[str, Any]: - """ - Load data from a YAML file. - """ - with open(path, encoding="utf-8") as input_: - data = yaml.safe_load(input_) - - return data - - -def get_more_specific_type(current_type: Optional[str], new_type: str) -> str: - """ - Given two types, return the most specific one. - - Different databases might store the same column as different types. For example, Hive - might store timestamps as strings, while Postgres would store the same data as a - datetime. - - >>> get_more_specific_type('str', 'datetime') - 'datetime' - >>> get_more_specific_type('str', 'int') - 'int' - - """ - if current_type is None: - return new_type - - hierarchy = [ - "bytes", - "str", - "float", - "int", - "Decimal", - "bool", - "datetime", - "date", - "time", - "timedelta", - "list", - "dict", - ] - - return sorted([current_type, new_type], key=hierarchy.index)[1] - - -async def index_databases(repository: Path, session: Session) -> List[Database]: - """ - Index all the databases. - """ - directory = repository / "databases" - - async def add_from_path(path: Path) -> Database: - name = get_name_from_path(repository, path) - _logger.info("Processing database %s", name) - - # check if the database was already indexed and if it's up-to-date - query = select(Database).where(Database.name == name) - database = session.exec(query).one_or_none() - if database: - # compare file modification time with timestamp on DB - mtime = path.stat().st_mtime - - # some DBs like SQLite will drop the timezone info; in that case - # we assume it's UTC - if database.updated_at.tzinfo is None: - database.updated_at = database.updated_at.replace(tzinfo=timezone.utc) - - if database.updated_at > datetime.fromtimestamp(mtime, tz=timezone.utc): - _logger.info("Database %s is up-to-date, skipping", name) - return database - - # delete existing database - created_at = database.created_at - session.delete(database) - session.flush() - else: - created_at = None - - _logger.info("Loading database from config %s", path) - data = load_data(path) - - _logger.info("Creating database %s", name) - data["name"] = name - data["created_at"] = created_at or datetime.now(timezone.utc) - data["updated_at"] = datetime.now(timezone.utc) - database = Database(**data) - - session.add(database) - session.flush() - - return database - - tasks = [add_from_path(path) for path in directory.glob("**/*.yaml")] - databases = await asyncio.gather(*tasks) - - return databases - - -def get_columns(representations: List[Representation]) -> List[Column]: - """ - Fetch all columns from a list of representations. - """ - columns: Dict[str, Column] = {} - for representation in representations: - engine = create_engine(representation.database.URI) - inspector = inspect(engine) - for column in inspector.get_columns( - representation.table, - schema=representation.schema_, - ): - name = column["name"] - type_ = column["type"].python_type.__name__ - - columns[name] = Column( - name=name, - type=get_more_specific_type(columns[name].type, type_) - if name in columns - else type_, - ) - - return list(columns.values()) - - -async def index_nodes(repository: Path, session: Session) -> List[Node]: - """ - Index all the nodes, computing their schema. - """ - # load all databases - databases = { - database.name: database for database in session.exec(select(Database)).all() - } - - directory = repository / "nodes" - - async def add_from_path(path: Path) -> Node: - name = get_name_from_path(repository, path) - _logger.info("Processing node %s", name) - - # check if the node was already indexed and if it's up-to-date - query = select(Node).where(Node.name == name) - node = session.exec(query).one_or_none() - if node: - # compare file modification time with timestamp on DB - mtime = path.stat().st_mtime - - # some DBs like SQLite will drop the timezone info; in that case - # we assume it's UTC - if node.updated_at.tzinfo is None: - node.updated_at = node.updated_at.replace(tzinfo=timezone.utc) - - if node.updated_at > datetime.fromtimestamp(mtime, tz=timezone.utc): - _logger.info("Node %s is up-do-date, skipping", name) - return node - - # delete existing node - created_at = node.created_at - session.delete(node) - session.flush() - else: - created_at = None - - _logger.info("Loading node from config %s", path) - data = load_data(path) - - # create representations and columns - representations = [] - for database_name, representation_data in data["representations"].items(): - representation_data["database"] = databases[database_name] - representation = Representation(**representation_data) - representations.append(representation) - data["representations"] = representations - data["columns"] = get_columns(representations) - - _logger.info("Creating node %s", name) - data["name"] = name - data["created_at"] = created_at or datetime.now(timezone.utc) - data["updated_at"] = datetime.now(timezone.utc) - node = Node(**data) - - session.add(node) - session.flush() - - return node - - tasks = [add_from_path(path) for path in directory.glob("**/*.yaml")] - nodes = await asyncio.gather(*tasks) - - return nodes - - -async def run(repository: Path) -> None: - """ - Compile the metrics repository. - """ - config = load_config(repository) - - engine = create_engine(config.index) - SQLModel.metadata.create_all(engine) - - with Session(engine) as session: - await index_databases(repository, session) - await index_nodes(repository, session) - - session.commit() diff --git a/src/datajunction/console.py b/src/datajunction/console.py deleted file mode 100644 index 99ddbfbe2..000000000 --- a/src/datajunction/console.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -DataJunction (DJ) is a metric repository. - -Usage: - dj compile [REPOSITORY] [--loglevel=INFO] - -Actions: - compile Compile repository - -Options: - --loglevel=LEVEL Level for logging. [default: INFO] - -Released under the MIT license. -(c) 2018 Beto Dealmeida -""" - -import asyncio -import logging -import os -from pathlib import Path - -from docopt import docopt - -from datajunction import __version__ -from datajunction.cli import compile as compile_ -from datajunction.utils import find_directory, setup_logging - -_logger = logging.getLogger(__name__) - - -async def main() -> None: - """ - Dispatch command. - """ - arguments = docopt(__doc__, version=__version__) - - setup_logging(arguments["--loglevel"]) - - if arguments["REPOSITORY"] is None: - repository = find_directory(Path(os.getcwd())) - else: - repository = Path(arguments["REPOSITORY"]) - - try: - if arguments["compile"]: - await compile_.run(repository) - except asyncio.CancelledError: - _logger.info("Canceled") - - -def run() -> None: - """ - Run the DJ CLI. - """ - try: - asyncio.run(main()) - except KeyboardInterrupt: - _logger.info("Stopping DJ") - - -if __name__ == "__main__": - run() diff --git a/src/datajunction/constants.py b/src/datajunction/constants.py deleted file mode 100644 index f1949d3ca..000000000 --- a/src/datajunction/constants.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Useful constants. -""" - -CONFIG_FILENAME = "dj.yaml" diff --git a/src/datajunction/models.py b/src/datajunction/models.py deleted file mode 100644 index 32645389a..000000000 --- a/src/datajunction/models.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -Models for nodes. -""" - -import os -from datetime import datetime, timezone -from functools import partial -from pathlib import Path -from typing import List, Optional - -from sqlalchemy import String -from sqlalchemy.sql.schema import Column as SqlaColumn -from sqlmodel import Field, Relationship, SQLModel - - -def get_name_from_path(repository: Path, path: Path) -> str: - """ - Compute the name of a node given its path and the repository path. - """ - # strip anything before the repository - relative_path = path.relative_to(repository) - - if len(relative_path.parts) < 2 or relative_path.parts[0] not in { - "nodes", - "databases", - }: - raise Exception(f"Invalid path: {path}") - - # remove the "nodes" directory from the path - relative_path = relative_path.relative_to(relative_path.parts[0]) - - # remove extension - relative_path = relative_path.with_suffix("") - - # encode percent symbols and periods - encoded = ( - str(relative_path) - .replace("%", "%25") - .replace(".", "%2E") - .replace(os.path.sep, ".") - ) - - return encoded - - -class Config(SQLModel): - """ - Configuration for a metric repository. - """ - - index: str - - -class Database(SQLModel, table=True): # type: ignore - """ - A database. - - A simple example:: - - name: druid - description: An Apache Druid database - URI: druid://localhost:8082/druid/v2/sql/ - read-only: true - - """ - - id: Optional[int] = Field(default=None, primary_key=True) - created_at: datetime = Field(default_factory=partial(datetime.now, timezone.utc)) - updated_at: datetime = Field(default_factory=partial(datetime.now, timezone.utc)) - name: str - description: str = "" - URI: str - read_only: bool = True - - representations: List["Representation"] = Relationship( - back_populates="database", - sa_relationship_kwargs={"cascade": "all, delete"}, - ) - - -class Node(SQLModel, table=True): # type: ignore - """ - A node. - """ - - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(sa_column=SqlaColumn("name", String, unique=True)) - description: str = "" - - created_at: datetime = Field(default_factory=partial(datetime.now, timezone.utc)) - updated_at: datetime = Field(default_factory=partial(datetime.now, timezone.utc)) - - expression: Optional[str] = None - - # schema - columns: List["Column"] = Relationship( - back_populates="node", - sa_relationship_kwargs={"cascade": "all, delete"}, - ) - - # storages - representations: List["Representation"] = Relationship( - back_populates="node", - sa_relationship_kwargs={"cascade": "all, delete"}, - ) - - -class Representation(SQLModel, table=True): # type: ignore - """ - A representation of data. - - Nodes can have multiple representations of data, in different databases. - """ - - id: Optional[int] = Field(default=None, primary_key=True) - - node_id: int = Field(foreign_key="node.id") - node: Node = Relationship(back_populates="representations") - - database_id: int = Field(foreign_key="database.id") - database: Database = Relationship(back_populates="representations") - catalog: Optional[str] = None - schema_: Optional[str] = Field(None, alias="schema") - table: str - - cost: float = 1.0 - - # aggregation_level => for materialized metrics? - - -class Column(SQLModel, table=True): # type: ignore - """ - A column. - """ - - id: Optional[int] = Field(default=None, primary_key=True) - name: str - type: str - - # only-on => for columns that are present in only a few DBs - - node_id: int = Field(foreign_key="node.id") - node: Node = Relationship(back_populates="columns") diff --git a/src/datajunction/utils.py b/src/datajunction/utils.py deleted file mode 100644 index 6814fa7db..000000000 --- a/src/datajunction/utils.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Utility functions. -""" - -import logging -from pathlib import Path - -import yaml -from rich.logging import RichHandler - -from datajunction.constants import CONFIG_FILENAME -from datajunction.models import Config - - -def setup_logging(loglevel: str) -> None: - """ - Setup basic logging. - """ - level = getattr(logging, loglevel.upper(), None) - if not isinstance(level, int): - raise ValueError(f"Invalid log level: {loglevel}") - - logformat = "[%(asctime)s] %(levelname)s: %(name)s: %(message)s" - logging.basicConfig( - level=level, - format=logformat, - datefmt="[%X]", - handlers=[RichHandler()], - force=True, - ) - - -def find_directory(cwd: Path) -> Path: - """ - Find root of the metrics repository, starting from `cwd`. - - The function will traverse up trying to find a configuration file. - """ - while not (cwd / CONFIG_FILENAME).exists(): - if cwd == cwd.parent: - raise SystemExit("No configuration found!") - cwd = cwd.parent - - return cwd - - -def load_config(repository: Path) -> Config: - """ - Return the configuration for a metrics repository. - """ - path = repository / CONFIG_FILENAME - if not path.exists(): - raise SystemExit("No configuration found!") - - with open(path, encoding="utf-8") as input_: - config_data = yaml.safe_load(input_) - - config = Config(**config_data) - - return config - - -def get_project_repository() -> Path: - """ - Return the project repository. - - This is used for unit tests. - """ - return Path(__file__).parent.parent.parent diff --git a/talks/2023-03-02/DJ@Netflix - A DJ Meetup.pdf b/talks/2023-03-02/DJ@Netflix - A DJ Meetup.pdf new file mode 100644 index 000000000..115555190 Binary files /dev/null and b/talks/2023-03-02/DJ@Netflix - A DJ Meetup.pdf differ diff --git a/tests/cli/compile_test.py b/tests/cli/compile_test.py deleted file mode 100644 index 268b94f76..000000000 --- a/tests/cli/compile_test.py +++ /dev/null @@ -1,245 +0,0 @@ -""" -Tests for ``datajunction.cli.compile``. -""" -# pylint: disable=redefined-outer-name, invalid-name - -from datetime import datetime, timezone -from operator import itemgetter -from pathlib import Path - -import pytest -import sqlalchemy -from freezegun import freeze_time -from pyfakefs.fake_filesystem import FakeFilesystem -from pytest_mock import MockerFixture -from sqlmodel import Session - -from datajunction.cli.compile import ( - get_columns, - get_more_specific_type, - index_databases, - index_nodes, - load_data, - run, -) -from datajunction.models import Column, Database, Node, Representation - - -def test_get_more_specific_type() -> None: - """ - Test ``get_more_specific_type``. - """ - assert get_more_specific_type("str", "datetime") == "datetime" - assert get_more_specific_type("str", "int") == "int" - assert get_more_specific_type(None, "int") == "int" - - -def test_load_data(fs: FakeFilesystem) -> None: - """ - Test ``load_data``. - """ - fs.create_file( - "/path/to/repository/dj.yaml", - contents="foo: bar", - ) - assert load_data(Path("/path/to/repository/dj.yaml")) == {"foo": "bar"} - - -@pytest.mark.asyncio -async def test_index_databases(repository: Path, session: Session) -> None: - """ - Test ``index_databases``. - """ - with freeze_time("2021-01-01T00:00:00Z"): - Path("/path/to/repository/databases/druid.yaml").touch() - Path("/path/to/repository/databases/postgres.yaml").touch() - Path("/path/to/repository/databases/gsheets.yaml").touch() - - with freeze_time("2021-01-02T00:00:00Z"): - databases = await index_databases(repository, session) - - configs = [database.dict(exclude={"id": True}) for database in databases] - assert sorted(configs, key=itemgetter("name")) == [ - { - "created_at": datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc), - "updated_at": datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc), - "name": "druid", - "description": "An Apache Druid database", - "URI": "druid://localhost:8082/druid/v2/sql/", - "read_only": True, - }, - { - "created_at": datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc), - "updated_at": datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc), - "name": "gsheets", - "description": "A Google Sheets connector", - "URI": "gsheets://", - "read_only": True, - }, - { - "created_at": datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc), - "updated_at": datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc), - "name": "postgres", - "description": "A Postgres database", - "URI": "postgresql://username:FoolishPassword@localhost:5433/examples", - "read_only": False, - }, - ] - - # update the Druid database and reindex - with freeze_time("2021-01-03T00:00:00Z"): - Path("/path/to/repository/databases/druid.yaml").touch() - databases = await index_databases(repository, session) - databases = sorted(databases, key=lambda database: database.name) - - assert [(database.name, database.updated_at) for database in databases] == [ - ("druid", datetime(2021, 1, 3, 0, 0, tzinfo=timezone.utc)), - ("gsheets", datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc)), - ("postgres", datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc)), - ] - - # test that a missing timezone is treated as UTC - databases[0].updated_at = databases[0].updated_at.replace(tzinfo=None) - with freeze_time("2021-01-03T00:00:00Z"): - databases = await index_databases(repository, session) - databases = sorted(databases, key=lambda database: database.name) - - assert [(database.name, database.updated_at) for database in databases] == [ - ("druid", datetime(2021, 1, 3, 0, 0, tzinfo=timezone.utc)), - ("gsheets", datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc)), - ("postgres", datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc)), - ] - - -def test_get_columns(mocker: MockerFixture) -> None: - """ - Test ``get_columns``. - """ - mocker.patch("datajunction.cli.compile.create_engine") - inspect = mocker.patch("datajunction.cli.compile.inspect") - inspect.return_value.get_columns.side_effect = [ - [ - {"name": "ds", "type": sqlalchemy.sql.sqltypes.String()}, - {"name": "cnt", "type": sqlalchemy.sql.sqltypes.Integer()}, - ], - [ - {"name": "ds", "type": sqlalchemy.sql.sqltypes.DateTime()}, - {"name": "cnt", "type": sqlalchemy.sql.sqltypes.Float()}, - ], - ] - - representations = [mocker.MagicMock(), mocker.MagicMock()] - assert get_columns(representations) == [ - Column(id=None, name="ds", type="datetime"), - Column(id=None, name="cnt", type="int"), - ] - - -@pytest.mark.asyncio -async def test_index_nodes( - mocker: MockerFixture, - repository: Path, - session: Session, -) -> None: - """ - Test ``index_nodes``. - """ - mocker.patch( - "datajunction.cli.compile.get_columns", - side_effect=[ - [ - Column(id=None, name="ds", type="datetime"), - Column(id=None, name="cnt", type="int"), - ], - [ - Column(id=None, name="ds", type="datetime"), - Column(id=None, name="cnt", type="int"), - ], - [ - Column(id=None, name="ds", type="datetime"), - Column(id=None, name="cnt", type="int"), - ], - [ - Column(id=None, name="ds", type="datetime"), - Column(id=None, name="cnt", type="int"), - ], - ], - ) - - session.add(Database(name="druid", URI="druid://localhost:8082/druid/v2/sql/")) - session.add( - Database( - name="postgres", - URI="postgresql://username:FoolishPassword@localhost:5433/examples", - ), - ) - session.add(Database(name="gsheets", URI="gsheets://")) - - with freeze_time("2021-01-01T00:00:00Z"): - Path("/path/to/repository/nodes/core/comments.yaml").touch() - Path("/path/to/repository/nodes/core/users.yaml").touch() - - with freeze_time("2021-01-02T00:00:00Z"): - nodes = await index_nodes(repository, session) - - configs = [node.dict(exclude={"id": True}) for node in nodes] - assert sorted(configs, key=itemgetter("name")) == [ - { - "name": "core.comments", - "description": "A fact table with comments", - "created_at": datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc), - "updated_at": datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc), - "expression": None, - }, - { - "name": "core.users", - "description": "A user dimension table", - "created_at": datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc), - "updated_at": datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc), - "expression": None, - }, - ] - - # update one of the nodes and reindex - with freeze_time("2021-01-03T00:00:00Z"): - Path("/path/to/repository/nodes/core/users.yaml").touch() - nodes = await index_nodes(repository, session) - nodes = sorted(nodes, key=lambda node: node.name) - - assert [(node.name, node.updated_at) for node in nodes] == [ - ("core.comments", datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc)), - ("core.users", datetime(2021, 1, 3, 0, 0, tzinfo=timezone.utc)), - ] - - # test that a missing timezone is treated as UTC - nodes[0].updated_at = nodes[0].updated_at.replace(tzinfo=None) - with freeze_time("2021-01-03T00:00:00Z"): - nodes = await index_nodes(repository, session) - nodes = sorted(nodes, key=lambda node: node.name) - - assert [(node.name, node.updated_at) for node in nodes] == [ - ("core.comments", datetime(2021, 1, 2, 0, 0, tzinfo=timezone.utc)), - ("core.users", datetime(2021, 1, 3, 0, 0, tzinfo=timezone.utc)), - ] - - -@pytest.mark.asyncio -async def test_run(mocker: MockerFixture, repository: Path) -> None: - """ - Test the ``run`` command. - """ - mocker.patch("datajunction.cli.compile.create_engine") - mocker.patch("datajunction.cli.compile.SQLModel") - - Session = mocker.patch("datajunction.cli.compile.Session") - session = Session.return_value.__enter__.return_value - - index_databases = mocker.patch("datajunction.cli.compile.index_databases") - index_nodes = mocker.patch("datajunction.cli.compile.index_nodes") - - await run(repository) - - index_databases.assert_called_with(repository, session) - index_nodes.assert_called_with(repository, session) - - session.commit.assert_called() diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index aa5a6845d..000000000 --- a/tests/conftest.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Fixtures for testing. -""" -# pylint: disable=redefined-outer-name, invalid-name - -from pathlib import Path -from typing import Iterator - -import pytest -from pyfakefs.fake_filesystem import FakeFilesystem -from sqlmodel import Session, SQLModel, create_engine - -from datajunction.models import Config -from datajunction.utils import get_project_repository, load_config - - -@pytest.fixture -def repository(fs: FakeFilesystem) -> Iterator[Path]: - """ - Create the main repository. - """ - # add the examples repository to the fake filesystem - repository = get_project_repository() - fs.add_real_directory( - repository / "examples/configs", - target_path="/path/to/repository", - ) - - path = Path("/path/to/repository") - yield path - - -@pytest.fixture -def config(repository: Path) -> Iterator[Config]: - """ - Load the configuration for a given repository. - """ - yield load_config(repository) - - -@pytest.fixture() -def session() -> Iterator[Session]: - """ - Create an in-memory SQLite session to test models. - """ - engine = create_engine("sqlite://") - SQLModel.metadata.create_all(engine) - - with Session(engine) as session: - yield session diff --git a/tests/console_test.py b/tests/console_test.py deleted file mode 100644 index 5b314446c..000000000 --- a/tests/console_test.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Tests for ``datajunction.console``. -""" - -import asyncio -from pathlib import Path - -import pytest -from pytest_mock import MockerFixture - -from datajunction import console - - -@pytest.mark.asyncio -async def test_main_compile(mocker: MockerFixture) -> None: - """ - Test ``main`` with the "compile" action. - """ - compile_ = mocker.patch("datajunction.console.compile_") - compile_.run = mocker.AsyncMock() - - mocker.patch( - "datajunction.console.docopt", - return_value={ - "--loglevel": "debug", - "compile": True, - "REPOSITORY": None, - }, - ) - mocker.patch( - "datajunction.console.find_directory", - return_value=Path("/path/to/repository"), - ) - - await console.main() - compile_.run.assert_called_with(Path("/path/to/repository")) - - -@pytest.mark.asyncio -async def test_main_compile_passing_repository(mocker: MockerFixture) -> None: - """ - Test ``main`` with the "compile" action. - """ - compile_ = mocker.patch("datajunction.console.compile_") - compile_.run = mocker.AsyncMock() - - mocker.patch( - "datajunction.console.docopt", - return_value={ - "--loglevel": "debug", - "compile": True, - "REPOSITORY": "/path/to/another/repository", - }, - ) - - await console.main() - compile_.run.assert_called_with(Path("/path/to/another/repository")) - - -@pytest.mark.asyncio -async def test_main_canceled(mocker: MockerFixture) -> None: - """ - Test canceling the ``main`` coroutine. - """ - compile_ = mocker.patch("datajunction.console.compile_") - compile_.run = mocker.AsyncMock(side_effect=asyncio.CancelledError("Canceled")) - _logger = mocker.patch("datajunction.console._logger") - - mocker.patch( - "datajunction.console.docopt", - return_value={ - "--loglevel": "debug", - "compile": True, - "REPOSITORY": "/path/to/another/repository", - }, - ) - - await console.main() - - _logger.info.assert_called_with("Canceled") - - -@pytest.mark.asyncio -async def test_main_no_action(mocker: MockerFixture) -> None: - """ - Test ``main`` without any actions -- should not happen. - """ - - mocker.patch( - "datajunction.console.docopt", - return_value={ - "--loglevel": "debug", - "compile": False, - "REPOSITORY": "/path/to/another/repository", - }, - ) - - await console.main() - - -def test_run(mocker: MockerFixture) -> None: - """ - Test ``run``. - """ - main = mocker.AsyncMock() - mocker.patch("datajunction.console.main", main) - - console.run() - - main.assert_called() - - -def test_interrupt(mocker: MockerFixture) -> None: - """ - Test that ``CTRL-C`` stops the CLI. - """ - main = mocker.AsyncMock(side_effect=KeyboardInterrupt()) - mocker.patch("datajunction.console.main", main) - _logger = mocker.patch("datajunction.console._logger") - - console.run() - - _logger.info.assert_called_with("Stopping DJ") diff --git a/tests/models_test.py b/tests/models_test.py deleted file mode 100644 index 5190c6bea..000000000 --- a/tests/models_test.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Tests for ``datajuntion.models``. -""" - -from pathlib import Path - -import pytest - -from datajunction.models import get_name_from_path - - -def test_get_name_from_path() -> None: - """ - Test ``get_name_from_path``. - """ - with pytest.raises(Exception) as excinfo: - get_name_from_path(Path("/path/to/repository"), Path("/path/to/repository")) - assert str(excinfo.value) == "Invalid path: /path/to/repository" - - with pytest.raises(Exception) as excinfo: - get_name_from_path( - Path("/path/to/repository"), - Path("/path/to/repository/nodes"), - ) - assert str(excinfo.value) == "Invalid path: /path/to/repository/nodes" - - with pytest.raises(Exception) as excinfo: - get_name_from_path( - Path("/path/to/repository"), - Path("/path/to/repository/invalid/test.yaml"), - ) - assert str(excinfo.value) == "Invalid path: /path/to/repository/invalid/test.yaml" - - assert ( - get_name_from_path( - Path("/path/to/repository"), - Path("/path/to/repository/nodes/test.yaml"), - ) - == "test" - ) - - assert ( - get_name_from_path( - Path("/path/to/repository"), - Path("/path/to/repository/nodes/core/test.yaml"), - ) - == "core.test" - ) - - assert ( - get_name_from_path( - Path("/path/to/repository"), - Path("/path/to/repository/nodes/dev.nodes/test.yaml"), - ) - == "dev%2Enodes.test" - ) - - assert ( - get_name_from_path( - Path("/path/to/repository"), - Path("/path/to/repository/nodes/5%_nodes/test.yaml"), - ) - == "5%25_nodes.test" - ) diff --git a/tests/utils_test.py b/tests/utils_test.py deleted file mode 100644 index c32565ff8..000000000 --- a/tests/utils_test.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Tests for ``datajunction.utils``. -""" - -import logging -from pathlib import Path - -import pytest -from pyfakefs.fake_filesystem import FakeFilesystem - -from datajunction.models import Config -from datajunction.utils import find_directory, load_config, setup_logging - - -def test_setup_logging() -> None: - """ - Test ``setup_logging``. - """ - setup_logging("debug") - assert logging.root.level == logging.DEBUG - - with pytest.raises(ValueError) as excinfo: - setup_logging("invalid") - assert str(excinfo.value) == "Invalid log level: invalid" - - -def test_find_directory(fs: FakeFilesystem) -> None: # pylint: disable=invalid-name - """ - Test ``find_directory``. - """ - fs.create_dir("/path/to/repository/nodes/core") - fs.create_file("/path/to/repository/dj.yaml") - - path = find_directory(Path("/path/to/repository/nodes/core")) - assert path == Path("/path/to/repository") - - with pytest.raises(SystemExit) as excinfo: - find_directory(Path("/path/to")) - assert str(excinfo.value) == "No configuration found!" - - -def test_load_config(repository: Path, config: Config) -> None: - """ - Test ``load_config``. - """ - config = load_config(repository) - assert config.dict() == {"index": "sqlite:///dj.db"} - - with pytest.raises(SystemExit) as excinfo: - load_config(Path("/path/to")) - assert str(excinfo.value) == "No configuration found!" diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..2c09c5c5e --- /dev/null +++ b/tox.ini @@ -0,0 +1,18 @@ +[tox] +envlist = py3 + +[testenv] +pip_pre = true +deps = + -rrequirements/test.txt + pytest + testfixtures + coverage +commands = + pip install -e .[testing] + coverage run --source dj --parallel-mode -m pytest {posargs} --without-integration --without-slow-integration + coverage html --fail-under 100 -d test-reports/{envname}/coverage-html + +[flake8] +per-file-ignores = + datajunction-server/datajunction_server/sql/functions.py:F811