├── .gitignore ├── invoice-processing-pipeline ├── .gcloudignore ├── uploader │ ├── requirements.txt │ ├── templates │ │ └── index.html │ ├── Dockerfile │ └── main.py ├── processor │ ├── requirements.txt │ ├── Dockerfile │ ├── helpers.py │ ├── deploy.cloudbuild.yaml │ ├── main.py │ └── process.py ├── incoming │ ├── eager-soy-7087.pdf │ ├── bold-harbor-9397.pdf │ ├── brass-curve-1311.pdf │ ├── chief-fruit-1296.pdf │ ├── free-wasabi-1570.pdf │ ├── internal-bit-9601.pdf │ ├── kind-camera-2069.pdf │ ├── latent-beef-7784.pdf │ ├── primary-film-7926.pdf │ ├── bent-apparition-4244.pdf │ ├── buoyant-wetland-3799.pdf │ ├── creative-center-9036.pdf │ ├── decidable-duck-1038.pdf │ ├── district-curve-4969.pdf │ ├── feasible-door-3062.pdf │ ├── foggy-executive-6661.pdf │ ├── gold-parakeet-9573.pdf │ ├── huge-interval-1322.pdf │ ├── humid-rectangle-1584.pdf │ ├── magnetic-vector-3156.pdf │ ├── natural-search-9170.pdf │ ├── solid-category-8831.pdf │ ├── solid-dataframe-5985.pdf │ ├── stern-kilometer-9179.pdf │ ├── blistering-grouse-1266.pdf │ ├── congruent-buffalo-2185.pdf │ ├── equidistant-root-1097.pdf │ ├── frigid-broadcast-2396.pdf │ ├── internal-allegory-3577.pdf │ ├── optical-sandcrab-5095.pdf │ ├── pleasant-incircle-4916.pdf │ ├── plum-partnership-3384.pdf │ └── rectilinear-starter-6340.pdf ├── reviewer │ ├── requirements.txt │ ├── Dockerfile │ ├── templates │ │ └── list.html │ └── main.py └── README.md ├── screenshot ├── .gcloudignore ├── package.json ├── Dockerfile ├── README.md └── screenshot.js ├── parallel-processing ├── requirements.txt ├── Procfile ├── README.md ├── deploy-parallel-job.sh └── process.py ├── user-journeys ├── Dockerfile ├── package.json ├── journeys │ ├── example.json │ └── go-to-cloud-run-pricing.json ├── README.md ├── replay_every_day.sh ├── replay_on_gcp.sh ├── runner.js └── package-lock.json ├── .github └── dependabot.yml ├── CONTRIBUTING.md ├── README.md ├── CODE_OF_CONDUCT.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** -------------------------------------------------------------------------------- /invoice-processing-pipeline/.gcloudignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | -------------------------------------------------------------------------------- /screenshot/.gcloudignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /parallel-processing/requirements.txt: -------------------------------------------------------------------------------- 1 | google-cloud-storage==2.3.0 -------------------------------------------------------------------------------- /invoice-processing-pipeline/uploader/requirements.txt: -------------------------------------------------------------------------------- 1 | flask>=2.0.0 2 | google-cloud-storage>=2.0.0 3 | gunicorn>=20.0.0 -------------------------------------------------------------------------------- /invoice-processing-pipeline/processor/requirements.txt: -------------------------------------------------------------------------------- 1 | google-cloud-documentai==1.2.1 2 | google-cloud-storage==2.0.0 3 | google-cloud-firestore==2.3.4 -------------------------------------------------------------------------------- /parallel-processing/Procfile: -------------------------------------------------------------------------------- 1 | # Buildpacks require a web process to be defined 2 | # but this process will not be used. 3 | web: echo "no web" 4 | 5 | python: python -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/eager-soy-7087.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/eager-soy-7087.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/reviewer/requirements.txt: -------------------------------------------------------------------------------- 1 | flask>=2.0.0 2 | google-auth>=2.0.0 3 | google-cloud-firestore>=2.0.0 4 | google-cloud-storage>=2.0.0 5 | gunicorn>=20.0.0 -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/bold-harbor-9397.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/bold-harbor-9397.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/brass-curve-1311.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/brass-curve-1311.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/chief-fruit-1296.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/chief-fruit-1296.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/free-wasabi-1570.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/free-wasabi-1570.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/internal-bit-9601.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/internal-bit-9601.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/kind-camera-2069.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/kind-camera-2069.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/latent-beef-7784.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/latent-beef-7784.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/primary-film-7926.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/primary-film-7926.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/bent-apparition-4244.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/bent-apparition-4244.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/buoyant-wetland-3799.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/buoyant-wetland-3799.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/creative-center-9036.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/creative-center-9036.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/decidable-duck-1038.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/decidable-duck-1038.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/district-curve-4969.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/district-curve-4969.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/feasible-door-3062.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/feasible-door-3062.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/foggy-executive-6661.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/foggy-executive-6661.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/gold-parakeet-9573.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/gold-parakeet-9573.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/huge-interval-1322.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/huge-interval-1322.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/humid-rectangle-1584.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/humid-rectangle-1584.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/magnetic-vector-3156.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/magnetic-vector-3156.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/natural-search-9170.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/natural-search-9170.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/solid-category-8831.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/solid-category-8831.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/solid-dataframe-5985.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/solid-dataframe-5985.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/stern-kilometer-9179.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/stern-kilometer-9179.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/blistering-grouse-1266.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/blistering-grouse-1266.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/congruent-buffalo-2185.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/congruent-buffalo-2185.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/equidistant-root-1097.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/equidistant-root-1097.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/frigid-broadcast-2396.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/frigid-broadcast-2396.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/internal-allegory-3577.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/internal-allegory-3577.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/optical-sandcrab-5095.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/optical-sandcrab-5095.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/pleasant-incircle-4916.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/pleasant-incircle-4916.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/plum-partnership-3384.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/plum-partnership-3384.pdf -------------------------------------------------------------------------------- /invoice-processing-pipeline/incoming/rectilinear-starter-6340.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/jobs-demos/HEAD/invoice-processing-pipeline/incoming/rectilinear-starter-6340.pdf -------------------------------------------------------------------------------- /user-journeys/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/puppeteer/puppeteer:17.1.3 2 | COPY package*.json ./ 3 | RUN npm ci --omit=dev 4 | COPY --chown=pptruser:pptruser . . 5 | ENTRYPOINT ["node", "runner.js"] 6 | -------------------------------------------------------------------------------- /screenshot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "screenshot", 3 | "version": "1.0.0", 4 | "description": "Create a job to capture screenshots", 5 | "main": "screenshot.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Google LLC", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "@google-cloud/storage": "^5.18.2", 13 | "puppeteer": "^15.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /invoice-processing-pipeline/uploader/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 | 14 |
15 | 16 |
17 |
18 | 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | commit-message: 11 | prefix: "chore(deps): " 12 | rebase-strategy: "disabled" 13 | schedule: 14 | interval: "monthly" 15 | ignore: 16 | - dependency-name: "*" 17 | update-types: ["version-update:semver-patch"] # Security updates are unaffected by this setting 18 | -------------------------------------------------------------------------------- /user-journeys/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user-journeys-demo", 3 | "version": "1.0.0", 4 | "description": "Replay recorded user journeys of your website on Cloud Run jobs.", 5 | "main": "runner.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/GoogleCloudPlatform/jobs-demos.git" 12 | }, 13 | "author": "Steren Giannini ", 14 | "license": "Apache-2.0", 15 | "bugs": { 16 | "url": "https://github.com/GoogleCloudPlatform/jobs-demos/issues" 17 | }, 18 | "homepage": "https://github.com/GoogleCloudPlatform/jobs-demos/tree/main/user-journeys#readme", 19 | "dependencies": { 20 | "@puppeteer/replay": "^1.2.0", 21 | "puppeteer": "^17.1.3" 22 | }, 23 | "type": "module" 24 | } 25 | -------------------------------------------------------------------------------- /invoice-processing-pipeline/processor/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Use the official lightweight Python image. 16 | # https://hub.docker.com/_/python 17 | FROM python:3.10-buster 18 | 19 | ENV PYTHONUNBUFFERED True 20 | 21 | # Copy local code to the container image. 22 | ENV APP_HOME /app 23 | WORKDIR $APP_HOME 24 | COPY . ./ 25 | 26 | # Install production dependencies. 27 | RUN pip install -r requirements.txt 28 | 29 | CMD ["/usr/local/bin/python3", "main.py"] -------------------------------------------------------------------------------- /invoice-processing-pipeline/reviewer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Use the official lightweight Python image. 16 | # https://hub.docker.com/_/python 17 | FROM python:3.9-slim 18 | 19 | ENV PYTHONUNBUFFERED True 20 | 21 | # Copy local code to the container image. 22 | ENV APP_HOME /app 23 | WORKDIR $APP_HOME 24 | COPY . ./ 25 | 26 | # Install production dependencies. 27 | RUN pip install -r requirements.txt 28 | 29 | CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app -------------------------------------------------------------------------------- /invoice-processing-pipeline/uploader/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Use the official lightweight Python image. 16 | # https://hub.docker.com/_/python 17 | FROM python:3.9-slim 18 | 19 | ENV PYTHONUNBUFFERED True 20 | 21 | # Copy local code to the container image. 22 | ENV APP_HOME /app 23 | WORKDIR $APP_HOME 24 | COPY . ./ 25 | 26 | # Install production dependencies. 27 | RUN pip install -r requirements.txt 28 | 29 | CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app -------------------------------------------------------------------------------- /screenshot/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM ghcr.io/puppeteer/puppeteer:16.1.0 16 | 17 | # Copy application dependency manifests to the container image. 18 | # A wildcard is used to ensure both package.json AND package-lock.json are copied. 19 | # Copying this separately prevents re-running npm install on every code change. 20 | COPY package*.json ./ 21 | 22 | # Install production dependencies. 23 | RUN npm ci --omit=dev 24 | 25 | # Copy all scripts 26 | COPY . . 27 | 28 | ENTRYPOINT ["node", "screenshot.js"] -------------------------------------------------------------------------------- /user-journeys/journeys/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "example", 3 | "steps": [ 4 | { 5 | "type": "setViewport", 6 | "width": 1254, 7 | "height": 721, 8 | "deviceScaleFactor": 1, 9 | "isMobile": false, 10 | "hasTouch": false, 11 | "isLandscape": false 12 | }, 13 | { 14 | "type": "navigate", 15 | "url": "https://example.com/", 16 | "assertedEvents": [ 17 | { 18 | "type": "navigation", 19 | "url": "https://example.com/", 20 | "title": "Example Domain" 21 | } 22 | ] 23 | }, 24 | { 25 | "type": "click", 26 | "selectors": [ 27 | [ 28 | "aria/More information..." 29 | ], 30 | [ 31 | "body > div > p:nth-child(3) > a" 32 | ] 33 | ], 34 | "target": "main", 35 | "offsetX": 115, 36 | "offsetY": 11.791656494140625, 37 | "assertedEvents": [ 38 | { 39 | "type": "navigation", 40 | "url": "https://www.iana.org/domains/reserved", 41 | "title": "IANA-managed Reserved Domains" 42 | } 43 | ] 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code Reviews 20 | 21 | All submissions, including submissions by project members, require review. We 22 | use GitHub pull requests for this purpose. Consult 23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 24 | information on using pull requests. 25 | 26 | ## Community Guidelines 27 | 28 | This project follows 29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloud Run Jobs demos 2 | 3 | [Cloud Run Jobs](https://cloud.google.com/run/docs/) allows you to run a container to completion without a server. 4 | 5 | This repository contains a collection of samples for Jobs for various use cases. 6 | 7 | ## Samples 8 | 9 | | Sample | Description | 10 | | ---------------------------------------- | --------------------------------------------------------------- | 11 | | [Screenshot](./screenshot/) | Create a Cloud Run job to take screenshots of web pages. | 12 | | [User Journey Replayer](./user-journeys/)| Replay recorded user journeys of your website on Cloud Run jobs.| 13 | | [Invoice Processing](./invoice-processing-pipeline/)| Process invoices nightly from a GCS bucket.| 14 | | [Parallel Processing](./parallel-processing/) | Use the Task Index and Task Count environment variables to allow parallel processing in Cloud Run Jobs. | 15 | 16 | ## Contributing changes 17 | 18 | Bug fixes are welcome, either as pull 19 | requests or as GitHub issues. 20 | 21 | See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute. 22 | 23 | ## Licensing 24 | 25 | Code in this repository is licensed under the Apache 2.0. See [LICENSE](LICENSE). 26 | 27 | ------- 28 | 29 | This is not an official Google product. 30 | -------------------------------------------------------------------------------- /invoice-processing-pipeline/processor/helpers.py: -------------------------------------------------------------------------------- 1 | # /usr/env/python3 2 | # Copyright 2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import google.auth 17 | import requests 18 | 19 | METADATA_URI = "http://metadata.google.internal/computeMetadata/v1/" 20 | 21 | 22 | def get_project_id() -> str: 23 | """Use the 'google-auth-library' to make a request to the metadata server or 24 | default to Application Default Credentials in your local environment.""" 25 | _, project = google.auth.default() 26 | return project 27 | 28 | 29 | def get_service_region() -> str: 30 | """Get region from local metadata server 31 | Region in format: projects/PROJECT_NUMBER/regions/REGION""" 32 | slug = "instance/region" 33 | data = requests.get(METADATA_URI + slug, headers={"Metadata-Flavor": "Google"}) 34 | return data.content 35 | 36 | -------------------------------------------------------------------------------- /user-journeys/README.md: -------------------------------------------------------------------------------- 1 | # User Journeys Replayer 2 | 3 | This demo shows how to replay recorded user journeys of your website on Cloud Run jobs. 4 | 5 | ## Record your user journeys 6 | 7 | 1. Use [Chrome DevTools' Recorder](https://developer.chrome.com/docs/devtools/recorder/) to record critical user journeys for your publicly accessible website. 8 | 1. Export the replay to JSON using DevTools' Recorder [export feature](https://developer.chrome.com/docs/devtools/recorder/#export-flows) 9 | 1. Save the exported `.json` file under the `journeys/` folder. 10 | 11 | ## Before you begin 12 | 13 | 1. Install the [`gcloud` command line](https://cloud.google.com/sdk/docs/install). 14 | 1. Create a Google Cloud project. 15 | 1. Set your current project in `gcloud`: 16 | ``` 17 | gcloud config set project PROJECT_ID 18 | ``` 19 | 20 | ## Replaying on Google Cloud 21 | 22 | Run `./replay_on_gcp.sh` to setup and run a Cloud Run job to replay critical 23 | user journeys in multiple tasks. The number of user journeys must match the 24 | number of tasks. See [replay_on_gcp.sh](replay_on_gcp.sh) for details. 25 | 26 | ## Replaying every day 27 | 28 | Run `./replay_every_day.sh` to create a Cloud Scheduler Job that will run the 29 | Cloud Run Job every day. See [replay_every_day.sh](replay_every_day.sh) for 30 | details. 31 | 32 | ## Testing locally 33 | 34 | The following steps assume you have `docker` installed on your local machine. If you don't proceed to the next section to deploy to Google Cloud. 35 | 36 | Build with: 37 | 38 | ```sh 39 | docker build . -t user-journeys-demo 40 | ``` 41 | 42 | Run locally: 43 | 44 | ```sh 45 | docker run --cap-add=SYS_ADMIN user-journeys-demo 46 | ``` 47 | -------------------------------------------------------------------------------- /invoice-processing-pipeline/processor/deploy.cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - id: "Build Container Image" 17 | name: "gcr.io/cloud-builders/docker:latest" 18 | args: [ 'build', '-t', 'gcr.io/$PROJECT_ID/$_SERVICE_NAME:$BUILD_ID', '.' ] 19 | 20 | - id: "Push Container Image" 21 | name: "gcr.io/cloud-builders/docker:latest" 22 | args: [ 'push', "gcr.io/$PROJECT_ID/$_SERVICE_NAME:$BUILD_ID"] 23 | 24 | - id: "Deploy to Cloud Run" 25 | name: "gcr.io/cloud-builders/gcloud:latest" 26 | entrypoint: /bin/bash 27 | args: 28 | - "-c" 29 | - | 30 | gcloud beta run jobs create invoice-processing \ 31 | --image gcr.io/$PROJECT_ID/$_SERVICE_NAME \ 32 | --region us-central1 \ 33 | --execution-environment gen2 34 | 35 | gcloud beta run jobs update invoice-processing \ 36 | --image gcr.io/$PROJECT_ID/$_SERVICE_NAME:$BUILD_ID \ 37 | --region us-central1 \ 38 | --update-env-vars PROCESSOR_ID=$_PROCESSOR_ID \ 39 | --update-env-vars BUCKET=$_BUCKET 40 | 41 | gcloud beta run jobs execute invoice-processing --region us-central1 42 | 43 | 44 | substitutions: 45 | _SERVICE_NAME: invoice-processor 46 | _BUCKET: run-jobs-friction-invoices 47 | _PROCESSOR_ID: 46bfd13ce436d58 -------------------------------------------------------------------------------- /user-journeys/replay_every_day.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | export PROJECT_ID=$(gcloud config get-value project) 18 | 19 | # Choose us-central1 if REGION is not defined. 20 | export REGION=${REGION:=us-central1} 21 | 22 | echo "Replaying every day" 23 | 24 | echo "Create a new service account" 25 | gcloud iam service-accounts create job-runner --description="Can run Cloud Run Jobs" 26 | 27 | echo "Grant this Service account the permission to run the Cloud Run job" 28 | gcloud projects add-iam-policy-binding ${PROJECT_ID} \ 29 | --member="serviceAccount:job-runner@${PROJECT_ID}.iam.gserviceaccount.com" \ 30 | --role="roles/run.invoker" 31 | 32 | echo "Create a Cloud Scheduler Job that will run the Cloud Run Job everyday" 33 | gcloud scheduler jobs create http job-runner \ 34 | --location "${REGION}" \ 35 | --schedule='0 12 * * *' \ 36 | --uri=https://${REGION}-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/${PROJECT_ID}/jobs/user-journeys-demo:run \ 37 | --message-body='' \ 38 | --oauth-service-account-email=job-runner@${PROJECT_ID}.iam.gserviceaccount.com \ 39 | --oauth-token-scope=https://www.googleapis.com/auth/cloud-platform 40 | 41 | echo "Test that Cloud Scheduler can correctly run the Cloud Run job" 42 | gcloud scheduler jobs execute job-runner --location "${REGION}" -------------------------------------------------------------------------------- /parallel-processing/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Parallel Processing 3 | 4 | This demo shows how to use the Task Index and Task Count environment variables to allow parallel processing in Cloud Run Jobs 5 | 6 | 7 | ## Background 8 | 9 | Typical batch processing systems would have a set of work being sent to a processing node, which would process the entire payload. By setting the `--tasks` in Cloud Run Jobs, you can have a number of tasks happening at once, but they would have the same input arguments, so batch separation needs to be determined by the task itself. 10 | 11 | The Task Index (`CLOUD_RUN_TASK_INDEX`) variable identifies the index of a worker, and the Task Count (`CLOUD_RUN_TASK_COUNT`) variable holds the value of `--tasks`. Using these two values, the data to be processed can be split into `task_count` chunks, with each worker performing the `task_index` element. This ensures that the entire data set that needs to be processed is separated between all tasks, and no data is processed twice. 12 | 13 | In this sample, the data set is a single Cloud Storage object containing a list of inputs to be processed. Each task takes the index and count information, splits the contents of the object into chunks, and processes one chunk as determined by it's index. 14 | 15 | You can adapt this model further, such as processing a chunk of Cloud Storage objects, a chunk of records in a Cloud SQL database, etc. 16 | 17 | 18 | ## Before you begin 19 | 20 | 21 | 22 | 1. Install the [gcloud](https://cloud.google.com/sdk/docs/install) command line. 23 | 2. Create a Google Cloud project. 24 | 3. Set your current project in gcloud: 25 | ``` 26 | gcloud config set project PROJECT_ID 27 | ``` 28 | 29 | 30 | ## Deploying the sample 31 | 32 | Run `./deploy-parallel-job.sh` to setup and run a job that will parallel process a large Cloud Storage object. See [deploy-parallel-job.sh](deploy-parallel-job.sh) for details 33 | -------------------------------------------------------------------------------- /screenshot/README.md: -------------------------------------------------------------------------------- 1 | # Screenshot Job 2 | 3 | Create a Cloud Run job to take screenshots of web pages. 4 | 5 | See the full [codelab](https://codelabs.developers.google.com/codelabs/cloud-starting-cloudrun-jobs#0). 6 | 7 | * Setup gcloud 8 | ``` 9 | PROJECT_ID=[YOUR-PROJECT-ID] 10 | REGION=us-central1 11 | gcloud config set core/project $PROJECT_ID 12 | gcloud config set run/region $REGION 13 | ``` 14 | 15 | * Enable APIs 16 | ``` 17 | gcloud services enable \ 18 | artifactregistry.googleapis.com \ 19 | cloudbuild.googleapis.com \ 20 | run.googleapis.com 21 | ``` 22 | 23 | * Create a Artifact Registry repository 24 | ``` 25 | gcloud artifacts repositories create containers --repository-format=docker --location=$REGION 26 | ``` 27 | 28 | * Build the container image 29 | ``` 30 | gcloud builds submit -t $REGION-docker.pkg.dev/$PROJECT_ID/containers/screenshot:v1 31 | ``` 32 | 33 | * Create a service account for the job's identity 34 | ``` 35 | gcloud iam service-accounts create screenshot-sa --display-name="Screenshot app service account" 36 | ``` 37 | 38 | * Grant the service account permissions to access Cloud Storage 39 | ``` 40 | gcloud projects add-iam-policy-binding $PROJECT_ID \ 41 | --role roles/storage.admin \ 42 | --member serviceAccount:screenshot-sa@$PROJECT_ID.iam.gserviceaccount.com 43 | ``` 44 | 45 | * Create the Cloud Run job 46 | ``` 47 | gcloud run jobs create screenshot \ 48 | --image=$REGION-docker.pkg.dev/$PROJECT_ID/containers/screenshot:v1 \ 49 | --args="screenshot.js" \ 50 | --args="https://example.com" \ 51 | --args="https://cloud.google.com" \ 52 | --tasks=2 \ 53 | --task-timeout=5m \ 54 | --set-env-vars=BUCKET_NAME=screenshot-$PROJECT_ID \ 55 | --service-account=screenshot-sa@$PROJECT_ID.iam.gserviceaccount.com 56 | ``` 57 | 58 | * Run the job 59 | ``` 60 | gcloud run jobs execute screenshot 61 | ``` 62 | 63 | * Describe the execution 64 | ``` 65 | gcloud run executions describe 66 | ``` -------------------------------------------------------------------------------- /parallel-processing/deploy-parallel-job.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | export PROJECT_ID=$(gcloud config get project) 18 | export REGION=${REGION:=us-central1} # default us-central1 region if not defined 19 | 20 | JOB_NAME=parallel-job 21 | NUM_TASKS=10 22 | 23 | IMAGE_NAME=gcr.io/${PROJECT_ID}/${JOB_NAME} 24 | 25 | INPUT_FILE=input_file.txt 26 | INPUT_BUCKET=input-${PROJECT_ID} 27 | 28 | echo "Configure gcloud to use $REGION for Cloud Run" 29 | gcloud config set run/region ${REGION} 30 | 31 | echo "Enabling required services" 32 | gcloud services enable \ 33 | run.googleapis.com \ 34 | cloudbuild.googleapis.com 35 | 36 | echo "Build sample into a container" 37 | gcloud builds submit --pack image=$IMAGE_NAME 38 | 39 | echo "Creating input bucket $INPUT_BUCKET and generating random data." 40 | gsutil mb gs://${INPUT_BUCKET} 41 | base64 /dev/urandom | head -c 100000 >${INPUT_FILE} 42 | gsutil cp $INPUT_FILE gs://${INPUT_BUCKET}/${INPUT_FILE} 43 | 44 | # Delete job if it already exists. 45 | gcloud run jobs delete ${JOB_NAME} --quiet 46 | 47 | echo "Creating ${JOB_NAME} using $IMAGE_NAME, ${NUM_TASKS} tasks, bucket $INPUT_BUCKET, file $INPUT_FILE" 48 | gcloud run jobs create ${JOB_NAME} --execute-now \ 49 | --image $IMAGE_NAME \ 50 | --command python \ 51 | --args process.py \ 52 | --tasks $NUM_TASKS \ 53 | --set-env-vars=INPUT_BUCKET=$INPUT_BUCKET,INPUT_FILE=$INPUT_FILE 54 | 55 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, 4 | and in the interest of fostering an open and welcoming community, 5 | we pledge to respect all people who contribute through reporting issues, 6 | posting feature requests, updating documentation, 7 | submitting pull requests or patches, and other activities. 8 | 9 | We are committed to making participation in this project 10 | a harassment-free experience for everyone, 11 | regardless of level of experience, gender, gender identity and expression, 12 | sexual orientation, disability, personal appearance, 13 | body size, race, ethnicity, age, religion, or nationality. 14 | 15 | Examples of unacceptable behavior by participants include: 16 | 17 | * The use of sexualized language or imagery 18 | * Personal attacks 19 | * Trolling or insulting/derogatory comments 20 | * Public or private harassment 21 | * Publishing other's private information, 22 | such as physical or electronic 23 | addresses, without explicit permission 24 | * Other unethical or unprofessional conduct. 25 | 26 | Project maintainers have the right and responsibility to remove, edit, or reject 27 | comments, commits, code, wiki edits, issues, and other contributions 28 | that are not aligned to this Code of Conduct. 29 | By adopting this Code of Conduct, 30 | project maintainers commit themselves to fairly and consistently 31 | applying these principles to every aspect of managing this project. 32 | Project maintainers who do not follow or enforce the Code of Conduct 33 | may be permanently removed from the project team. 34 | 35 | This code of conduct applies both within project spaces and in public spaces 36 | when an individual is representing the project or its community. 37 | 38 | Instances of abusive, harassing, or otherwise unacceptable behavior 39 | may be reported by opening an issue 40 | or contacting one or more of the project maintainers. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, 43 | available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) 44 | -------------------------------------------------------------------------------- /user-journeys/replay_on_gcp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | export PROJECT_ID=$(gcloud config get-value project) 18 | 19 | # Choose us-central1 if REGION is not defined. 20 | export REGION=${REGION:=us-central1} 21 | 22 | echo "Replaying on Google Cloud" 23 | 24 | no_of_journeys=$(ls journeys/ | wc -l) 25 | echo "Number of journeys: $no_of_journeys" 26 | 27 | echo "Configure your local gcloud to use your project and a region to use for Cloud Run" 28 | gcloud config set project ${PROJECT_ID} 29 | gcloud config set run/region ${REGION} 30 | 31 | echo "Enable required services" 32 | gcloud services enable artifactregistry.googleapis.com run.googleapis.com cloudbuild.googleapis.com 33 | 34 | echo "Create a new Artifact Registry container repository" 35 | gcloud artifacts repositories create containers --repository-format=docker --location=${REGION} 36 | 37 | echo "Build this repository into a container image" 38 | gcloud builds submit -t us-central1-docker.pkg.dev/${PROJECT_ID}/containers/user-journeys-demo 39 | 40 | echo "Create a service account that has no permission, this will ensure replayed user journeys cannot access any of your Google Cloud resources" 41 | gcloud iam service-accounts create no-permission --description="No IAM permission" 42 | 43 | echo "Create a Cloud Run job" 44 | gcloud run jobs create user-journeys-demo \ 45 | --tasks $no_of_journeys \ 46 | --image us-central1-docker.pkg.dev/${PROJECT_ID}/containers/user-journeys-demo:latest \ 47 | --service-account no-permission@${PROJECT_ID}.iam.gserviceaccount.com \ 48 | --memory 1Gi 49 | 50 | echo "Run the Cloud Run job" 51 | gcloud run jobs execute user-journeys-demo 52 | -------------------------------------------------------------------------------- /user-journeys/journeys/go-to-cloud-run-pricing.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "cloud-run", 3 | "steps": [ 4 | { 5 | "type": "setViewport", 6 | "width": 1237, 7 | "height": 721, 8 | "deviceScaleFactor": 1, 9 | "isMobile": false, 10 | "hasTouch": false, 11 | "isLandscape": false 12 | }, 13 | { 14 | "type": "navigate", 15 | "url": "https://cloud.google.com/", 16 | "assertedEvents": [ 17 | { 18 | "type": "navigation", 19 | "url": "https://cloud.google.com/", 20 | "title": "Cloud Computing Services  |  Google Cloud" 21 | } 22 | ] 23 | }, 24 | { 25 | "type": "click", 26 | "selectors": [ 27 | [ 28 | "aria/Products" 29 | ], 30 | [ 31 | "body > section > devsite-header > div > div.devsite-top-logo-row-wrapper-wrapper > div > div > div.devsite-top-logo-row-middle > div.devsite-header-upper-tabs > cloudx-tabs-nav > nav > tab:nth-child(3) > a.gc-analytics-event" 32 | ] 33 | ], 34 | "target": "main", 35 | "offsetX": 45.09375, 36 | "offsetY": 21 37 | }, 38 | { 39 | "type": "click", 40 | "selectors": [ 41 | [ 42 | "body > section > devsite-header > div > div.devsite-top-logo-row-wrapper-wrapper > div > div > div.devsite-top-logo-row-middle > div.devsite-header-upper-tabs > cloudx-tabs-nav > nav > tab:nth-child(3) > div > div.devsite-tabs-dropdown-content > div:nth-child(1) > ul:nth-child(11) > li > a > div.devsite-nav-item-title" 43 | ] 44 | ], 45 | "target": "main", 46 | "offsetX": 33, 47 | "offsetY": 9, 48 | "assertedEvents": [ 49 | { 50 | "type": "navigation", 51 | "url": "https://cloud.google.com/run", 52 | "title": "" 53 | } 54 | ] 55 | }, 56 | { 57 | "type": "click", 58 | "selectors": [ 59 | [ 60 | "aria/View all features" 61 | ], 62 | [ 63 | "#section-2 > div > div:nth-child(2) > div.cws-grid__col--span-8 > a" 64 | ] 65 | ], 66 | "target": "main", 67 | "offsetX": 91, 68 | "offsetY": 26 69 | }, 70 | { 71 | "type": "click", 72 | "selectors": [ 73 | [ 74 | "aria/View pricing details" 75 | ], 76 | [ 77 | "#section-14 > div > div.pricing-module__pricing-cta > a" 78 | ] 79 | ], 80 | "target": "main", 81 | "offsetX": 126, 82 | "offsetY": 28.33331298828125 83 | } 84 | ] 85 | } -------------------------------------------------------------------------------- /user-journeys/runner.js: -------------------------------------------------------------------------------- 1 | console.log('User journey runner is started.') 2 | 3 | import fs from 'fs'; 4 | import puppeteer from 'puppeteer'; 5 | import { createRunner, parse, PuppeteerRunnerExtension } from '@puppeteer/replay'; 6 | 7 | const journeyFolder = './journeys'; 8 | 9 | const replays = fs.readdirSync(journeyFolder); 10 | 11 | if(replays.length === 0) { 12 | console.log({ 13 | message:"No user journey found in the /journeys folder.", 14 | severity: "WARNING", 15 | }); 16 | process.exit(1); 17 | } else { 18 | console.log(`Found ${replays.length} user journeys in folder "journeys"`); 19 | } 20 | 21 | let taskIndex = 0; 22 | 23 | // If this container is running as a Cloud Run job execution 24 | if(process.env.CLOUD_RUN_JOB) { 25 | taskIndex = parseInt(process.env.CLOUD_RUN_TASK_INDEX, 10); 26 | } 27 | 28 | if(taskIndex > replays.length) { 29 | console.error({ 30 | message: `The job has been configured with too many tasks and not enough user journeys. 31 | We recommend using the same number of tasks as user journeys. 32 | Number of journeys found: ${replays.length}. 33 | Index of the current task: ${taskIndexs}. 34 | This process will now exit.`, 35 | severity: "WARNING", 36 | }); 37 | process.exit(1); 38 | } 39 | 40 | // Create an extension that prints at every step of the replay 41 | class Extension extends PuppeteerRunnerExtension { 42 | async beforeEachStep(step, flow) { 43 | await super.beforeEachStep(step, flow); 44 | console.log('Step: ', `${step.type} ${step.url || ''}`); 45 | } 46 | 47 | async afterAllSteps(flow) { 48 | await super.afterAllSteps(flow); 49 | console.log('All steps done'); 50 | } 51 | } 52 | 53 | // Start a browser and new page, needed to initialize the extension. 54 | // TODO: remove these lines if https://github.com/puppeteer/replay/issues/201 is fixed 55 | const browser = await puppeteer.launch({ 56 | headless: true, 57 | }); 58 | const page = await browser.newPage(); 59 | 60 | const recordingText = fs.readFileSync(`./journeys/${replays[taskIndex]}`, 'utf8'); 61 | const recording = parse(JSON.parse(recordingText)); 62 | 63 | console.log(`User journey ${taskIndex} running: ${replays[taskIndex]}`); 64 | const runner = await createRunner(recording, new Extension(browser, page, 7000)); 65 | const result = await runner.run(); 66 | 67 | if(result) { 68 | console.log(`User journey ${taskIndex} completed successfully: ${replays[taskIndex]}`); 69 | console.log('User journey runner has finished, exiting successfully') 70 | process.exit(); 71 | } else { 72 | console.log(`User journey ${taskIndex} completed with errors: ${replays[taskIndex]}`); 73 | console.log('User journey runner has finished, exiting with error') 74 | process.exit(1); 75 | } 76 | -------------------------------------------------------------------------------- /parallel-processing/process.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import hashlib 18 | import math 19 | import os 20 | import time 21 | 22 | import google.auth 23 | from google.cloud import storage 24 | 25 | storage_client = storage.Client() 26 | 27 | _, PROJECT_ID = google.auth.default() 28 | TASK_INDEX = int(os.environ.get("CLOUD_RUN_TASK_INDEX", 0)) 29 | TASK_COUNT = int(os.environ.get("CLOUD_RUN_TASK_COUNT", 1)) 30 | 31 | INPUT_BUCKET = os.environ.get("INPUT_BUCKET", f"input-{PROJECT_ID}") 32 | INPUT_FILE = os.environ.get("INPUT_FILE", "input_file.txt") 33 | 34 | # Process a Cloud Storage object. 35 | def process(): 36 | method_start = time.time() 37 | 38 | # Output useful information about the processing starting. 39 | print( 40 | f"Task {TASK_INDEX}: Processing part {TASK_INDEX} of {TASK_COUNT} " 41 | f"for gs://{INPUT_BUCKET}/{INPUT_FILE}" 42 | ) 43 | 44 | # Download the Cloud Storage object 45 | bucket = storage_client.bucket(INPUT_BUCKET) 46 | blob = bucket.blob(INPUT_FILE) 47 | 48 | # Split blog into a list of strings. 49 | contents = blob.download_as_string().decode("utf-8") 50 | data = contents.split("\n") 51 | 52 | # Determine the chunk size, and identity this task's chunk to process. 53 | chunk_size = math.ceil(len(data) / TASK_COUNT) 54 | chunk_start = chunk_size * TASK_INDEX 55 | chunk_end = chunk_start + chunk_size 56 | 57 | # Process each line in the chunk. 58 | count = 0 59 | loop_start = time.time() 60 | for line in data[chunk_start:chunk_end]: 61 | # Perform your operation here. This is just a placeholder. 62 | _ = hashlib.md5(line.encode("utf-8")).hexdigest() 63 | time.sleep(0.1) 64 | count += 1 65 | 66 | # Output useful information about the processing completed. 67 | time_taken = round(time.time() - method_start, 3) 68 | time_setup = round(loop_start - method_start, 3) 69 | print( 70 | f"Task {TASK_INDEX}: Processed {count} lines " 71 | f"(ln {chunk_start}-{min(chunk_end-1, len(data))} of {len(data)}) " 72 | f"in {time_taken}s ({time_setup}s preparing)" 73 | ) 74 | 75 | 76 | if __name__ == "__main__": 77 | process() 78 | -------------------------------------------------------------------------------- /invoice-processing-pipeline/uploader/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | This web app allows users to upload one or more files to a particular bucket, 17 | where they may later be processed. The canonical use case is for uploading 18 | invoices in human-readable form, with the expectation that the information 19 | in those invoices will be later extracted and handled by an organization's 20 | standard business practices. 21 | 22 | Requirements: 23 | 24 | - Python 3.7 or later 25 | - All packages in requirements.txt installed 26 | - A bucket to place the files in 27 | - Software environment has ADC or other credentials to write to the bucket 28 | - The name of the bucket (not the URI) in the environment variable BUCKET 29 | 30 | This Flask app can be run directly via "python main.py" or with gunicorn 31 | or other common WSGI web servers. 32 | """ 33 | 34 | from flask import Flask, render_template, request 35 | import os 36 | from uuid import uuid4 37 | 38 | from google.cloud import storage 39 | 40 | 41 | app = Flask(__name__) 42 | 43 | 44 | @app.route("/", methods=["GET"]) 45 | def show_upload_page(): 46 | return render_template("index.html"), 200 47 | 48 | 49 | @app.route("/", methods=["POST"]) 50 | def handle_uploads(): 51 | BUCKET_NAME = os.environ.get("BUCKET") 52 | client = storage.Client() 53 | 54 | try: 55 | bucket = client.get_bucket(BUCKET_NAME) 56 | except Exception as e: 57 | return f"Could not open bucket: {e}", 400 58 | 59 | handled = 0 60 | for key in request.files: 61 | for file in request.files.getlist(key): 62 | if uploaded_to_storage(file, bucket): 63 | handled += 1 64 | 65 | return f"Uploaded {handled} file(s)", 200 66 | 67 | 68 | def uploaded_to_storage(file, bucket): 69 | mimetype = file.mimetype 70 | if mimetype is None: 71 | mimetype = "application/octet-stream" 72 | 73 | blob_key = f"incoming/{uuid4()}" 74 | blob = bucket.blob(blob_key) 75 | blob.content_type = mimetype 76 | 77 | blob.upload_from_file(file.stream) 78 | 79 | return True 80 | 81 | 82 | if __name__ == "__main__": 83 | app.run(host="127.0.0.1", port=8080, debug=True) -------------------------------------------------------------------------------- /invoice-processing-pipeline/reviewer/templates/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 47 | 48 | 49 | 50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {% for invoice in invoices %} 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {% endfor %} 77 | 78 |
Unapproved invoices
CompanyInvoice DateDue DateAmount DueViewApprove?
{{ invoice["company"] }}{{ invoice["date"] }}{{ invoice["due_date"] }}{{ invoice["amount_due"] }}View
79 | 80 | 81 |
82 | 83 | -------------------------------------------------------------------------------- /screenshot/screenshot.js: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const puppeteer = require("puppeteer"); 16 | const { Storage } = require("@google-cloud/storage"); 17 | 18 | async function initBrowser() { 19 | console.log("Initializing browser"); 20 | return await puppeteer.launch(); 21 | } 22 | 23 | async function takeScreenshot(browser, url) { 24 | const page = await browser.newPage(); 25 | 26 | console.log(`Navigating to ${url}`); 27 | await page.goto(url); 28 | 29 | console.log(`Taking a screenshot of ${url}`); 30 | return await page.screenshot({ 31 | fullPage: true, 32 | }); 33 | } 34 | 35 | async function createStorageBucketIfMissing(storage, bucketName) { 36 | console.log( 37 | `Checking for Cloud Storage bucket '${bucketName}' and creating if not found` 38 | ); 39 | const bucket = storage.bucket(bucketName); 40 | const [exists] = await bucket.exists(); 41 | if (exists) { 42 | // Bucket exists, nothing to do here 43 | return bucket; 44 | } 45 | 46 | // Create bucket 47 | const [createdBucket] = await storage.createBucket(bucketName); 48 | console.log(`Created Cloud Storage bucket '${createdBucket.name}'`); 49 | return createdBucket; 50 | } 51 | 52 | async function uploadImage(bucket, taskIndex, imageBuffer) { 53 | // Create filename using the current time and task index 54 | const date = new Date(); 55 | date.setMinutes(date.getMinutes() - date.getTimezoneOffset()); 56 | const filename = `${date.toISOString()}-task${taskIndex}.png`; 57 | 58 | console.log(`Uploading screenshot as '${filename}'`); 59 | await bucket.file(filename).save(imageBuffer); 60 | } 61 | 62 | async function main(urls) { 63 | console.log(`Passed in urls: ${urls}`); 64 | 65 | const taskIndex = process.env.CLOUD_RUN_TASK_INDEX || 0; 66 | const url = urls[taskIndex]; 67 | if (!url) { 68 | throw new Error( 69 | `No url found for task ${taskIndex}. Ensure at least ${ 70 | parseInt(taskIndex, 10) + 1 71 | } url(s) have been specified as command args.` 72 | ); 73 | } 74 | const bucketName = process.env.BUCKET_NAME; 75 | if (!bucketName) { 76 | throw new Error( 77 | "No bucket name specified. Set the BUCKET_NAME env var to specify which Cloud Storage bucket the screenshot will be uploaded to." 78 | ); 79 | } 80 | 81 | const browser = await initBrowser(); 82 | const imageBuffer = await takeScreenshot(browser, url).catch(async (err) => { 83 | // Make sure to close the browser if we hit an error. 84 | await browser.close(); 85 | throw err; 86 | }); 87 | await browser.close(); 88 | 89 | console.log("Initializing Cloud Storage client"); 90 | const storage = new Storage(); 91 | const bucket = await createStorageBucketIfMissing(storage, bucketName); 92 | await uploadImage(bucket, taskIndex, imageBuffer); 93 | 94 | console.log("Upload complete!"); 95 | } 96 | 97 | main(process.argv.slice(2)).catch((err) => { 98 | console.error(JSON.stringify({ severity: "ERROR", message: err.message })); 99 | process.exit(1); 100 | }); 101 | -------------------------------------------------------------------------------- /invoice-processing-pipeline/processor/main.py: -------------------------------------------------------------------------------- 1 | # /usr/env/python3 2 | # Copyright 2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | An application to extract information from PDF invoices and update a 18 | database with that information. Intended to run in Cloud Run Jobs. 19 | 20 | Requirements: 21 | 22 | - Python 3.7 or later 23 | - All packages in requirements.txt installed 24 | - A bucket with the invoice files in the /incoming folder 25 | - Firestore database to store information from the invoices 26 | - Software environment has ADC or other credentials to read and write 27 | to and from from the bucket, and to read and to the Firestore database 28 | - The name of the bucket (not the URI) in the environment variable BUCKET 29 | 30 | This app can be run directly via "python main.py". 31 | """ 32 | 33 | 34 | INCOMING_PREFIX = "incoming/" 35 | PROCESSED_PREFIX = "processed/" 36 | FIRST_CHARACTERS = "0123456789abcdef" # Blob names start with one of these 37 | 38 | import os 39 | import process 40 | from helpers import get_project_id 41 | 42 | from google.cloud import storage 43 | 44 | 45 | if __name__ == "__main__": 46 | # Retrieve Jobs-defined env vars (for parallel processing) 47 | TASK_NUM = int(os.getenv("CLOUD_RUN_TASK_INDEX", 0)) 48 | TASK_COUNT = int(os.getenv("CLOUD_RUN_TASK_COUNT", 1)) 49 | ATTEMPT_NUM = int(os.getenv("CLOUD_RUN_TASK_ATTEMPT", 0)) 50 | print(f"Starting attempt {ATTEMPT_NUM} of task {TASK_NUM} of {TASK_COUNT} tasks.") 51 | 52 | chunks = [] 53 | 54 | chars_remaining = FIRST_CHARACTERS 55 | count_remaining = TASK_COUNT 56 | 57 | while count_remaining > 0: 58 | chunk_size = int(len(chars_remaining) / count_remaining) 59 | chunks.append(chars_remaining[:chunk_size]) 60 | chars_remaining = chars_remaining[chunk_size:] 61 | count_remaining -= 1 62 | 63 | print(chunks) 64 | my_chunk = chunks[TASK_NUM] 65 | print(f"My chunk is '{my_chunk}' for task {TASK_NUM}.") 66 | 67 | # Retrieve user-defined env vars 68 | location = "us" 69 | project_id = os.getenv("GOOGLE_CLOUD_PROJECT", get_project_id()) 70 | processor_id = os.environ["PROCESSOR_ID"] 71 | bucket_name = os.environ["BUCKET"] 72 | 73 | client = storage.Client() 74 | 75 | for blob in client.list_blobs(bucket_name, prefix=INCOMING_PREFIX): 76 | # Is this blob our responsibility, or a different task's? 77 | if blob.name[len(INCOMING_PREFIX)] not in my_chunk: 78 | continue # Not my problem 79 | 80 | # Is this really a blob, or a folder? 81 | if blob.name.endswith("/"): 82 | continue # Not my problem 83 | 84 | # Okay, this one is really my responsibility 85 | 86 | # Extract the invoice data 87 | document = process.process_blob( 88 | project_id, location, processor_id, blob) 89 | 90 | # Save to Firestore 91 | process.save_processed_document(document, blob) 92 | 93 | # Move blob to the processed/ folder 94 | bare_name = blob.name[len(INCOMING_PREFIX):] # Drop folder name 95 | new_name = f"{PROCESSED_PREFIX}{bare_name}" 96 | blob.bucket.rename_blob(blob, new_name) 97 | -------------------------------------------------------------------------------- /invoice-processing-pipeline/processor/process.py: -------------------------------------------------------------------------------- 1 | # /usr/env/python3 2 | # Copyright 2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | from pydoc import doc 18 | import re 19 | 20 | from google.cloud import documentai_v1 as documentai 21 | from google.cloud import firestore 22 | from google.cloud import storage 23 | 24 | INCOMING_PREFIX = "incoming/" 25 | 26 | db = firestore.Client() 27 | 28 | 29 | # Use Document AI to examine a PDF invoice, provided as a Cloud Storage Blob, 30 | # and return information in a Document AI object. 31 | def process_blob( 32 | project_id: str, location: str, processor_id: str, blob: storage.blob.Blob 33 | ): 34 | """ 35 | Applies the specified DocumentAI processor to the contents of the Blob 36 | """ 37 | 38 | # Instantiate a synchronous Document AI client 39 | client_options = { 40 | "api_endpoint": "{}-documentai.googleapis.com".format(location)} 41 | client = documentai.DocumentProcessorServiceClient(client_options=client_options) 42 | 43 | # The full resource name of the processor, e.g.: 44 | # projects/project-id/locations/location/processor/processor-id 45 | # You must create new processors in the Cloud Console first 46 | resource_name = client.processor_path(project_id, location, processor_id) 47 | 48 | # Read the file into memory 49 | doc = {"content": blob.download_as_bytes(), "mime_type": blob.content_type} 50 | 51 | # Configure the process request 52 | request = documentai.ProcessRequest(name=resource_name, raw_document=doc) 53 | 54 | # Recognizes text entities in the PDF document 55 | result = client.process_document(request=request) 56 | 57 | return result.document 58 | 59 | 60 | def document_info(document): 61 | info = {"lines": []} 62 | 63 | for entity in document.entities: 64 | if entity.type_ == "line_item": 65 | line = {} 66 | for property in entity.properties: 67 | line[property.type_] = property.mention_text 68 | info["lines"].append(line) 69 | else: 70 | info[entity.type_] = entity.mention_text 71 | 72 | return info 73 | 74 | 75 | # Pull the desired data from the Document AI document and save in Firestore DB 76 | def save_processed_document(document, blob): 77 | collection = os.getenv("COLLECTION", "invoices") 78 | 79 | info = document_info(document) 80 | 81 | total_string = re.sub(r"[,\$]", "", info.get("total_amount", "N/A")) 82 | try: 83 | total = float(total_string) 84 | except: 85 | total = 0.0 86 | 87 | paid_string = re.sub(r"[,\$]", "", info.get("amount_paid_since_last_invoice", "N/A")) 88 | try: 89 | paid = float(paid_string) 90 | except: 91 | paid = 0.0 92 | 93 | rounded_total = "{:.2f}".format(total) 94 | rounded_amount_due = "{:.2f}".format(total - paid) 95 | 96 | data = { 97 | "blob_name": blob.name[len(INCOMING_PREFIX):], 98 | "company": info.get("supplier_name", "Missing name"), 99 | "date": info.get("invoice_date", "N/A").strip(), 100 | "due_date": info.get("due_date", "N/A").strip(), 101 | "total": rounded_total, 102 | "amount_due": rounded_amount_due, 103 | "state": "Not Approved" 104 | } 105 | 106 | db.collection(collection).document(data["blob_name"]).set(data) -------------------------------------------------------------------------------- /invoice-processing-pipeline/reviewer/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Web app to review information extracted from submitted invoices, and 17 | mark the result from the review. 18 | 19 | A separate app enables vendors to submit invoices, and the main Cloud Run 20 | Jobs app extracts the data. 21 | 22 | Requirements: 23 | 24 | - Python 3.7 or later 25 | - All packages in requirements.txt installed 26 | - A bucket with the invoice files in the /processed folder 27 | - Firestore database with information about those invoices 28 | - Software environment has ADC or other credentials to read from the 29 | bucket (in order to display to the reviewer), and to read and write 30 | to the Firestore database (to display information and update status) 31 | - The name of the bucket (not the URI) in the environment variable BUCKET 32 | 33 | This Flask app can be run directly via "python main.py" or with gunicorn 34 | or other WSGI web servers. 35 | """ 36 | 37 | from datetime import timedelta 38 | import os 39 | 40 | from flask import Flask, redirect, render_template, request 41 | 42 | from google import auth 43 | from google.auth.transport import requests 44 | from google.cloud import firestore 45 | from google.cloud import storage 46 | 47 | BUCKET_NAME = os.environ.get("BUCKET") 48 | PROCESSED_PREFIX = "processed/" 49 | APPROVED_PREFIX = "approved/" 50 | 51 | app = Flask(__name__) 52 | 53 | 54 | # GET to / will return a list of processed invoices with data, links, and a form 55 | @app.route("/", methods=["GET"]) 56 | def show_list_to_review(): 57 | # Query the DB for all "Not Approved" invoices 58 | db = firestore.Client() 59 | colref = db.collection("invoices") 60 | query = colref.where("state", "==", "Not Approved") 61 | 62 | # Build data list to work with and then render in a template 63 | invoices = [rec.to_dict() for rec in query.stream()] 64 | 65 | # Will need signed URLs in web page so users can see the PDFs 66 | # Prepare storage client to create those 67 | gcs = storage.Client() 68 | bucket = gcs.get_bucket(BUCKET_NAME) 69 | 70 | # Will need credentials to generate signed URLs 71 | credentials, _ = auth.default() 72 | if credentials.token is None: 73 | credentials.refresh(requests.Request()) 74 | 75 | # Update the data list with signed URLs 76 | for invoice in invoices: 77 | full_name = f"{PROCESSED_PREFIX}{invoice['blob_name']}" 78 | print(f"Blob full name is {full_name}") 79 | blob = bucket.get_blob(full_name) 80 | 81 | # Add the URLs to the list 82 | url = "None" # Fallback that should never be needed 83 | 84 | if blob is not None: 85 | url = blob.generate_signed_url( 86 | version="v4", expiration=timedelta(hours=1), 87 | service_account_email=credentials.service_account_email, 88 | access_token=credentials.token, method="get", scheme="https") 89 | print(f"url is {url}") 90 | 91 | invoice["url"] = url 92 | 93 | # Populate the template with the invoice data and return the page 94 | return render_template("list.html", invoices=invoices), 200 95 | 96 | 97 | # POST to / will note approval of selected invoices 98 | # Approval results in updating DB status and moving PDFs to a different folder 99 | @app.route("/", methods=["POST"]) 100 | def approve_selected_invoices(): 101 | # Will be making changes in DB and Cloud Storage, so prepare clients 102 | db = firestore.Client() 103 | gcs = storage.Client() 104 | bucket = gcs.get_bucket(BUCKET_NAME) 105 | 106 | # Checked boxes will show up as keys in the Flask request form object 107 | for blob_name in request.form.keys(): 108 | # Set the state to Approved in Firestore 109 | docref = db.collection("invoices").document(blob_name) 110 | info = docref.get().to_dict() 111 | info["state"] = "Approved" 112 | docref.set(info) 113 | 114 | # Rename storage blob from PROCESSED_PREFIX to APPROVED_PREFIX 115 | blob = bucket.get_blob(f"{PROCESSED_PREFIX}{blob_name}") 116 | bucket.rename_blob(blob, f"{APPROVED_PREFIX}{blob_name}") 117 | 118 | # Show the home page again to users 119 | return redirect("/") 120 | 121 | 122 | if __name__ == "__main__": 123 | app.run(host="127.0.0.1", port=8080, debug=True) -------------------------------------------------------------------------------- /invoice-processing-pipeline/README.md: -------------------------------------------------------------------------------- 1 | # Cloud Run Jobs Nightly Invoice Processing 2 | 3 | This job uses [Document AI](https://cloud.google.com/document-ai) 4 | to process data from human-readable invoices 5 | in a variety of file formats stored in a 6 | [Cloud Storage](https://cloud.google.com/storage) bucket, 7 | and saves that data in a 8 | [Cloud Firestore](https://cloud.google.com/firestore) database. 9 | 10 | ## The code 11 | 12 | The job being executed is in `processor/main.py`. That program 13 | calls code from the `processor/process.py` module to work with 14 | the Document AI and Cloud Firestore client libraries. 15 | 16 | The Dockerfile manifest defines a minimal container using the official Python image to run a single Python script. 17 | 18 | ## Prepare for the job 19 | 20 | * Create a Google Cloud project using the console or command 21 | line. 22 | 23 | * Define the project region you'll create components in: 24 | 25 | ``` 26 | GOOGLE_CLOUD_PROJECT=<> 27 | GOOGLE_CLOUD_REGION=us-central1 28 | ``` 29 | 30 | * Enable the Cloud Run API, Firestore API, and Cloud Document API. 31 | 32 | ``` 33 | gcloud services enable \ 34 | firestore.googleapis.com \ 35 | run.googleapis.com \ 36 | documentai.googleapis.com 37 | ``` 38 | 39 | * Create the Firestore database: 40 | 41 | ``` 42 | gcloud app create --region=$GOOGLE_CLOUD_REGION 43 | gcloud firestore databases create --project $GOOGLE_CLOUD_PROJECT --region $GOOGLE_CLOUD_REGION 44 | ``` 45 | 46 | * Navigate to the 47 | [Document AI](https://console.cloud.google.com/ai/document-ai) 48 | section and create a new _Invoice Parser_ processor. Learn how to [Create a Document AI processor in the console](https://cloud.google.com/document-ai/docs/create-processor#create-processor). 49 | 50 | * Note the Bucket name and the Document AI Processor ID 51 | which will be used in the command to create the job. 52 | 53 | ``` 54 | export PROCESSOR_ID=<> 55 | export BUCKET=${GOOGLE_CLOUD_PROJECT}-invoices 56 | ``` 57 | 58 | 59 | * Create a bucket in the command line or the console to hold invoices to process. 60 | 61 | ``` 62 | gsutil mb -l $GOOGLE_CLOUD_REGION gs://${BUCKET} 63 | ``` 64 | 65 | * New invoices should be place in a bucket folder called `incoming/` and 66 | the file names should start with a lower-case hex digit 67 | (one of 0123456789abcdef). Naming them with UUID4 value 68 | works well. 69 | 70 | ``` 71 | # Copy provided example invoices to bucket 72 | gsutil cp -r incoming/*.pdf gs://${BUCKET}/incoming 73 | ``` 74 | 75 | 76 | ## Create the Cloud Run Job 77 | 78 | * Cloud Run Jobs can create a job from a container. The 79 | container can be built with a variety of tools, including 80 | Google Cloud Build with the command: 81 | 82 | ``` 83 | gcloud builds submit --tag=gcr.io/$GOOGLE_CLOUD_PROJECT/invoice-processor 84 | ``` 85 | 86 | * Once a container is available in a container repository, create 87 | the job with the command: 88 | 89 | ``` 90 | gcloud run jobs create invoice-processing \ 91 | --image gcr.io/$GOOGLE_CLOUD_PROJECT/invoice-processor \ 92 | --region $GOOGLE_CLOUD_REGION \ 93 | --set-env-vars BUCKET=$BUCKET \ 94 | --set-env-vars PROCESSOR_ID=$PROCESSOR_ID 95 | ``` 96 | 97 | ## Execute the job 98 | 99 | * Execute the job from the command line with the command: 100 | 101 | ``` 102 | gcloud run jobs execute invoice-processing 103 | ``` 104 | 105 | ## The complete pipeline 106 | 107 | ### Create a Cloud Scheduler job 108 | Run your job nightly with a cron job. 109 | 110 | * Create new service account 111 | ``` 112 | gcloud iam service-accounts create process-identity 113 | ``` 114 | 115 | * Give the service account access to invoke the `invoice-processing` job 116 | ``` 117 | gcloud run jobs add-iam-policy-binding invoice-processing \ 118 | --member serviceAccount:process-identity@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com \ 119 | --role roles/run.invoker 120 | ``` 121 | Note: The job does not have a publicly available endpoint; therefore must the Cloud Scheduler Job must have permissions to invoke. 122 | 123 | * Create Cloud Scheduler Job for every day at midnight: 124 | ``` 125 | gcloud scheduler jobs create http my-job \ 126 | --schedule="0 0 * * *" \ 127 | --uri="https://${GOOGLE_CLOUD_REGION}-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/${GOOGLE_CLOUD_PROJECT}/jobs/invoice-processing:run" \ 128 | --http-method=POST \ 129 | --oauth-service-account-email=process-identity@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com 130 | ``` 131 | 132 | ### Deploy supporting services 133 | This repo also includes services for uploading and reviewing the processed invoices. 134 | 135 | * Deploy the Uploader service: 136 | 137 | ``` 138 | gcloud run deploy uploader \ 139 | --source uploader/ \ 140 | --set-env-vars BUCKET=$BUCKET \ 141 | --allow-unauthenticated 142 | ``` 143 | 144 | * Deploy the Reviewer service: 145 | 146 | ``` 147 | gcloud run deploy reviewer \ 148 | --source reviewer/ \ 149 | --set-env-vars BUCKET=$BUCKET \ 150 | --allow-unauthenticated 151 | ``` 152 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /user-journeys/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user-journeys-demo", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "user-journeys-demo", 9 | "version": "1.0.0", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "@puppeteer/replay": "^1.2.0", 13 | "puppeteer": "^17.1.3" 14 | } 15 | }, 16 | "node_modules/@colors/colors": { 17 | "version": "1.5.0", 18 | "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", 19 | "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", 20 | "optional": true, 21 | "engines": { 22 | "node": ">=0.1.90" 23 | } 24 | }, 25 | "node_modules/@puppeteer/replay": { 26 | "version": "1.2.0", 27 | "resolved": "https://registry.npmjs.org/@puppeteer/replay/-/replay-1.2.0.tgz", 28 | "integrity": "sha512-K+j1lWQYpYxvqQqJVkQpZNaFmfYHB5Pp8WBb2nq26tb+drcUWodzy569fc9+kCr+Y2zf4pJZw9frzBmJ+Afhgw==", 29 | "dependencies": { 30 | "cli-table3": "0.6.2", 31 | "colorette": "2.0.19", 32 | "yargs": "17.5.1" 33 | }, 34 | "bin": { 35 | "replay": "lib/cli.js" 36 | }, 37 | "engines": { 38 | "node": ">=14" 39 | }, 40 | "peerDependencies": { 41 | "puppeteer": ">=16.2.0" 42 | }, 43 | "peerDependenciesMeta": { 44 | "puppeteer": { 45 | "optional": true 46 | } 47 | } 48 | }, 49 | "node_modules/@types/node": { 50 | "version": "18.7.18", 51 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz", 52 | "integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==", 53 | "optional": true 54 | }, 55 | "node_modules/@types/yauzl": { 56 | "version": "2.10.0", 57 | "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", 58 | "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", 59 | "optional": true, 60 | "dependencies": { 61 | "@types/node": "*" 62 | } 63 | }, 64 | "node_modules/agent-base": { 65 | "version": "6.0.2", 66 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 67 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 68 | "dependencies": { 69 | "debug": "4" 70 | }, 71 | "engines": { 72 | "node": ">= 6.0.0" 73 | } 74 | }, 75 | "node_modules/ansi-regex": { 76 | "version": "5.0.1", 77 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 78 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 79 | "engines": { 80 | "node": ">=8" 81 | } 82 | }, 83 | "node_modules/ansi-styles": { 84 | "version": "4.3.0", 85 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 86 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 87 | "dependencies": { 88 | "color-convert": "^2.0.1" 89 | }, 90 | "engines": { 91 | "node": ">=8" 92 | }, 93 | "funding": { 94 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 95 | } 96 | }, 97 | "node_modules/balanced-match": { 98 | "version": "1.0.2", 99 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 100 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 101 | }, 102 | "node_modules/base64-js": { 103 | "version": "1.5.1", 104 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 105 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 106 | "funding": [ 107 | { 108 | "type": "github", 109 | "url": "https://github.com/sponsors/feross" 110 | }, 111 | { 112 | "type": "patreon", 113 | "url": "https://www.patreon.com/feross" 114 | }, 115 | { 116 | "type": "consulting", 117 | "url": "https://feross.org/support" 118 | } 119 | ] 120 | }, 121 | "node_modules/bl": { 122 | "version": "4.1.0", 123 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 124 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 125 | "dependencies": { 126 | "buffer": "^5.5.0", 127 | "inherits": "^2.0.4", 128 | "readable-stream": "^3.4.0" 129 | } 130 | }, 131 | "node_modules/brace-expansion": { 132 | "version": "1.1.11", 133 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 134 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 135 | "dependencies": { 136 | "balanced-match": "^1.0.0", 137 | "concat-map": "0.0.1" 138 | } 139 | }, 140 | "node_modules/buffer": { 141 | "version": "5.7.1", 142 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 143 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 144 | "funding": [ 145 | { 146 | "type": "github", 147 | "url": "https://github.com/sponsors/feross" 148 | }, 149 | { 150 | "type": "patreon", 151 | "url": "https://www.patreon.com/feross" 152 | }, 153 | { 154 | "type": "consulting", 155 | "url": "https://feross.org/support" 156 | } 157 | ], 158 | "dependencies": { 159 | "base64-js": "^1.3.1", 160 | "ieee754": "^1.1.13" 161 | } 162 | }, 163 | "node_modules/buffer-crc32": { 164 | "version": "0.2.13", 165 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 166 | "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", 167 | "engines": { 168 | "node": "*" 169 | } 170 | }, 171 | "node_modules/chownr": { 172 | "version": "1.1.4", 173 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 174 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 175 | }, 176 | "node_modules/cli-table3": { 177 | "version": "0.6.2", 178 | "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", 179 | "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", 180 | "dependencies": { 181 | "string-width": "^4.2.0" 182 | }, 183 | "engines": { 184 | "node": "10.* || >= 12.*" 185 | }, 186 | "optionalDependencies": { 187 | "@colors/colors": "1.5.0" 188 | } 189 | }, 190 | "node_modules/cliui": { 191 | "version": "7.0.4", 192 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 193 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 194 | "dependencies": { 195 | "string-width": "^4.2.0", 196 | "strip-ansi": "^6.0.0", 197 | "wrap-ansi": "^7.0.0" 198 | } 199 | }, 200 | "node_modules/color-convert": { 201 | "version": "2.0.1", 202 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 203 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 204 | "dependencies": { 205 | "color-name": "~1.1.4" 206 | }, 207 | "engines": { 208 | "node": ">=7.0.0" 209 | } 210 | }, 211 | "node_modules/color-name": { 212 | "version": "1.1.4", 213 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 214 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 215 | }, 216 | "node_modules/colorette": { 217 | "version": "2.0.19", 218 | "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", 219 | "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" 220 | }, 221 | "node_modules/concat-map": { 222 | "version": "0.0.1", 223 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 224 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 225 | }, 226 | "node_modules/cross-fetch": { 227 | "version": "3.1.5", 228 | "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", 229 | "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", 230 | "dependencies": { 231 | "node-fetch": "2.6.7" 232 | } 233 | }, 234 | "node_modules/debug": { 235 | "version": "4.3.4", 236 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 237 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 238 | "dependencies": { 239 | "ms": "2.1.2" 240 | }, 241 | "engines": { 242 | "node": ">=6.0" 243 | }, 244 | "peerDependenciesMeta": { 245 | "supports-color": { 246 | "optional": true 247 | } 248 | } 249 | }, 250 | "node_modules/devtools-protocol": { 251 | "version": "0.0.1036444", 252 | "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1036444.tgz", 253 | "integrity": "sha512-0y4f/T8H9lsESV9kKP1HDUXgHxCdniFeJh6Erq+FbdOEvp/Ydp9t8kcAAM5gOd17pMrTDlFWntoHtzzeTUWKNw==" 254 | }, 255 | "node_modules/emoji-regex": { 256 | "version": "8.0.0", 257 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 258 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 259 | }, 260 | "node_modules/end-of-stream": { 261 | "version": "1.4.4", 262 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 263 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 264 | "dependencies": { 265 | "once": "^1.4.0" 266 | } 267 | }, 268 | "node_modules/escalade": { 269 | "version": "3.1.1", 270 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 271 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 272 | "engines": { 273 | "node": ">=6" 274 | } 275 | }, 276 | "node_modules/extract-zip": { 277 | "version": "2.0.1", 278 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", 279 | "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", 280 | "dependencies": { 281 | "debug": "^4.1.1", 282 | "get-stream": "^5.1.0", 283 | "yauzl": "^2.10.0" 284 | }, 285 | "bin": { 286 | "extract-zip": "cli.js" 287 | }, 288 | "engines": { 289 | "node": ">= 10.17.0" 290 | }, 291 | "optionalDependencies": { 292 | "@types/yauzl": "^2.9.1" 293 | } 294 | }, 295 | "node_modules/fd-slicer": { 296 | "version": "1.1.0", 297 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", 298 | "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", 299 | "dependencies": { 300 | "pend": "~1.2.0" 301 | } 302 | }, 303 | "node_modules/fs-constants": { 304 | "version": "1.0.0", 305 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 306 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" 307 | }, 308 | "node_modules/fs.realpath": { 309 | "version": "1.0.0", 310 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 311 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" 312 | }, 313 | "node_modules/get-caller-file": { 314 | "version": "2.0.5", 315 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 316 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 317 | "engines": { 318 | "node": "6.* || 8.* || >= 10.*" 319 | } 320 | }, 321 | "node_modules/get-stream": { 322 | "version": "5.2.0", 323 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", 324 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", 325 | "dependencies": { 326 | "pump": "^3.0.0" 327 | }, 328 | "engines": { 329 | "node": ">=8" 330 | }, 331 | "funding": { 332 | "url": "https://github.com/sponsors/sindresorhus" 333 | } 334 | }, 335 | "node_modules/glob": { 336 | "version": "7.2.3", 337 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 338 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 339 | "dependencies": { 340 | "fs.realpath": "^1.0.0", 341 | "inflight": "^1.0.4", 342 | "inherits": "2", 343 | "minimatch": "^3.1.1", 344 | "once": "^1.3.0", 345 | "path-is-absolute": "^1.0.0" 346 | }, 347 | "engines": { 348 | "node": "*" 349 | }, 350 | "funding": { 351 | "url": "https://github.com/sponsors/isaacs" 352 | } 353 | }, 354 | "node_modules/https-proxy-agent": { 355 | "version": "5.0.1", 356 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 357 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 358 | "dependencies": { 359 | "agent-base": "6", 360 | "debug": "4" 361 | }, 362 | "engines": { 363 | "node": ">= 6" 364 | } 365 | }, 366 | "node_modules/ieee754": { 367 | "version": "1.2.1", 368 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 369 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 370 | "funding": [ 371 | { 372 | "type": "github", 373 | "url": "https://github.com/sponsors/feross" 374 | }, 375 | { 376 | "type": "patreon", 377 | "url": "https://www.patreon.com/feross" 378 | }, 379 | { 380 | "type": "consulting", 381 | "url": "https://feross.org/support" 382 | } 383 | ] 384 | }, 385 | "node_modules/inflight": { 386 | "version": "1.0.6", 387 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 388 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 389 | "dependencies": { 390 | "once": "^1.3.0", 391 | "wrappy": "1" 392 | } 393 | }, 394 | "node_modules/inherits": { 395 | "version": "2.0.4", 396 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 397 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 398 | }, 399 | "node_modules/is-fullwidth-code-point": { 400 | "version": "3.0.0", 401 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 402 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 403 | "engines": { 404 | "node": ">=8" 405 | } 406 | }, 407 | "node_modules/minimatch": { 408 | "version": "3.1.2", 409 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 410 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 411 | "dependencies": { 412 | "brace-expansion": "^1.1.7" 413 | }, 414 | "engines": { 415 | "node": "*" 416 | } 417 | }, 418 | "node_modules/mkdirp-classic": { 419 | "version": "0.5.3", 420 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 421 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" 422 | }, 423 | "node_modules/ms": { 424 | "version": "2.1.2", 425 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 426 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 427 | }, 428 | "node_modules/node-fetch": { 429 | "version": "2.6.7", 430 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", 431 | "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", 432 | "dependencies": { 433 | "whatwg-url": "^5.0.0" 434 | }, 435 | "engines": { 436 | "node": "4.x || >=6.0.0" 437 | }, 438 | "peerDependencies": { 439 | "encoding": "^0.1.0" 440 | }, 441 | "peerDependenciesMeta": { 442 | "encoding": { 443 | "optional": true 444 | } 445 | } 446 | }, 447 | "node_modules/once": { 448 | "version": "1.4.0", 449 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 450 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 451 | "dependencies": { 452 | "wrappy": "1" 453 | } 454 | }, 455 | "node_modules/path-is-absolute": { 456 | "version": "1.0.1", 457 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 458 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 459 | "engines": { 460 | "node": ">=0.10.0" 461 | } 462 | }, 463 | "node_modules/pend": { 464 | "version": "1.2.0", 465 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 466 | "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" 467 | }, 468 | "node_modules/progress": { 469 | "version": "2.0.3", 470 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 471 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", 472 | "engines": { 473 | "node": ">=0.4.0" 474 | } 475 | }, 476 | "node_modules/proxy-from-env": { 477 | "version": "1.1.0", 478 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 479 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 480 | }, 481 | "node_modules/pump": { 482 | "version": "3.0.0", 483 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 484 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 485 | "dependencies": { 486 | "end-of-stream": "^1.1.0", 487 | "once": "^1.3.1" 488 | } 489 | }, 490 | "node_modules/puppeteer": { 491 | "version": "17.1.3", 492 | "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-17.1.3.tgz", 493 | "integrity": "sha512-tVtvNSOOqlq75rUgwLeDAEQoLIiBqmRg0/zedpI6fuqIocIkuxG23A7FIl1oVSkuSMMLgcOP5kVhNETmsmjvPw==", 494 | "hasInstallScript": true, 495 | "dependencies": { 496 | "cross-fetch": "3.1.5", 497 | "debug": "4.3.4", 498 | "devtools-protocol": "0.0.1036444", 499 | "extract-zip": "2.0.1", 500 | "https-proxy-agent": "5.0.1", 501 | "progress": "2.0.3", 502 | "proxy-from-env": "1.1.0", 503 | "rimraf": "3.0.2", 504 | "tar-fs": "2.1.1", 505 | "unbzip2-stream": "1.4.3", 506 | "ws": "8.8.1" 507 | }, 508 | "engines": { 509 | "node": ">=14.1.0" 510 | } 511 | }, 512 | "node_modules/readable-stream": { 513 | "version": "3.6.0", 514 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 515 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 516 | "dependencies": { 517 | "inherits": "^2.0.3", 518 | "string_decoder": "^1.1.1", 519 | "util-deprecate": "^1.0.1" 520 | }, 521 | "engines": { 522 | "node": ">= 6" 523 | } 524 | }, 525 | "node_modules/require-directory": { 526 | "version": "2.1.1", 527 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 528 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 529 | "engines": { 530 | "node": ">=0.10.0" 531 | } 532 | }, 533 | "node_modules/rimraf": { 534 | "version": "3.0.2", 535 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 536 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 537 | "dependencies": { 538 | "glob": "^7.1.3" 539 | }, 540 | "bin": { 541 | "rimraf": "bin.js" 542 | }, 543 | "funding": { 544 | "url": "https://github.com/sponsors/isaacs" 545 | } 546 | }, 547 | "node_modules/safe-buffer": { 548 | "version": "5.2.1", 549 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 550 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 551 | "funding": [ 552 | { 553 | "type": "github", 554 | "url": "https://github.com/sponsors/feross" 555 | }, 556 | { 557 | "type": "patreon", 558 | "url": "https://www.patreon.com/feross" 559 | }, 560 | { 561 | "type": "consulting", 562 | "url": "https://feross.org/support" 563 | } 564 | ] 565 | }, 566 | "node_modules/string_decoder": { 567 | "version": "1.3.0", 568 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 569 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 570 | "dependencies": { 571 | "safe-buffer": "~5.2.0" 572 | } 573 | }, 574 | "node_modules/string-width": { 575 | "version": "4.2.3", 576 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 577 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 578 | "dependencies": { 579 | "emoji-regex": "^8.0.0", 580 | "is-fullwidth-code-point": "^3.0.0", 581 | "strip-ansi": "^6.0.1" 582 | }, 583 | "engines": { 584 | "node": ">=8" 585 | } 586 | }, 587 | "node_modules/strip-ansi": { 588 | "version": "6.0.1", 589 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 590 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 591 | "dependencies": { 592 | "ansi-regex": "^5.0.1" 593 | }, 594 | "engines": { 595 | "node": ">=8" 596 | } 597 | }, 598 | "node_modules/tar-fs": { 599 | "version": "2.1.1", 600 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", 601 | "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", 602 | "dependencies": { 603 | "chownr": "^1.1.1", 604 | "mkdirp-classic": "^0.5.2", 605 | "pump": "^3.0.0", 606 | "tar-stream": "^2.1.4" 607 | } 608 | }, 609 | "node_modules/tar-stream": { 610 | "version": "2.2.0", 611 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 612 | "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 613 | "dependencies": { 614 | "bl": "^4.0.3", 615 | "end-of-stream": "^1.4.1", 616 | "fs-constants": "^1.0.0", 617 | "inherits": "^2.0.3", 618 | "readable-stream": "^3.1.1" 619 | }, 620 | "engines": { 621 | "node": ">=6" 622 | } 623 | }, 624 | "node_modules/through": { 625 | "version": "2.3.8", 626 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 627 | "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" 628 | }, 629 | "node_modules/tr46": { 630 | "version": "0.0.3", 631 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 632 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 633 | }, 634 | "node_modules/unbzip2-stream": { 635 | "version": "1.4.3", 636 | "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", 637 | "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", 638 | "dependencies": { 639 | "buffer": "^5.2.1", 640 | "through": "^2.3.8" 641 | } 642 | }, 643 | "node_modules/util-deprecate": { 644 | "version": "1.0.2", 645 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 646 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 647 | }, 648 | "node_modules/webidl-conversions": { 649 | "version": "3.0.1", 650 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 651 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 652 | }, 653 | "node_modules/whatwg-url": { 654 | "version": "5.0.0", 655 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 656 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 657 | "dependencies": { 658 | "tr46": "~0.0.3", 659 | "webidl-conversions": "^3.0.0" 660 | } 661 | }, 662 | "node_modules/wrap-ansi": { 663 | "version": "7.0.0", 664 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 665 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 666 | "dependencies": { 667 | "ansi-styles": "^4.0.0", 668 | "string-width": "^4.1.0", 669 | "strip-ansi": "^6.0.0" 670 | }, 671 | "engines": { 672 | "node": ">=10" 673 | }, 674 | "funding": { 675 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 676 | } 677 | }, 678 | "node_modules/wrappy": { 679 | "version": "1.0.2", 680 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 681 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 682 | }, 683 | "node_modules/ws": { 684 | "version": "8.8.1", 685 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", 686 | "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", 687 | "engines": { 688 | "node": ">=10.0.0" 689 | }, 690 | "peerDependencies": { 691 | "bufferutil": "^4.0.1", 692 | "utf-8-validate": "^5.0.2" 693 | }, 694 | "peerDependenciesMeta": { 695 | "bufferutil": { 696 | "optional": true 697 | }, 698 | "utf-8-validate": { 699 | "optional": true 700 | } 701 | } 702 | }, 703 | "node_modules/y18n": { 704 | "version": "5.0.8", 705 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 706 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 707 | "engines": { 708 | "node": ">=10" 709 | } 710 | }, 711 | "node_modules/yargs": { 712 | "version": "17.5.1", 713 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", 714 | "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", 715 | "dependencies": { 716 | "cliui": "^7.0.2", 717 | "escalade": "^3.1.1", 718 | "get-caller-file": "^2.0.5", 719 | "require-directory": "^2.1.1", 720 | "string-width": "^4.2.3", 721 | "y18n": "^5.0.5", 722 | "yargs-parser": "^21.0.0" 723 | }, 724 | "engines": { 725 | "node": ">=12" 726 | } 727 | }, 728 | "node_modules/yargs-parser": { 729 | "version": "21.1.1", 730 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 731 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 732 | "engines": { 733 | "node": ">=12" 734 | } 735 | }, 736 | "node_modules/yauzl": { 737 | "version": "2.10.0", 738 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", 739 | "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", 740 | "dependencies": { 741 | "buffer-crc32": "~0.2.3", 742 | "fd-slicer": "~1.1.0" 743 | } 744 | } 745 | }, 746 | "dependencies": { 747 | "@colors/colors": { 748 | "version": "1.5.0", 749 | "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", 750 | "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", 751 | "optional": true 752 | }, 753 | "@puppeteer/replay": { 754 | "version": "1.2.0", 755 | "resolved": "https://registry.npmjs.org/@puppeteer/replay/-/replay-1.2.0.tgz", 756 | "integrity": "sha512-K+j1lWQYpYxvqQqJVkQpZNaFmfYHB5Pp8WBb2nq26tb+drcUWodzy569fc9+kCr+Y2zf4pJZw9frzBmJ+Afhgw==", 757 | "requires": { 758 | "cli-table3": "0.6.2", 759 | "colorette": "2.0.19", 760 | "yargs": "17.5.1" 761 | } 762 | }, 763 | "@types/node": { 764 | "version": "18.7.18", 765 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz", 766 | "integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==", 767 | "optional": true 768 | }, 769 | "@types/yauzl": { 770 | "version": "2.10.0", 771 | "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", 772 | "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", 773 | "optional": true, 774 | "requires": { 775 | "@types/node": "*" 776 | } 777 | }, 778 | "agent-base": { 779 | "version": "6.0.2", 780 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 781 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 782 | "requires": { 783 | "debug": "4" 784 | } 785 | }, 786 | "ansi-regex": { 787 | "version": "5.0.1", 788 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 789 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 790 | }, 791 | "ansi-styles": { 792 | "version": "4.3.0", 793 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 794 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 795 | "requires": { 796 | "color-convert": "^2.0.1" 797 | } 798 | }, 799 | "balanced-match": { 800 | "version": "1.0.2", 801 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 802 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 803 | }, 804 | "base64-js": { 805 | "version": "1.5.1", 806 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 807 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" 808 | }, 809 | "bl": { 810 | "version": "4.1.0", 811 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 812 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 813 | "requires": { 814 | "buffer": "^5.5.0", 815 | "inherits": "^2.0.4", 816 | "readable-stream": "^3.4.0" 817 | } 818 | }, 819 | "brace-expansion": { 820 | "version": "1.1.11", 821 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 822 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 823 | "requires": { 824 | "balanced-match": "^1.0.0", 825 | "concat-map": "0.0.1" 826 | } 827 | }, 828 | "buffer": { 829 | "version": "5.7.1", 830 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 831 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 832 | "requires": { 833 | "base64-js": "^1.3.1", 834 | "ieee754": "^1.1.13" 835 | } 836 | }, 837 | "buffer-crc32": { 838 | "version": "0.2.13", 839 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 840 | "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" 841 | }, 842 | "chownr": { 843 | "version": "1.1.4", 844 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 845 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 846 | }, 847 | "cli-table3": { 848 | "version": "0.6.2", 849 | "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", 850 | "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", 851 | "requires": { 852 | "@colors/colors": "1.5.0", 853 | "string-width": "^4.2.0" 854 | } 855 | }, 856 | "cliui": { 857 | "version": "7.0.4", 858 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 859 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 860 | "requires": { 861 | "string-width": "^4.2.0", 862 | "strip-ansi": "^6.0.0", 863 | "wrap-ansi": "^7.0.0" 864 | } 865 | }, 866 | "color-convert": { 867 | "version": "2.0.1", 868 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 869 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 870 | "requires": { 871 | "color-name": "~1.1.4" 872 | } 873 | }, 874 | "color-name": { 875 | "version": "1.1.4", 876 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 877 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 878 | }, 879 | "colorette": { 880 | "version": "2.0.19", 881 | "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", 882 | "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" 883 | }, 884 | "concat-map": { 885 | "version": "0.0.1", 886 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 887 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 888 | }, 889 | "cross-fetch": { 890 | "version": "3.1.5", 891 | "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", 892 | "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", 893 | "requires": { 894 | "node-fetch": "2.6.7" 895 | } 896 | }, 897 | "debug": { 898 | "version": "4.3.4", 899 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 900 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 901 | "requires": { 902 | "ms": "2.1.2" 903 | } 904 | }, 905 | "devtools-protocol": { 906 | "version": "0.0.1036444", 907 | "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1036444.tgz", 908 | "integrity": "sha512-0y4f/T8H9lsESV9kKP1HDUXgHxCdniFeJh6Erq+FbdOEvp/Ydp9t8kcAAM5gOd17pMrTDlFWntoHtzzeTUWKNw==" 909 | }, 910 | "emoji-regex": { 911 | "version": "8.0.0", 912 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 913 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 914 | }, 915 | "end-of-stream": { 916 | "version": "1.4.4", 917 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 918 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 919 | "requires": { 920 | "once": "^1.4.0" 921 | } 922 | }, 923 | "escalade": { 924 | "version": "3.1.1", 925 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 926 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" 927 | }, 928 | "extract-zip": { 929 | "version": "2.0.1", 930 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", 931 | "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", 932 | "requires": { 933 | "@types/yauzl": "^2.9.1", 934 | "debug": "^4.1.1", 935 | "get-stream": "^5.1.0", 936 | "yauzl": "^2.10.0" 937 | } 938 | }, 939 | "fd-slicer": { 940 | "version": "1.1.0", 941 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", 942 | "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", 943 | "requires": { 944 | "pend": "~1.2.0" 945 | } 946 | }, 947 | "fs-constants": { 948 | "version": "1.0.0", 949 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 950 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" 951 | }, 952 | "fs.realpath": { 953 | "version": "1.0.0", 954 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 955 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" 956 | }, 957 | "get-caller-file": { 958 | "version": "2.0.5", 959 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 960 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" 961 | }, 962 | "get-stream": { 963 | "version": "5.2.0", 964 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", 965 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", 966 | "requires": { 967 | "pump": "^3.0.0" 968 | } 969 | }, 970 | "glob": { 971 | "version": "7.2.3", 972 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 973 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 974 | "requires": { 975 | "fs.realpath": "^1.0.0", 976 | "inflight": "^1.0.4", 977 | "inherits": "2", 978 | "minimatch": "^3.1.1", 979 | "once": "^1.3.0", 980 | "path-is-absolute": "^1.0.0" 981 | } 982 | }, 983 | "https-proxy-agent": { 984 | "version": "5.0.1", 985 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 986 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 987 | "requires": { 988 | "agent-base": "6", 989 | "debug": "4" 990 | } 991 | }, 992 | "ieee754": { 993 | "version": "1.2.1", 994 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 995 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" 996 | }, 997 | "inflight": { 998 | "version": "1.0.6", 999 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1000 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 1001 | "requires": { 1002 | "once": "^1.3.0", 1003 | "wrappy": "1" 1004 | } 1005 | }, 1006 | "inherits": { 1007 | "version": "2.0.4", 1008 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1009 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1010 | }, 1011 | "is-fullwidth-code-point": { 1012 | "version": "3.0.0", 1013 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1014 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 1015 | }, 1016 | "minimatch": { 1017 | "version": "3.1.2", 1018 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1019 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1020 | "requires": { 1021 | "brace-expansion": "^1.1.7" 1022 | } 1023 | }, 1024 | "mkdirp-classic": { 1025 | "version": "0.5.3", 1026 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 1027 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" 1028 | }, 1029 | "ms": { 1030 | "version": "2.1.2", 1031 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1032 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 1033 | }, 1034 | "node-fetch": { 1035 | "version": "2.6.7", 1036 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", 1037 | "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", 1038 | "requires": { 1039 | "whatwg-url": "^5.0.0" 1040 | } 1041 | }, 1042 | "once": { 1043 | "version": "1.4.0", 1044 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1045 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1046 | "requires": { 1047 | "wrappy": "1" 1048 | } 1049 | }, 1050 | "path-is-absolute": { 1051 | "version": "1.0.1", 1052 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1053 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" 1054 | }, 1055 | "pend": { 1056 | "version": "1.2.0", 1057 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 1058 | "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" 1059 | }, 1060 | "progress": { 1061 | "version": "2.0.3", 1062 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 1063 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" 1064 | }, 1065 | "proxy-from-env": { 1066 | "version": "1.1.0", 1067 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 1068 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 1069 | }, 1070 | "pump": { 1071 | "version": "3.0.0", 1072 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 1073 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 1074 | "requires": { 1075 | "end-of-stream": "^1.1.0", 1076 | "once": "^1.3.1" 1077 | } 1078 | }, 1079 | "puppeteer": { 1080 | "version": "17.1.3", 1081 | "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-17.1.3.tgz", 1082 | "integrity": "sha512-tVtvNSOOqlq75rUgwLeDAEQoLIiBqmRg0/zedpI6fuqIocIkuxG23A7FIl1oVSkuSMMLgcOP5kVhNETmsmjvPw==", 1083 | "requires": { 1084 | "cross-fetch": "3.1.5", 1085 | "debug": "4.3.4", 1086 | "devtools-protocol": "0.0.1036444", 1087 | "extract-zip": "2.0.1", 1088 | "https-proxy-agent": "5.0.1", 1089 | "progress": "2.0.3", 1090 | "proxy-from-env": "1.1.0", 1091 | "rimraf": "3.0.2", 1092 | "tar-fs": "2.1.1", 1093 | "unbzip2-stream": "1.4.3", 1094 | "ws": "8.8.1" 1095 | } 1096 | }, 1097 | "readable-stream": { 1098 | "version": "3.6.0", 1099 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 1100 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 1101 | "requires": { 1102 | "inherits": "^2.0.3", 1103 | "string_decoder": "^1.1.1", 1104 | "util-deprecate": "^1.0.1" 1105 | } 1106 | }, 1107 | "require-directory": { 1108 | "version": "2.1.1", 1109 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1110 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" 1111 | }, 1112 | "rimraf": { 1113 | "version": "3.0.2", 1114 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 1115 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 1116 | "requires": { 1117 | "glob": "^7.1.3" 1118 | } 1119 | }, 1120 | "safe-buffer": { 1121 | "version": "5.2.1", 1122 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1123 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 1124 | }, 1125 | "string_decoder": { 1126 | "version": "1.3.0", 1127 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1128 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1129 | "requires": { 1130 | "safe-buffer": "~5.2.0" 1131 | } 1132 | }, 1133 | "string-width": { 1134 | "version": "4.2.3", 1135 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1136 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1137 | "requires": { 1138 | "emoji-regex": "^8.0.0", 1139 | "is-fullwidth-code-point": "^3.0.0", 1140 | "strip-ansi": "^6.0.1" 1141 | } 1142 | }, 1143 | "strip-ansi": { 1144 | "version": "6.0.1", 1145 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1146 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1147 | "requires": { 1148 | "ansi-regex": "^5.0.1" 1149 | } 1150 | }, 1151 | "tar-fs": { 1152 | "version": "2.1.1", 1153 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", 1154 | "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", 1155 | "requires": { 1156 | "chownr": "^1.1.1", 1157 | "mkdirp-classic": "^0.5.2", 1158 | "pump": "^3.0.0", 1159 | "tar-stream": "^2.1.4" 1160 | } 1161 | }, 1162 | "tar-stream": { 1163 | "version": "2.2.0", 1164 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 1165 | "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 1166 | "requires": { 1167 | "bl": "^4.0.3", 1168 | "end-of-stream": "^1.4.1", 1169 | "fs-constants": "^1.0.0", 1170 | "inherits": "^2.0.3", 1171 | "readable-stream": "^3.1.1" 1172 | } 1173 | }, 1174 | "through": { 1175 | "version": "2.3.8", 1176 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 1177 | "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" 1178 | }, 1179 | "tr46": { 1180 | "version": "0.0.3", 1181 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 1182 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 1183 | }, 1184 | "unbzip2-stream": { 1185 | "version": "1.4.3", 1186 | "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", 1187 | "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", 1188 | "requires": { 1189 | "buffer": "^5.2.1", 1190 | "through": "^2.3.8" 1191 | } 1192 | }, 1193 | "util-deprecate": { 1194 | "version": "1.0.2", 1195 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1196 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 1197 | }, 1198 | "webidl-conversions": { 1199 | "version": "3.0.1", 1200 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 1201 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 1202 | }, 1203 | "whatwg-url": { 1204 | "version": "5.0.0", 1205 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 1206 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 1207 | "requires": { 1208 | "tr46": "~0.0.3", 1209 | "webidl-conversions": "^3.0.0" 1210 | } 1211 | }, 1212 | "wrap-ansi": { 1213 | "version": "7.0.0", 1214 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1215 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1216 | "requires": { 1217 | "ansi-styles": "^4.0.0", 1218 | "string-width": "^4.1.0", 1219 | "strip-ansi": "^6.0.0" 1220 | } 1221 | }, 1222 | "wrappy": { 1223 | "version": "1.0.2", 1224 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1225 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 1226 | }, 1227 | "ws": { 1228 | "version": "8.8.1", 1229 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", 1230 | "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", 1231 | "requires": {} 1232 | }, 1233 | "y18n": { 1234 | "version": "5.0.8", 1235 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1236 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" 1237 | }, 1238 | "yargs": { 1239 | "version": "17.5.1", 1240 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", 1241 | "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", 1242 | "requires": { 1243 | "cliui": "^7.0.2", 1244 | "escalade": "^3.1.1", 1245 | "get-caller-file": "^2.0.5", 1246 | "require-directory": "^2.1.1", 1247 | "string-width": "^4.2.3", 1248 | "y18n": "^5.0.5", 1249 | "yargs-parser": "^21.0.0" 1250 | } 1251 | }, 1252 | "yargs-parser": { 1253 | "version": "21.1.1", 1254 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 1255 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" 1256 | }, 1257 | "yauzl": { 1258 | "version": "2.10.0", 1259 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", 1260 | "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", 1261 | "requires": { 1262 | "buffer-crc32": "~0.2.3", 1263 | "fd-slicer": "~1.1.0" 1264 | } 1265 | } 1266 | } 1267 | } 1268 | --------------------------------------------------------------------------------