├── .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 |
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 |
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 |
--------------------------------------------------------------------------------